Fermer

mai 21, 2020

Mouvement de caméra 3D dans Three.js – J'ai appris à la dure pour que vous n'ayez pas à


Récemment, j'ai dû faire face à une situation où je devais créer une maison 3D navigable dans un site Web. Dans cette maison, un utilisateur pourrait regarder autour d'une pièce en cliquant et en faisant glisser son point de vue, similaire à la vue sur la rue sur Google Maps. De plus, il y aurait des points d'accès cliquables qui permettraient à l'utilisateur de «marcher» et de regarder un élément spécifique dans la pièce. J'ai réalisé que ce n'était pas une tâche facile, surtout en ce qui concerne le mouvement de la caméra. Ce site a été construit en utilisant la bibliothèque Javascript Three.js en plus d'une application de page unique React v6. Voici quelques choses que j'ai apprises à la dure lors de la création de cette maison 3D.

Cliquez et faites glisser la caméra

Le premier problème qui devait être résolu était le mouvement de la caméra cliquer et faire glisser. Three.js propose quelques vues de caméra de base pour nous aider à démarrer. Celui que j'ai choisi était le Perspective Camera . C'est un bon début, mais la caméra avait besoin de quelques ajustements pour la faire bouger comme je le voulais.

Tout d'abord, la caméra se déplace dès que la souris se déplace sur la page plutôt que lorsque vous cliquez et faites glisser. Ce problème était assez facile à résoudre, mais en le résolvant, certaines des principales fonctionnalités de la caméra en perspective ont été modifiées. Afin de déplacer la caméra uniquement lorsque la souris est maintenue enfoncée, nous devions exploiter les écouteurs du document mousemove et mouseup les écouteurs d'événements, comme indiqué ci-dessous.

 document.addEventListener ('mousedown', onDocumentMouseDown, false);
document.addEventListener ('mousemove', onDocumentMouseMove, false);
document.addEventListener ('mouseup', onDocumentMouseUp, false); 

Cela m'a permis d'appeler des méthodes spécifiques lorsque le navigateur détecte que la souris est pressée, déplacée et soulevée respectueusement. Dans la fonction onDocumentMouseDown je note les positions X et Y de départ de la souris pour plus tard, ainsi que la définition d'une variable appelée isUserInteracting sur true. Cette variable est définie sur false dans la méthode onDocumentMouseUp permettant ainsi de suivre si l'utilisateur a la souris enfoncée. Par la suite, dans la méthode onDocumentMouseMove rien ne se produit à moins que isUserInteracting soit défini sur true. Maintenant, nous pouvons entrer dans la logique du déplacement réel de la caméra.

Par défaut, le mouvement de la caméra dans la caméra en perspective n'agit pas comme je le voulais. Il se déplace vers le haut lorsque votre souris monte et vers le bas lorsque votre souris descend. Idem avec gauche et droite. Vous pouvez voir un exemple ci-dessous dans une pièce 3D de validation de principe que j'ai installée.

 1 "width =" 300 "height =" 248 "srcset =" https: //i1.wp .com / blogs.perficient.com / files / 1.gif? resize = 300% 2C248 & ssl = 1 300w, https://i1.wp.com/blogs.perficient.com/files/1.gif?resize=500% 2C413 & ssl = 1 500w, https://i1.wp.com/blogs.perficient.com/files/1.gif?resize=600%2C495&ssl=1 600w, https://i1.wp.com/blogs.perficient. com / files / 1.gif? resize = 640% 2C528 & ssl = 1 640w "tailles =" (largeur max: 300px) 100vw, 300px "data-recalc-dims =" 1 "/></p data-recalc-dims=

Pour de nombreux cas d'utilisation qui les gens utilisent Three.js, c'est parfaitement acceptable. Mais, comme j'essaye d'imiter de manière intuitive quelqu'un pourrait cliquer et faire glisser une caméra dans une pièce (similaire à Google Maps), cela ne suffirait pas. Donc, la prochaine chose ce qui devait être fait était de réécrire la logique de la caméra.

