Site icon Blog ARC Optimizer

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.

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.

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 (ϕ)

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.

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.

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.

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

Plus d'informations sur cet auteur






Source link
Quitter la version mobile