Etape 11 : Three.js : positionnement de la caméra
Contrôle de l’appareil photo est probablement la partie la plus difficile de travailler avec ThreeJS. Je vous encourage fortement à chacun de ne pas s’embêter avec cet exercice dans la frustration. Choisissez un des contrôleurs génériques appareil photo exemple, insérez le script sur votre page et profitez. Si vous avez besoin calculer les paramètres de caméra personnalisée, vous êtes probablement mieux lotis d’apprentissage sur les Quaternions, dont je n’ai pas.
Aller l’itinéraire trig plat vous laisse dans un monde de douleur. Vous n’avez essentiellement aucun outil utile pour déboguer vos maths, et même une fois que votre calcul est juste les résultats peuvent semblent mal à cause de choses comme le "ordre d’Euler" (dont vous devez affecter YXZ si vous souhaitez que les contrôles de la caméra pour se sentir comme Pitch, roulis et lacet... juste définie camera.rotation.order = "YXZ";). Rotations de la caméra sont dans le système de coordonnées de la caméra, il faut toujours se rappeler de lui donner des instructions cette façon ou utiliser le hacky « cible » et « lookAt » stratégie (que j’ai fait) qui invariablement vous conduira aux orientations totalement tordues... c’est quand vous commencez à avoir à mettre « up » vecteur l’appareil photo manuellement pour le maintenir vers le haut et vous toujours doivent être à l’affût des choses comme le blocage de cardan (où vous perdez un degré de liberté en raison de deux axes parallèles dans l’appareil photo) et le fait qu’interpolation le * valeurs * de vecteurs de rotation de la caméra peut vous prendre correctement de A--> B dans l’espace, mais dans le mauvais sens de rotation. Vous pouvez penser, il est naturel de se tourner de 180 degrés en tournant la tête sur le côté, mais le plus simple chemin de rotation est peut-être celui qui va les frais généraux directement au lieu de cela. Blech !
Je ne vais pas pour vous emmener à travers les solutions à toutes ces questions, et si vous regardez attentivement, vous trouverez que mes solutions sont encore, dans certains endroits, un peu rude. Au lieu de cela, je vais me contenter de coller tout le code de mouvement de caméra ainsi que de petites notes ici sur ce que faire des parties de celui-ci. Si vous êtes vraiment plonger dans une pièce et que vous désirez plus d’explications, aller de l’avant et de poster un commentaire, donc je peux étoffer cette partie.
Critères de caméra : faits saillants
-Placer une étoile sélectionnée dans le centre de l' écran (zoomAndDollyToPoint)
-Placer une étoile sélectionnée dans le centre de l’écran avec le centre de la galaxie dans le fond pour empêcher la navigation les confins de l’univers (zoomToFitPointsFrom, CAMERA_RELATION. TOWARD_CENTER)
-Déplacement d’une étoile sélectionnée à un autre sans changer d’angle de la caméra (strafeFromPointToPoint)
-Trouver la sphère englobante pour un groupe d’étoiles et puis de trouver une position de la caméra tels que ces étoiles tiendraient sur l' écran (zoomToFitPointsFrom, tous les CAMERA_RELATIONs)
-Mettre étoiles adjacents du cluster même dans des positions d’écran confortable par rapport à un seul "sélectionné" étoile dans la constellation de l’auteur (showThreePointsNicely)
-Revenir à une position « d’origine » (Réinitialiser)
-Paramétrage d’un chemin d’accès pour la caméra afin qu’il peut lentement voler à travers la galaxie sur son propre quand pas assisté (attente des années 90 sans cliquer pour voir l’animation de démarrage)-- (beginAutomaticTravel et cameraSetupForParameter)
Le Code :
Galaxy.Settings = Galaxy.Settings || {}; Galaxy.CameraMotions = function(camera) {_.bindAll(this,'zoomToFitPointsFrom','startAnimation','endAnimation','cameraSetupForParameter','beginAutomaticTravel') ; this.target = Galaxy.Settings.cameraDefaultTarget.clone() ; this.camera = caméra; / / efface cette propriété éventuellement this.firstClick = true ; this.isAnimating = false;} Galaxy.CameraMotions.prototype = {constructeur : Galaxy.CameraMotions, startAnimation : function() {/ / startAnimation se réfère à des animations initiées par l’utilisateur. L’animation par défaut doit être enlevée si en cours. this.endAutomaticTravel() ; this.isAnimating = true ; }, endAnimation : function() {this.isAnimating = false;}, zoomAndDollyToPoint : function(point,callback) {si (this.isAnimating === vrai) retour; / / temporairement : zoomera au premier clic, et nous allons mitrailler après cela. si (this.firstClick === false) {/ / this.strafeFromPointToPoint(this.target,point,callback) ; this.zoomToFitPointsFrom ([point], cette. CAMERA_RELATION. TOWARD_CENTER, callback) ; retour ; } this.firstClick = false ; var qui = ceci, pointClone = point.clone(), cameraPath = this.cameraPathToPoint(this.camera.position.clone(), point.clone()), currentPosition = {maintenant: 0}, durée = 1.3, upClone = Galaxy.Settings.cameraDefaultUp.clone(), targetCurrent = this.target.clone() ; TweenMax.to (targetCurrent, durée/1.5, {x:pointClone.x, y:pointClone.y, z:pointClone.z}) ; TweenMax.to (currentPosition, durée, {maintenant : 0.8, onUpdate : function() {var pos = cameraPath.getPoint(currentPosition.now) ; that.target = trois nouvelles. 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() ; if (typeof rappel === "fonction") callback();}}) ; }, cameraPathToPoint : function(fromPoint,toPoint) {var spline = trois nouvelles. SplineCurve3 ([fromPoint, trois nouvelles. 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), radiomessagerie]) ; Retour à cannelures ; }, strafeFromPointToPoint : function(fromPoint,toPoint,callback) {var dest = toPoint.clone(), actuel = this.camera.position.clone(), durée = 0,5, qui = cela ; dest.sub(fromPoint.clone()) ; dest.add(current.clone()); / / console.log("\n\n",fromPoint,toPoint,current,dest) ; if (that.isAnimating === true) Then return 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 rappel === "fonction") callback();}, onStart : that.startAnimation})}, reset : function(callback) {var durée = 2, qui = ceci, Accueil = Galaxy.Settings.cameraDefaultPosition.clone(), Centre = Galaxy.Settings.cameraDefaultTarget.clone(), upGoal = Galaxy.Settings.cameraDefaultUp.clone(), upCurrent = this.camera.up.clone(), targetCurrent = this.target.clone(), positionCurrent = this.camera.position.clone(); / / jamais rien faire quand rien ne suffira. Le rappel ne doit avoir aucun retard. Si (this.camera.up.equals(Galaxy.Settings.cameraDefaultUp) & & this.camera.position.equals(Galaxy.Settings.cameraDefaultPosition) & & this.target.equals(Galaxy.Settings.cameraDefaultTarget)) {durée = 0,1;} si (that.isAnimating === true) Then return TweenMax.to (upCurrent, durée/1.5, {x: upGoal.x,y : upGoal.y,z : upGoal.z}) ; TweenMax.to (targetCurrent, durée/1.5, {x: center.x,y : center.y, z: center.z}) ; TweenMax.to (positionCurrent, durée, {x: home.x,y : home.y, z: home.z,ease : Power1.easeInOut, onUpdate : function() {that.target = trois nouvelles. 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 rappel === "fonction") callback();}, onStart : that.startAnimation})}, CAMERA_RELATION: {ci-avant: 0, SAME_ANGLE: 1, TOWARD_CENTER: 2}, zoomToFitPointsFrom : function(pointList,cameraRelation,callback) {si (! _.has (_.values (this. CAMERA_RELATION), cameraRelation)) {/ / console.log (_.values (this. CAMERA_RELATION)) ; Console.Error (cameraRelation + « n’est-ce pas un des RELATIVE_LOCATION ») ; retour ; } Si (this.isAnimating === true) Then return pointList censé pour être déjà en coordonnées universelles. Comprendre bondissant de sphère, puis déplacez la caméra par rapport à son centre var bSphere = trois nouvelles. Sphère (trois nouvelles. Vector3(0,0,0),5) ; bSphere.setFromPoints(pointList) ; a quelle distance nous faut-il être pour s’adapter à cette sphère? var targetDistance = (bSphere.radius / (Math.tan(Math.PI*this.camera.fov/360))), cameraPositionEnd, que cela, = durée = 1, haut = this.camera.up.clone(), currentCameraPosition = this.camera.position.clone() ; Switch (cameraRelation) {case 0: / / CAMERA_RELATION. AU-dessus de cameraPositionEnd = bSphere.center.clone () .add (trois nouvelles. Vector3(40,40,targetDistance)) ; rupture ; cas 1: / / CAMERA_RELATION. SAME_ANGLE chariots la caméra/sortie telle que ces points deviennent visibles var centre = bSphere.center.clone(), currentPos = that.camera.position.clone(), finalViewAngle = currentPos.sub(center).setLength(targetDistance) ; cameraPositionEnd = bSphere.center.clone().add(finalViewAngle) ; pour empêcher la caméra de passer sous le plan du fond : cameraPositionEnd.z = Math.max(cameraPositionEnd.z,40) ; rupture ; case 2: / / CAMERA_RELATION. TOWARD_CENTER trace une ligne entre l’origine du monde par le biais de point de centre de la sphère englobante, / / et met la caméra à la fin d’un vecteur de deux fois plus que la longueur. cameraPositionEnd = bSphere.center.clone().multiplyScalar(2) ; Si (cameraPositionEnd.length() < 125) cameraPositionEnd.setLength(125) ; C’est bizarre lorsque l’appareil photo devient trop près des étoiles dans la pause de milieu ; } 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, durée/1.5, {x: cameraTargetEnd.x,y : cameraTargetEnd.y, z: cameraTargetEnd.z}) ; NE changez pas « up » pour un angle élevé. Il est tordu et tourne la caméra désagréablement. Si (cameraRelation! == 0) {TweenMax.to (vers le haut, durée/1.5 {x: 0, y: 0, z: 1});} TweenMax.to (currentCameraPosition, durée, {x: cameraPositionEnd.x,y : cameraPositionEnd.y, cameraPositionEnd.z z, onUpdate : function() {that.target = trois nouvelles. 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 rappel === "fonction") callback();}, onStart : that.startAnimation})}, showThreePointsNicely : function (pointList, rappel) {/ / trouver un emplacement de la caméra et une rotation telle que le premier point apparaît vers le bas de l’écran, et / / les deux autres apparaissent vers le haut et à gauche et à droite. Ou alors. Si (this.isAnimating === true) Then return this.firstClick = false ; Si (! _.isArray(pointList)) {throw new Error ("matrice de points requis pour showThreePointsNicely");} else if (pointList.length! == 3) {/ / juste montrer le premierà. this.zoomAndDollyToPoint(pointList[0],callback) ; return;} var pointZero = pointList[0].clone() ; Regardez le monde sous l’angle de l’étoile qui sera centré : var viewFromPointZero = function(vector) {return vector.clone().sub(pointZero.clone());} ; Le vecteur « coupent » est un angle au centre entre les gauche et à droite des étoiles que nous essayons de rendre visible à l’écran, ainsi que de vecteur zéro var bisectLocal = viewFromPointZero (pointList [1]) .add (viewFromPointZero (pointList[2])).multiplyScalar(0.5) ; Le chemin linéaire serait décrite comme... var A = viewFromPointZero(pointList[1]) ; var B = viewFromPointZero(pointList[2]) ; var thêta = 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) ; Le chemin circulaire autour de point médian de la trajectoire linéaire serait, ensuite : rayon var = cameraStartPosition.clone().sub(cameraPathMidpoint.clone()).length() ; var qui = cela ; var worldPointOnCircularPath = function(t) {var x = rayon * Math.cos(t) ; var y = rayon * Math.sin(t) ; var vectorPointLocal = trois nouvelles. Vector3(x,y,0) ; Return vectorPointLocal.add(cameraPathMidpoint.clone()) ; }; backsolve l’angle de départ de la trajectoire circulaire. C’est l’inverse de x=a+r*cos(theta) = > thêta = acos((x-a)/r) ; var startPointRelativeToMidpoint = cameraStartPosition.clone().sub(cameraPathMidpoint.clone()) ; startAngle var = Math.atan(startPointRelativeToMidpoint.y/startPointRelativeToMidpoint.x) ; C’est l’angle de départ ou l’angle de la fin? C’est un ou l’autre, mais nous devons savoir qui... L’autre va être cela + PI si (worldPointOnCircularPath(startAngle).setZ(0).distanceTo(cameraStartPosition.clone().setZ(0)) > 100) {/ / je dois commencer à l’autre bout à la place. tel est le monde d’inverse trig fonctions startAngle += Math.PI;} paramètres var = {t: startAngle, z: cameraStartPosition.clone () .z}, durée = 2.0, up = 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 (vers le haut, durée/1.5 {x: 0, y: 0, z: 1}) ; TweenMax.to (paramètres, la durée, {t: startAngle - Math.PI, z: cameraEndPosition.clone () .z, facilité : 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() ; if (typeof rappel === "fonction") callback();}, onStart : that.startAnimation}) ; }, / / La caméra peut aussi « voyage » en mode sans assistance. Ce comportement nécessite peu de code pour définir de façon paramétrique et ensuite animer le chemin complex, / / mais c’est un peu différent en nature depuis les mouvements de caméra initiée par l’utilisateur décrits ci-dessus. beginAutomaticTravel : function() {/ / cette fonction absolument positivement doit commencer de la caméra maison postes. / / console.log ("voyage caméra automatique à compter'); var obj = {cameraParameter : Math.PI/2}, que cela, loopConstants = = this.loopConstants() ; this.reset(function() {que .__automaticCameraAnimation = TweenMax.to (obj, loopConstants.duration, {cameraParameter : 5*Math.PI/2, onUpdate:function() {that.cameraSetupForParameter(obj.cameraParameter,loopConstants);}, facilité : null, répéter : -1 / / loop infiniment});});}, loopConstants : function() {var galaxyLoopStart = trois nouvelles. Vector3(200,0,15), targetLoopStart = trois nouvelles. Vector3(200,200,5), upLoopStart = trois nouvelles. Vector3(0,0,1) ; retour {Durée : 400, / / secondes 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 (), maximum = loopConstants.upLoopStart.clone() ; if (cameraParameter < 2*Math.PI & & cameraParameter > Math.PI) {cameraParameter-= Math.PI; / / aller un cercle complet autour des galaxie pos = trois nouvelles. Vector3(200*Math.cos(cameraParameter),200*Math.Sin(cameraParameter),15) ; copie de var = pos.clone() ; lookAt = trois nouvelles. Vector3 (copy.x, copy.y + copy.x,5) ; } else {/ / après avoir un cercle complet autour de la galaxie, animer toutes les caractéristiques de la caméra sur la position « home », puis répéter de début var pathMultiplier = Math.sin (cameraParameter); / / bon de 0 à PI. En dehors de cette plage, cela va négative et semble incontrôlable. POS = loopConstants.galaxyLoopStart.clone().add(loopConstants.galaxyLoopToHome.clone().multiplyScalar(pathMultiplier)) ; lookAt = loopConstants.targetLoopStart.clone().add(loopConstants.targetLoopToHome.clone().multiplyScalar(pathMultiplier)) ; up = 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 (ce .__automaticCameraAnimation) {this.__automaticCameraAnimation.kill();}}, / / débogage des outils logVec : function(message,vec) {console.log (message + ":" + vec.x + "" + vec.y + "" + vec.z);}, addTestCubeAtPosition : function(position) {var cube = trois nouvelles. Maille (trois nouvelles. CubeGeometry (5, 5, 5), trois nouveaux. MeshNormalMaterial()) ; Cube.Position = position.clone() ; Galaxy.Scene.add (cube) ; } }