J'ai mis à jour la logique de la caméra en utilisant la fonction camera.lookAt de Three.js pour regarder différents points dans l'espace 3D lorsque l'utilisateur clique et fait glisser s. Pour ce faire, les coordonnées X, Y et Z des points dans l'espace 3D doivent être calculées pour chaque petit mouvement de la souris. Tout d'abord, dans la méthode onDocumentMouseMove je prends les positions X et Y de départ du clic de souris mentionné précédemment et je fais quelques calculs avec. J'ai également utilisé les positions X et Y de l'endroit où la souris s'est déplacée pendant le glissement pour déterminer ce que j'appelle la latitude ( lat ) et la longitude ( lon ). Les calculs sont présentés ci-dessous:

 lon = (downPointer.x - event.clientX) * dragFactor + downPointer.lon;
lat = (event.clientY - downPointer.y) * dragFactor + downPointer.lat; 

Maintenant, je décrirai comment ce calcul fonctionne. Pour calculer la longitude, nous commençons par calculer la différence entre l'emplacement de la souris au moment du clic ( downPointer.x ) et l'emplacement actuel de la souris dans le processus de glissement ( event.clientX ). Ceci est ensuite multiplié par ce que j'appelle un dragFactor . Il s'agit simplement d'un facteur permettant de modifier la vitesse à laquelle la caméra se déplace lors du glissement. Cela a été modifié au besoin, mais j'ai fini avec 0,2. De là, downPointer.lon est ajouté à la somme. Cette variable est la valeur de ce que la variable précédente lon était après la dernière traînée (initialisée à 0).

La latitude est calculée de manière similaire. Avant d'avancer, il est garanti que la variable lat est comprise entre -85 et 85 degrés. Il s'agit de s'assurer que l'utilisateur ne peut pas regarder trop loin vers le haut ou vers le bas et retourner l'appareil photo d'une manière étrange. Ceci est assuré par le code suivant:

 lat = Math.max (-85, Math.min (85, lat)); 

À partir de là, je devais définir deux autres variables, phi et thêta en convertissant lat et lon en radians. De plus, lat doit d'abord être soustrait de 90 degrés.

 const phi = THREE.Math.degToRad (90 - lat);
const theta = THREE.Math.degToRad (lon); 

Enfin, nous pouvons calculer les valeurs X, Y et Z du point 3D vers lequel nous pointerons la caméra. Cela se fait en utilisant la trigonométrie. Les formules sont les suivantes:

 camera.target.x = radius * Math.sin (phi) * Math.cos (theta);
camera.target.y = radius * Math.cos (phi);
camera.target.z = rayon * Math.sin (phi) * Math.sin (theta);

Il y a une variable illustrée ci-dessus dont je n'ai pas encore parlé: radius . C'était la variable la plus délicate pour comprendre comment calculer et nous en parlerons en détail dans la section suivante.

La dernière chose à faire est un simple camera.lookAt (camera.target) . Cela sera appelé pour chaque petit mouvement que la souris fait lorsqu'elle est glissée et semble transparente, comme le montre le gif suivant.

 2 "width =" 300 "height =" 248 "srcset =" https: // i1.wp.com/blogs.perficient.com/files/2.gif?resize=300%2C248&ssl=1 300w, https://i1.wp.com/blogs.perficient.com/files/2.gif?resize = 500% 2C413 & ssl = 1 500w, https://i1.wp.com/blogs.perficient.com/files/2.gif?resize=600%2C495&ssl=1 600w, https://i1.wp.com/blogs .perficient.com / files / 2.gif? resize = 640% 2C528 & ssl = 1 640w "tailles =" (largeur max: 300px) 100vw, 300px "data-recalc-dims =" 1 "/></p data-recalc-dims=

] Correction de l'étrangeté de la caméra

 Plates-formes et technologie - Un guide pour les chefs d'entreprise sur les tendances clés du cloud

