Paso 11: Three.js: cámara de posicionamiento
Control de la cámara es probablemente la parte más difícil de trabajar con ThreeJS. Animo fuertemente a todos no te molestes con este ejercicio de frustración. Elige uno de los controladores de cámara genérico ejemplo, inserte el script en tu página y disfrutar. Si necesita calcular parámetros de la cámara de medida, es probablemente mejor fuera aprendiendo sobre Quaternions, que no lo hice.
El camino plain-Jane trig te deja en un mundo de dolor. No tiene esencialmente útiles para depurar sus matemáticas, e incluso una vez que la matemática es correcta los resultados pueden mirar mal a causa de cosas como la "orden de Euler" (que se debe establecer a YXZ si desea que los controles de la cámara sienta como Pitch, Roll y Yaw... set camera.rotation.order = "YXZ";). Rotaciones de la cámara están en el sistema de coordenadas de la cámara, así que siempre tienes que recordar darle instrucciones camino o uso el hacky "objetivo" y "lookAt" estrategia (que) que invariablemente lleva a orientaciones totalmente chifladas... que es cuando empiezas a tener que configurar "para arriba" vector la cámara manualmente para mantenerlo hacia arriba y usted siempre debe estar pendiente de cosas como cerradura del cardán (donde pierde un grado de libertad debido a dos ejes paralelos en la cámara) y el hecho de que interpolación los * valores * de vectores de rotación de la cámara puede tomar correctamente de la A--> B en el espacio, pero a lo largo de la dirección de rotación errónea. Puede pensar que es natural de giro de 180 grados girando la cabeza hacia los lados, pero el camino más simple de rotación bien puede ser que va recto arriba en su lugar. Blech!
No voy a llevarte a través de las soluciones a todos esos problemas, y si te fijas bien encontrarás que mis soluciones son, en algunos lugares, un poco áspero. En cambio, voy a pegar todo el código de movimiento de la cámara junto con notas poco aquí sobre qué partes de él. Si usted está realmente zambullirse en una pieza y le gustaría una explicación más detallada, seguir adelante y publicar un comentario así puedo encarnar de esa parte.
Criterio de la cámara: destaca
-Poner una estrella seleccionada en el centro de la pantalla (zoomAndDollyToPoint)
-Poner una estrella seleccionada en el centro de la pantalla con el centro de la galaxia en el fondo para evitar que la navegación fuera del borde del universo (zoomToFitPointsFrom, CAMERA_RELATION. TOWARD_CENTER)
-Pasar de una estrella seleccionada a otro sin cambiar de ángulo de la cámara (strafeFromPointToPoint)
-Encontrar el ámbito delimitador para un grupo de estrellas y luego buscar una posición de la cámara que esas estrellas cabrían en la pantalla (zoomToFitPointsFrom, todos CAMERA_RELATIONs)
-Poner estrellas adyacentes en el mismo cluster en posiciones de pantalla cómodo en relación a un solo "seleccionado" estrellas en la constelación de un autor (showThreePointsNicely)
-Volver a una posición "inicial" (reset)
-Parametrización de una trayectoria para la cámara por lo que puede poco a poco volar a través de la galaxia en su propia cuando no asistió (esperar los 90 sin hacer clic para ver la animación de inicio)-- (beginAutomaticTravel y cameraSetupForParameter)
El código:
Galaxy.Settings = Galaxy.Settings || {}; Galaxy.CameraMotions = function(camera) {_.bindAll(this,'zoomToFitPointsFrom','startAnimation','endAnimation','cameraSetupForParameter','beginAutomaticTravel'); this.target = Galaxy.Settings.cameraDefaultTarget.clone(); this.camera = cámara; / / borrar esta propiedad finalmente this.firstClick = true; this.isAnimating = false;} Galaxy.CameraMotions.prototype = {constructor: Galaxy.CameraMotions, startAnimation: function() {/ / startAnimation se refiere a animaciones iniciada por el usuario. La animación por defecto debe quitarse si continua. this.endAutomaticTravel(); this.isAnimating = true; }, endAnimation: function() {this.isAnimating = false;}, zoomAndDollyToPoint: function(point,callback) {si (this.isAnimating === verdadero) volver; / / temporalmente: el primer clic se zoom y a bombardear después de que. Si (this.firstClick === false) {/ / this.strafeFromPointToPoint(this.target,point,callback); this.zoomToFitPointsFrom ([punto], este. CAMERA_RELATION. TOWARD_CENTER, callback); retorno; } this.firstClick = false; var que = esta, pointClone = point.clone(), cameraPath = this.cameraPathToPoint(this.camera.position.clone(), point.clone()), currentPosition = {ahora: 0}, duración = 1.3, upClone = Galaxy.Settings.cameraDefaultUp.clone(), targetCurrent = this.target.clone(); TweenMax.to (targetCurrent, duración/1.5 {x:pointClone.x, y:pointClone.y, z:pointClone.z}); TweenMax.to (currentPosition, duración, {ahora: 0.8, onUpdate: function() {var pos = cameraPath.getPoint(currentPosition.now); that.target = tres nuevos. Vector3(targetCurrent.x,targetCurrent.y,targetCurrent.z); that.Camera.position.set(pos.x,pos.y,pos.z); that.Camera.up.set(upClone.x,upClone.y,upClone.z); that.camera.lookAt(that.target); that.camera.updateProjectionMatrix(); }, onStart: that.startAnimation, onComplete: function() {that.endAnimation(); si (typeof callback === "función") callback();}}); }, cameraPathToPoint: function(fromPoint,toPoint) {var spline = tres nuevos. SplineCurve3 ([fromPoint, tres nuevos. Vector3 ((toPoint.x-fromPoint.x) * 0,5 + fromPoint.x, (toPoint.y-fromPoint.y) * 0,5 + fromPoint.y, (toPoint.z-fromPoint.z) * 0,7 + fromPoint.z), apunto]); volver la tira; }, strafeFromPointToPoint: function(fromPoint,toPoint,callback) {var dest = toPoint.clone(), actual = this.camera.position.clone(), duración = 0.5, que = esto; dest.sub(fromPoint.clone()); dest.add(current.clone()); / / console.log("\n\n",fromPoint,toPoint,current,dest); si (that.isAnimating === true) volver; TweenMax.to (this.camera.position,duration, {x: dest.x,y: dest.y, z: dest.z, onComplete: function() {that.endAnimation(); that.camera.lookAt(toPoint.clone()); that.target = toPoint.clone(); if (typeof callback === "función") callback();}, onStart: that.startAnimation})}, reset: function(callback) {var duración = 2, que = esto, Inicio = Galaxy.Settings.cameraDefaultPosition.clone(), centro = Galaxy.Settings.cameraDefaultTarget.clone(), upGoal = Galaxy.Settings.cameraDefaultUp.clone(), upCurrent = this.camera.up.clone(), targetCurrent = this.target.clone(), positionCurrent = this.camera.position.clone(); / / no hacer nada cuando nada es suficiente. La devolución de llamada no debe tener ningún retraso. Si (this.camera.up.equals(Galaxy.Settings.cameraDefaultUp) & & this.camera.position.equals(Galaxy.Settings.cameraDefaultPosition) & & this.target.equals(Galaxy.Settings.cameraDefaultTarget)) {duración = 0.1;} si (that.isAnimating === true) volver; TweenMax.to (upCurrent, duración/1.5, {x: upGoal.x,y: upGoal.y,z: upGoal.z}); TweenMax.to (targetCurrent, duración/1.5, {x: center.x,y: center.y, z: center.z}); TweenMax.to (positionCurrent, duración, {x: home.x,y: home.y, z: home.z,ease: Power1.easeInOut, onUpdate: function() {that.target = tres nuevos. Vector3(targetCurrent.x,targetCurrent.y,targetCurrent.z); that.Camera.position.set(positionCurrent.x,positionCurrent.y,positionCurrent.z); that.Camera.up.set (upCurrent.x,upCurrent.y,upCurrent.z); that.camera.lookAt(that.target.clone()); that.camera.updateProjectionMatrix(); }, onComplete: function() {that.endAnimation(); that.firstClick = true; if (typeof callback === "función") callback();}, onStart: that.startAnimation})}, CAMERA_RELATION: {arriba: 0, SAME_ANGLE: 1, TOWARD_CENTER: 2}, zoomToFitPointsFrom: function(pointList,cameraRelation,callback) {si (! _.has (_.values (esto. CAMERA_RELATION), cameraRelation)) {/ / console.log (_.values (esto. CAMERA_RELATION)); Console.error (cameraRelation + "no es uno de los RELATIVE_LOCATION"); retorno; } Si (this.isAnimating == true) volver; pointList asumida que ya está en coordenadas de mundo. Averiguar delimitador de la esfera, a continuación, mover la cámara con respecto a su centro var bSphere = tres nuevos. Esfera (tres nuevos. Vector3(0,0,0),5); bSphere.setFromPoints(pointList); a qué distancia tenemos que para esta esfera? var targetDistance = (bSphere.radius / (Math.tan(Math.PI*this.camera.fov/360))), cameraPositionEnd, que =, duración = 1, = hasta this.camera.up.clone(), currentCameraPosition = this.camera.position.clone(); interruptor (cameraRelation) {caso 0: / / CAMERA_RELATION. ARRIBA cameraPositionEnd = bSphere.center.clone () Add (tres nuevos. Vector3(40,40,targetDistance)); rotura; caso 1: / / CAMERA_RELATION. SAME_ANGLE dollies la cámara de entrada/salida que estos puntos se convierten en visibles var centro = bSphere.center.clone(), currentPos = that.camera.position.clone(), finalViewAngle = currentPos.sub(center).setLength(targetDistance); cameraPositionEnd = bSphere.center.clone().add(finalViewAngle); para evitar atravesar el plano de fondo: cameraPositionEnd.z = Math.max(cameraPositionEnd.z,40); rotura; caso 2: / / CAMERA_RELATION. TOWARD_CENTER dibuja una línea desde el origen del mundo a través del punto central de la delimitación de la esfera, / / y pone la cámara en el extremo de un vector de dos veces esa longitud. cameraPositionEnd = bSphere.center.clone().multiplyScalar(2); Si cameraPositionEnd.setLength(125) (cameraPositionEnd.length() < 125); Es raro cuando la cámara consigue demasiado cerca a las estrellas en la rotura del media; } var cameraTargetCurrent = {x: this.target.x, y: this.target.y z: this.target.z}; var cameraTargetEnd = bSphere.center.clone(); that.logVec('up',that.camera.up.clone()); that.logVec('target',that.target.clone()); that.logVec('position',that.camera.position.clone()); TweenMax.to (cameraTargetCurrent, duración/1.5, {x: cameraTargetEnd.x,y: cameraTargetEnd.y, z: cameraTargetEnd.z}); NO cambie "up" de alto ángulo. Se pone chiflado y gira la cámara desagradable. Si (cameraRelation! == 0) {TweenMax.to (up, duración/1.5 {x: 0, y: 0, z: 1});} TweenMax.to (currentCameraPosition, duración, {x: cameraPositionEnd.x,y: cameraPositionEnd.y, cameraPositionEnd.z z:, onUpdate: function() {that.target = tres nuevos. Vector3(cameraTargetCurrent.x,cameraTargetCurrent.y,cameraTargetCurrent.z); that.Camera.position.set(currentCameraPosition.x,currentCameraPosition.y,currentCameraPosition.z); that.Camera.up.set (up.x,up.y,up.z); that.camera.lookAt(that.target.clone()); that.camera.updateProjectionMatrix(); }, onComplete: function() {/ / that.logVec('up',that.camera.up.clone()); / / that.logVec('target',that.target.clone()); / / that.logVec('position',that.camera.position.clone()); that.endAnimation(); if (typeof callback === "función") callback();}, onStart: that.startAnimation})}, showThreePointsNicely: función (pointList, callback) {/ / buscar una ubicación de la cámara y la rotación tal que aparece el primer punto hacia la parte inferior de la pantalla, y / o los otros dos aparecen y a la izquierda y derecha. O algo así. Si (this.isAnimating == true) volver; this.firstClick = false; Si (! _.isArray(pointList)) {throw new Error ("matriz de puntos para showThreePointsNicely");} else if (pointList.length! == 3) {/ / Mostrar sólo la primera de ellas. this.zoomAndDollyToPoint(pointList[0],callback); return;} var pointZero = pointList[0].clone(); mirar el mundo desde la perspectiva de la estrella que se centrarán: var viewFromPointZero = function(vector) {return vector.clone().sub(pointZero.clone());}; El vector "atraviesan" es un ángulo central entre las estrellas de izquierda y derecha que estamos tratando de hacer visible en pantalla, junto con el vector cero var bisectLocal = viewFromPointZero (pointList [1]) Add (viewFromPointZero (pointList[2])).multiplyScalar(0.5); La ruta lineal sería descrito como... var A = viewFromPointZero(pointList[1]); var B = viewFromPointZero(pointList[2]); var theta = Math.acos(A.clone().dot(B.clone()) / A.length() / B.length()); var distanceAwayBasedOnAngle = Math.min(Math.max(theta*2.5,2),4); var cameraEndPosition = pointZero.clone().sub(bisectLocal.clone().multiplyScalar(distanceAwayBasedOnAngle)); var cameraStartPosition = this.camera.position.clone(); var cameraPathMidpoint = cameraEndPosition.clone().add(cameraStartPosition.clone()).multiplyScalar(0.5); La trayectoria circular alrededor de punto medio de la trayectoria lineal, entonces sería: Radio var = cameraStartPosition.clone().sub(cameraPathMidpoint.clone()).length(); var que = esto; var worldPointOnCircularPath = function(t) {var x = radius * Math.cos(t); var y = radio * Math.sin(t); var vectorPointLocal = tres nuevos. Vector3(x,y,0); volver vectorPointLocal.add(cameraPathMidpoint.clone()); }; backsolve el ángulo de inicio de la trayectoria circular. Es la inversa de x=a+r*cos(theta) = > theta = acos((x-a)/r); var startPointRelativeToMidpoint = cameraStartPosition.clone().sub(cameraPathMidpoint.clone()); startAngle var = Math.atan(startPointRelativeToMidpoint.y/startPointRelativeToMidpoint.x); Es el ángulo de inicio o el ángulo final? Es uno o el otro, pero tenemos que saber que... Otro sea + PI si (worldPointOnCircularPath(startAngle).setZ(0).distanceTo(cameraStartPosition.clone().setZ(0)) > 100) {/ / tienes que comenzar a mitad de camino alrededor en lugar de otro. tal es el mundo de la inversa trigonométricas funciones startAngle += Math.PI;} parámetros var = {t: startAngle, z: cameraStartPosition.clone () .z}, duración = 2.0, hasta = that.camera.up.clone(); var pointZeroClone = pointZero.clone(); TweenMax.to (that.target,duration/1.5, {x: pointZeroClone.x,y: pointZeroClone.y,z: pointZeroClone.z}); TweenMax.to (up, duración/1.5 {x: 0, y: 0, z: 1}); TweenMax.to (parámetros, duración, {t: startAngle - Math.PI, z: cameraEndPosition.clone () .z, facilidad: Power1.easeOut, onUpdate: function() {var xyCurrent = worldPointOnCircularPath(parameters.t); that.camera.position.set(xyCurrent.x,xyCurrent.y,parameters.z); that.camera.up.set (up.x,up.y,up.z); that.camera.lookAt(that.target); that.camera.updateProjectionMatrix();}, onComplete: function() {that.endAnimation(); si (typeof callback === "función") callback();}, onStart: that.startAnimation}); }, / / La cámara puede también "viajar" en el modo desatendido. Este comportamiento requiere algo de código para definir paramétricamente y luego animar el camino complejo, / / pero es algo diferente en especie de los movimientos de cámara iniciada por el usuario descritos anteriormente. beginAutomaticTravel: function() {/ / esta función absolutamente positivamente debe comenzar desde las posiciones de casa cámara. / / console.log ('comenzando el recorrido automático de la cámara'); var obj = {cameraParameter: Math.PI/2}, ese = este, loopConstants = this.loopConstants(); this.reset(function() {eso .__automaticCameraAnimation = TweenMax.to (obj, loopConstants.duration, {cameraParameter: 5*Math.PI/2, onUpdate:function() {that.cameraSetupForParameter(obj.cameraParameter,loopConstants);}, facilidad: null, repita: -1 / / loop infinitamente});});}, loopConstants: function() {var galaxyLoopStart = tres nuevos. Vector3(200,0,15), targetLoopStart = tres nuevos. Vector3(200,200,5), upLoopStart = tres nuevos. Vector3(0,0,1); volver {duración: 400, / segundos galaxyLoopStart: galaxyLoopStart, targetLoopStart: targetLoopStart, upLoopStart: upLoopStart, galaxyLoopToHome: Galaxy.Settings.cameraDefaultPosition.clone().sub(galaxyLoopStart.clone()), targetLoopToHome: Galaxy.Settings.cameraDefaultTarget.clone().sub(targetLoopStart.clone()), upLoopToHome: Galaxy.Settings.cameraDefaultUp.clone().sub(upLoopStart.clone())}}, cameraSetupForParameter: function(cameraParameter,loopConstants) {var pos, lookAt = loopConstants.targetLoopStart.clone (), hasta = loopConstants.upLoopStart.clone(); if (cameraParameter < 2*Math.PI & & cameraParameter > Math.PI) {cameraParameter-= Math.PI; / / vaya un círculo completo alrededor de las galaxia pos = tres nuevos. Vector3(200*Math.cos(cameraParameter),200*Math.sin(cameraParameter),15); copia de var = pos.clone(); lookAt = tres nuevos. Vector3 (copy.x, copy.y + copy.x,5); } else {/ / después de pasar un círculo completo alrededor de la galaxia, animar a todas las características de la cámara a la posición "Inicio", luego repita de inicio var pathMultiplier = Math.sin (cameraParameter); / / bueno de 0 a PI. Fuera de dicho rango, esto va negativo y parece haywire. pos = loopConstants.galaxyLoopStart.clone().add(loopConstants.galaxyLoopToHome.clone().multiplyScalar(pathMultiplier)); lookAt = loopConstants.targetLoopStart.clone().add(loopConstants.targetLoopToHome.clone().multiplyScalar(pathMultiplier)); alto = loopConstants.upLoopStart.clone().add(loopConstants.upLoopToHome.clone().multiplyScalar(pathMultiplier)); } this.camera.position.set(pos.x,pos.y,pos.z); this.Camera.up.set(up.x,up.y,up.z); this.Target = lookAt; this.camera.lookAt(lookAt); this.camera.updateProjectionMatrix(); }, endAutomaticTravel: function() {si (este .__automaticCameraAnimation) {this.__automaticCameraAnimation.kill();}}, / / logVec de herramientas de depuración: function(message,vec) {console.log (mensaje + ":" + vec.x + "" + vec.y + "" + vec.z);}, addTestCubeAtPosition: function(position) {var cubo = tres nuevos. Malla (tres nuevos. CubeGeometry (5, 5, 5), tres nuevos. MeshNormalMaterial()); Cube.Position = position.clone(); Galaxy.Scene.add (cubo); } }