Je vais maintenant entrer dans la façon dont cette variable de rayon est calculée et les problèmes que j'ai rencontrés en cours de route pour le découvrir. [19659004] Mathématiquement, la variable de rayon représente la distance radiale entre le marqueur et la caméra et les formules utilisées pour calculer les X, Y et Les coordonnées Z sont des formules éprouvées en mathématiques illustrées ci-dessous, avec un graphique qui aide à expliquer ce que chacune des autres variables représente.

x = r sin (ϕ) cos (θ)
y = r sin (ϕ ) sin (θ)
z = r cos (ϕ)

 Math

Dans d'autres blogs, articles et questions de débordement de pile concernant le mouvement de l'appareil photo Three.js, il est indiqué que le La variable de rayon peut être un nombre fixe, tel que 500 (exemple ici ). Cela fonctionne pour la position initiale de la caméra, mais dès que la caméra se déplace vers un marqueur de point chaud, les calculs s'effondrent. Après le mouvement, le premier point calculé en essayant de faire glisser la caméra est décalé d'une certaine quantité, provoquant un saut dans la caméra, illustré dans le gif ci-dessous.

 3 "width =" 300 "height = "248" srcset = "https://i0.wp.com/blogs.perficient.com/files/3.gif?resize=300%2C248&ssl=1 300w, https://i0.wp.com/blogs. perficient.com/files/3.gif?resize=500%2C413&ssl=1 500w, https://i0.wp.com/blogs.perficient.com/files/3.gif?resize=600%2C495&ssl=1 600w, https://i0.wp.com/blogs.perficient.com/files/3.gif?resize=640%2C528&ssl=1 640w "tailles =" (largeur max: 300px) 100vw, 300px "data-recalc-dims = "1" /></p data-recalc-dims=

Pour remédier à ce problème, le rayon doit être calculé initialement et recalculé à chaque mouvement de la caméra. Pour calculer la longueur correcte du rayon, la version 3D du théorème de Pythagore peut être utilisée: √ ( x 2 + y 2 + z 2 ). Dans le code, cela ressemble à ceci en utilisant le raccourci Math.hypot : [ 1 9659005] radius = Math.hypot (x, y, z);

Le rayon doit être calculé initialement, après chaque mouvement de caméra dans la méthode onDocumentMouseMove et à la fin de la méthode déplaçant le caméra à un marqueur de point d'accès spécifique.

Maintenant, nous avons terminé, non?! Faux! Cela n'a pas complètement résolu le problème dans tous les cas! Selon la pièce ou l'emplacement du marqueur / caméra, il y aurait encore plus de bizarrerie avec la caméra. Le clic et le glisser semblent parfois enfermés dans une certaine zone, comme le montre le gif ci-dessous.

 4 "width =" 300 "height =" 248 "srcset =" https://i2.wp.com /blogs.perficient.com/files/4.gif?resize=300%2C248&ssl=1 300w, https://i2.wp.com/blogs.perficient.com/files/4.gif?resize=500%2C413&ssl= 1 500w, https://i2.wp.com/blogs.perficient.com/files/4.gif?resize=600%2C495&ssl=1 600w, https://i2.wp.com/blogs.perficient.com/ files / 4.gif? resize = 640% 2C528 & ssl = 1 640w "tailles =" (largeur max: 300px) 100vw, 300px "data-recalc-dims =" 1 "/></p data-recalc-dims=

Comme vous pouvez le voir, même si la traînée continue vers la gauche, la caméra commence à regarder vers la droite. Comment résoudre ce problème?

J'ai découvert que ce problème était dû au fait que le point 3D calculé à regarder était trop proche de la position de la caméra. Cela signifie que la variable de rayon est très petite et agit bizarrement lors du déplacement autour de la position de la caméra la plus proche. La solution que j'ai trouvée était de calculer un 3 rd point a longtemps le même chemin vectoriel de la position de la caméra et du point calculé à partir du théorème de Pythagore, mais beaucoup plus loin de la caméra. Voici la fonction que j'ai faite pour calculer ce nouveau point.

 const getNewPointOnVector = (p1, p2) => {
  laissez distAway = 200;
  soit vector = {x: p2.x - p1.x, y: p2.y - p1.y, z: p2.z - p1.z};
  soit vl = Math.sqrt (Math.pow (vector.x, 2) + Math.pow (vector.y, 2) + Math.pow (vector.z, 2));
  soit vectorLength = {x: vector.x / vl, y: vector.y / vl, z: vector.z / vl};
  soit v = {x: distAway * vectorLength.x, y: distAway * vectorLength.y, z: distAway * vectorLength.z};
  retourner {x: p2.x + v.x, y: p2.y + v.y, z: p2.z + v.z};
} 

En utilisant ce nouveau point calculé pour la caméra, mes problèmes de glissement ont été résolus.

Rotation vs Quaternion

Le dernier problème que j'ai rencontré lors de la création de cette pièce 3D l'expérience change l'orientation de la caméra lorsque vous cliquez sur un marqueur de point d'accès. À l'origine, j'ai implémenté cela en modifiant la rotation et la position de la caméra à l'aide de Tween.js, ce qui semblait être une solution logique. Tween.js est un moteur d'interpolation génial et facile à utiliser pour les animations. L'interpolation de la position a très bien fonctionné, mais l'interpolation de la rotation de la caméra s'est révélée problématique dans certains cas. La caméra tournait en spirale vers sa nouvelle orientation, qui avait l'air vertigineuse. Cela s'avère être un problème connu sous le nom de verrouillage du cardan. Un exemple est illustré ci-dessous.

 5 "width =" 300 "height =" 248 "srcset =" https://i1.wp.com/blogs.perficient.com/files/5.gif?resize = 300% 2C248 & ssl = 1 300w, https://i1.wp.com/blogs.perficient.com/files/5.gif?resize=500%2C413&ssl=1 500w, https://i1.wp.com/blogs .perficient.com / files / 5.gif? resize = 600% 2C495 & ssl = 1 600w, https://i1.wp.com/blogs.perficient.com/files/5.gif?resize=640%2C528&ssl=1 640w "tailles =" (largeur max: 300px) 100vw, 300px "data-recalc-dims =" 1 "/></p data-recalc-dims=

Pour résoudre ce problème, j'ai recherché la bonne façon de changer l'orientation dans l'espace 3D pour éviter le verrouillage du cardan. cette recherche, j'ai découvert quelque chose appelé quaternions. Les quaternions sont utilisés assez régulièrement en infographie 3D pour calculer les rotations tridimensionnelles. Mathématiquement, un quaternion est un nombre complexe à 4 dimensions. Sans entrer dans plus de détails sur ce qu'est un quaternion, disons simplement se concentrer sur la façon dont il a résolu mon problème. Les quaternions offrent un moyen beaucoup plus rapide et plus efficace de faire la transition de la caméra Three.js par rapport à sa propriété de rotation. A l'origine, en utilisant la rotation, la fonction pour se déplacer vers un marqueur était la suivante:

 function cameraToMarker (marker) {
  const currentCamPosition = {x: marker.cameraPositionX, y: camera.position.y, z: marker.cameraPositionZ};
  const storedMarkerPosition = new THREE.Vector3 (marker.positionX, marker.positionY, marker.positionZ);
  const newCameraTarget = getNewPointOnVector (currentCamPosition, storedMarkerPosition);
  const markerPosition = new THREE.Vector3 (... Object.values ​​(newCameraTarget));
  const startRotation = new THREE.Euler (). copy (camera.rotation);
  camera.lookAt (storedMarkerPosition);
  const endRotation = new THREE.Euler (). copy (camera.rotation);
  camera.rotation.copy (startRotation);

  nouveau TWEEN.Tween (camera.rotation)
    .à(
      {
        x: endRotation.x,
        y: endRotation.y,
        z: endRotation.z,
      }, 500)
    .easing (TWEEN.Easing.Quadratic.InOut)
    .onComplete (() => {
      nouveau TWEEN.Tween (camera.position)
        .à({
          x: marker.cameraPositionX,
          y: camera.position.y,
          z: marker.cameraPositionZ,
        })
        .easing (TWEEN.Easing.Quadratic.InOut)
        .onUpdate (() => {
          camera.lookAt (storedMarkerPosition);
        })
        .onComplete (() => {
          camera.lookAt (storedMarkerPosition);
          radius = Math.hypot (... Object.values ​​(markerPosition));
          phi = Math.acos (markerPosition.y / radius);
          thêta = Math.atan2 (markerPosition.z, markerPosition.x);
          lon = THREE.Math.radToDeg (thêta);
          lat = 90 - TROIS.MAT.radToDeg (phi);
        })
        .début();
    })
    .début();
}

Comme vous pouvez le voir, j'ai interpolé la rotation vers la variable endRotation qui contenait la camera.rotation après avoir utilisé camera.lookAt pour obtenir la valeur de rotation après avoir regardé la position du marqueur. L'appel à camera.lookAt ne regarde pas réellement cette position car, deux lignes plus tard, je copie la rotation de la caméra à sa valeur d'origine. Par la suite, j'ai interpolé la camera.position qui fonctionnait très bien.

Il y a d'autres choses importantes à noter sur cette fonction. Vous pouvez voir que je génère une newCameraTarget en utilisant la fonction getNewPointOnVector décrite dans la section précédente. De plus, je règle le rayon phi theta lon et lat une fois l'animation terminée. terminé pour que la fonctionnalité de cliquer-glisser fonctionne comme prévu après le mouvement de la caméra.

La mise à jour pour utiliser les quaternions était assez simple. Il suffisait de modifier les variables startRotation et endRotation en startQuaternion et endQuaternion et d'obtenir leurs valeurs en utilisant camera.quaternion.clone () au lieu de THREE.Euler . Problème résolu, non? Encore une fois!

Il y avait un autre problème auquel je faisais encore face après tout cela! Parfois, le mouvement de la caméra se déformait d'un côté avant de régler correctement l'orientation. Cela était dû à l'interpolation entre les quaternions, ou en changeant lentement une valeur de quaternion en une autre. La façon dont les quaternions sont censés être modifiés se fait par «interpolation linéaire sphérique», ou slerp. Idéalement, Three.js a une méthode de slerp dans laquelle nous pouvons puiser. J'ai ensuite changé l'interpolation externe décrite ci-dessus comme suit:

 let time = {t: 0};
nouveau TWEEN.Tween (temps)
 .to ({t: 1}, 500)
 .easing (TWEEN.Easing.Quadratic.InOut)
 .onUpdate (() => {
    THREE.Quaternion.slerp (startQuaternion, endQuaternion, camera.quaternion, time.t);
}) 

L'interpolation d'une variable de temps arbitraire de 0 à 1 sur 500 millisecondes et l'appel de la méthode slerp de Three.js dans la méthode onUpdate ont résolu ce problème. Jetez un œil à la façon dont la caméra se déplace après ces mises à jour.

 6 "width =" 300 "height =" 248 "srcset =" https://i2.wp.com/blogs.perficient.com/files /6.gif?resize=300%2C248&ssl=1 300w, https://i2.wp.com/blogs.perficient.com/files/6.gif?resize=500%2C413&ssl=1 500w, https: // i2 .wp.com / blogs.perficient.com / files / 6.gif? resize = 600% 2C495 & ssl = 1 600w, https://i2.wp.com/blogs.perficient.com/files/6.gif?resize= 640% 2C528 & ssl = 1 640w "tailles =" (largeur max: 300px) 100vw, 300px "data-recalc-dims =" 1 "/></p data-recalc-dims=

J'ai couvert chacun des problèmes les plus minutieux que j'ai rencontrés en travaillant avec Three .js. Pendant que je travaillais, je souhaitais toujours qu'il y ait un blog décrivant spécifiquement ces problèmes, et maintenant il y en a un. J'espère que ce billet de blog vous permettra de créer de superbes mouvements de caméra Three.js en toute simplicité.

Dan est consultant technique principal chez Perficient, basé à Chicago.

Plus d'informations sur cet auteur






Source link