38 votes

Objets projecteurs et rayons de Three.js

J'ai essayé de travailler avec les classes Projector et Ray afin de faire quelques démonstrations de détection de collision. J'ai commencé par essayer d'utiliser la souris pour sélectionner des objets ou les faire glisser. J'ai regardé les exemples qui utilisent les objets, mais aucun d'entre eux ne semble avoir de commentaires expliquant ce que font exactement certaines des méthodes de Projector et Ray. J'ai quelques questions auxquelles, je l'espère, quelqu'un pourra répondre facilement.

Que se passe-t-il exactement et quelle est la différence entre Projector.projectVector() et Projector.unprojectVector() ? Je remarque qu'il semble que dans tous les exemples utilisant à la fois des objets projecteur et rayon, la méthode unproject est appelée avant la création du rayon. Quand utiliseriez-vous projectVector ?

J'utilise le code suivant dans ce cas Démonstration pour faire tourner le cube lorsqu'on le fait glisser avec la souris. Quelqu'un peut-il m'expliquer en termes simples ce qui se passe exactement lorsque je déprojette avec la souris3D et la caméra, puis que je crée le rayon. La raie dépend-elle de l'appel à unprojectVector() ?

/** Event fired when the mouse button is pressed down */
function onDocumentMouseDown(event) {
    event.preventDefault();
    mouseDown = true;
    mouse3D.x = mouse2D.x = mouseDown2D.x = (event.clientX / window.innerWidth) * 2 - 1;
    mouse3D.y = mouse2D.y = mouseDown2D.y = -(event.clientY / window.innerHeight) * 2 + 1;
    mouse3D.z = 0.5;

    /** Project from camera through the mouse and create a ray */
    projector.unprojectVector(mouse3D, camera);
    var ray = new THREE.Ray(camera.position, mouse3D.subSelf(camera.position).normalize());
    var intersects = ray.intersectObject(crateMesh); // store intersecting objects

    if (intersects.length > 0) {
        SELECTED = intersects[0].object;
        var intersects = ray.intersectObject(plane);
    }

}

/** This event handler is only fired after the mouse down event and
    before the mouse up event and only when the mouse moves */
function onDocumentMouseMove(event) {
    event.preventDefault();

    mouse3D.x = mouse2D.x = (event.clientX / window.innerWidth) * 2 - 1;
    mouse3D.y = mouse2D.y = -(event.clientY / window.innerHeight) * 2 + 1;
    mouse3D.z = 0.5;
    projector.unprojectVector(mouse3D, camera);

    var ray = new THREE.Ray(camera.position, mouse3D.subSelf(camera.position).normalize());

    if (SELECTED) {
        var intersects = ray.intersectObject(plane);
        dragVector.sub(mouse2D, mouseDown2D);
        return;
    }

    var intersects = ray.intersectObject(crateMesh);

    if (intersects.length > 0) {
        if (INTERSECTED != intersects[0].object) {
            INTERSECTED = intersects[0].object;
        }
    }
    else {
        INTERSECTED = null;
    }
}

/** Removes event listeners when the mouse button is let go */
function onDocumentMouseUp(event) {
    event.preventDefault();

    /** Update mouse position */
    mouse3D.x = mouse2D.x = (event.clientX / window.innerWidth) * 2 - 1;
    mouse3D.y = mouse2D.y = -(event.clientY / window.innerHeight) * 2 + 1;
    mouse3D.z = 0.5;

    if (INTERSECTED) {
        SELECTED = null;
    }

    mouseDown = false;
    dragVector.set(0, 0);
}

/** Removes event listeners if the mouse runs off the renderer */
function onDocumentMouseOut(event) {
    event.preventDefault();

    if (INTERSECTED) {
        plane.position.copy(INTERSECTED.position);
        SELECTED = null;
    }
    mouseDown = false;
    dragVector.set(0, 0);
}

72voto

acarlon Points 4636

J'ai découvert que je devais aller un peu plus loin sous la surface pour travailler en dehors du champ d'application de l'exemple de code (comme avoir un canevas qui ne remplit pas l'écran ou avoir des effets supplémentaires). J'ai écrit un billet de blog à ce sujet aquí . Il s'agit d'une version abrégée, mais elle devrait couvrir à peu près tout ce que j'ai trouvé.

Comment s'y prendre

Le code suivant (similaire à celui déjà fourni par @mrdoob) changera la couleur d'un cube lorsqu'on clique dessus :

    var mouse3D = new THREE.Vector3( ( event.clientX / window.innerWidth ) * 2 - 1,   //x
                                    -( event.clientY / window.innerHeight ) * 2 + 1,  //y
                                    0.5 );                                            //z
    projector.unprojectVector( mouse3D, camera );   
    mouse3D.sub( camera.position );                
    mouse3D.normalize();
    var raycaster = new THREE.Raycaster( camera.position, mouse3D );
    var intersects = raycaster.intersectObjects( objects );
    // Change color if hit block
    if ( intersects.length > 0 ) {
        intersects[ 0 ].object.material.color.setHex( Math.random() * 0xffffff );
    }

Avec les versions plus récentes de three.js (autour de r55 et plus), vous pouvez utiliser pickingRay qui simplifie encore plus les choses de sorte que cela devient :

    var mouse3D = new THREE.Vector3( ( event.clientX / window.innerWidth ) * 2 - 1,   //x
                                    -( event.clientY / window.innerHeight ) * 2 + 1,  //y
                                    0.5 );                                            //z
    var raycaster = projector.pickingRay( mouse3D.clone(), camera );
    var intersects = raycaster.intersectObjects( objects );
    // Change color if hit block
    if ( intersects.length > 0 ) {
        intersects[ 0 ].object.material.color.setHex( Math.random() * 0xffffff );
    }

Restons-en à l'ancienne approche, qui permet de mieux comprendre ce qui se passe sous le capot. Vous pouvez voir que cela fonctionne aquí Il suffit de cliquer sur le cube pour changer sa couleur.

Qu'est-ce qui se passe ?

    var mouse3D = new THREE.Vector3( ( event.clientX / window.innerWidth ) * 2 - 1,   //x
                                    -( event.clientY / window.innerHeight ) * 2 + 1,  //y
                                    0.5 );                                            //z

event.clientX est la coordonnée x de la position du clic. En divisant par window.innerWidth donne la position du clic en proportion de la largeur totale de la fenêtre. En fait, il s'agit de passer des coordonnées de l'écran qui commencent à (0,0) en haut à gauche à ( window.innerWidth , window.innerHeight ) en bas à droite, aux coordonnées cartésiennes de centre (0,0) et allant de (-1,-1) à (1,1) comme indiqué ci-dessous :

translation from web page coordinates

Notez que z a une valeur de 0,5. Je n'entrerai pas dans les détails de la valeur z à ce stade, si ce n'est pour dire qu'il s'agit de la profondeur du point éloigné de la caméra que nous projetons dans l'espace 3D le long de l'axe z. Nous y reviendrons plus tard.

Suivant :

    projector.unprojectVector( mouse3D, camera );

Si vous regardez le code de three.js, vous verrez qu'il s'agit en fait d'une inversion de la matrice de projection du monde 3D vers la caméra. Gardez à l'esprit que pour passer des coordonnées du monde 3D à une projection sur l'écran, le monde 3D doit être projeté sur la surface 2D de la caméra (ce que vous voyez sur votre écran). En fait, nous faisons l'inverse.

Notez que mouse3D contiendra désormais cette valeur non projetée. Il s'agit de la position d'un point dans l'espace 3D le long du rayon/trajectoire qui nous intéresse. Le point exact dépend de la valeur z (nous verrons cela plus tard).

À ce stade, il peut être utile de jeter un coup d'œil à l'image suivante :

Camera, unprojected value and ray

Le point que nous venons de calculer (mouse3D) est représenté par le point vert. Notez que la taille des points est purement illustrative, elle n'a aucune incidence sur la taille de la caméra ou du point mouse3D. Nous sommes plus intéressés par les coordonnées au centre des points.

Maintenant, nous ne voulons pas seulement un point unique dans l'espace 3D, mais plutôt un rayon/trajectoire (représenté par les points noirs) afin de pouvoir déterminer si un objet est positionné le long de ce rayon/trajectoire. Notez que les points représentés le long du rayon ne sont que des points arbitraires, le rayon est une direction depuis la caméra, pas un ensemble de points. .

Heureusement, comme nous avons un point le long du rayon et que nous savons que la trajectoire doit passer de la caméra à ce point, nous pouvons déterminer la direction du rayon. Par conséquent, l'étape suivante consiste à soustraire la position de la caméra de la position de la souris3D, ce qui donnera un vecteur directionnel plutôt qu'un simple point :

    mouse3D.sub( camera.position );                
    mouse3D.normalize();

Nous avons maintenant une direction de la caméra à ce point dans l'espace 3D (mouse3D contient maintenant cette direction). Cette direction est ensuite transformée en un vecteur unitaire en la normalisant.

L'étape suivante consiste à créer un rayon (Raycaster) en partant de la position de la caméra et en utilisant la direction (mouse3D) pour lancer le rayon :

    var raycaster = new THREE.Raycaster( camera.position, mouse3D );

Le reste du code détermine si les objets dans l'espace 3D sont coupés par le rayon ou non. Heureusement, tout cela est pris en charge dans les coulisses grâce à l'utilisation de intersectsObjects .

La démo

OK, regardons une démo de mon site. aquí qui montre que ces rayons sont projetés dans l'espace 3D. Lorsque vous cliquez n'importe où, la caméra tourne autour de l'objet pour vous montrer comment le rayon est projeté. Notez que lorsque la caméra revient à sa position initiale, vous ne voyez qu'un seul point. En effet, tous les autres points se trouvent le long de la ligne de projection et sont donc bloqués par le point avant. C'est un peu comme lorsque vous regardez le long de la ligne d'une flèche pointant directement dans la direction opposée à la vôtre : vous ne voyez que la base. Bien entendu, la même chose s'applique lorsque vous regardez la ligne d'une flèche qui se dirige directement vers vous (vous ne voyez que la tête), ce qui est généralement une mauvaise situation.

La coordonnée z

Regardons à nouveau cette coordonnée z. Référez-vous à cette démo à mesure que vous lisez cette section et que vous expérimentez différentes valeurs pour z.

OK, regardons à nouveau cette fonction :

    var mouse3D = new THREE.Vector3( ( event.clientX / window.innerWidth ) * 2 - 1,   //x
                                    -( event.clientY / window.innerHeight ) * 2 + 1,  //y
                                    0.5 );                                            //z  

Nous avons choisi 0,5 comme valeur. J'ai mentionné précédemment que la coordonnée z détermine la profondeur de la projection en 3D. Examinons donc différentes valeurs pour z afin de voir quel effet elles ont. Pour ce faire, j'ai placé un point bleu là où se trouve la caméra, et une ligne de points verts de la caméra à la position non projetée. Ensuite, après avoir calculé les intersections, je déplace la caméra en arrière et sur le côté pour montrer le rayon. On peut mieux voir avec quelques exemples.

D'abord, une valeur z de 0,5 :

z value of 0.5

Notez la ligne verte de points allant de la caméra (point bleu) à la valeur non projetée (la coordonnée dans l'espace 3D). C'est comme le canon d'un fusil, qui pointe dans la direction dans laquelle le rayon doit être projeté. La ligne verte représente essentiellement la direction qui est calculée avant d'être normalisée.

OK, essayons une valeur de 0,9 :

z value of 0.9

Comme vous pouvez le voir, la ligne verte s'étend maintenant plus loin dans l'espace 3D. 0,99 s'étend encore plus loin.

Je ne sais pas si la valeur de z a une importance quelconque. Il semble qu'une valeur plus grande serait plus précise (comme un canon de fusil plus long), mais comme nous calculons la direction, même une courte distance devrait être assez précise. Les exemples que j'ai vus utilisent 0,5, c'est donc ce que je retiendrai, sauf avis contraire.

Projection lorsque le canevas n'est pas en plein écran

Maintenant que nous en savons un peu plus sur ce qui se passe, nous pouvons déterminer quelles doivent être les valeurs lorsque le canevas ne remplit pas la fenêtre et est positionné sur la page. Disons, par exemple, que :

  • la div contenant le canevas three.js est décaléeX de la gauche et décaléeY du haut de l'écran.
  • le canevas a une largeur égale à viewWidth et une hauteur égale à viewHeight.

Le code serait alors :

    var mouse3D = new THREE.Vector3( ( event.clientX - offsetX ) / viewWidth * 2 - 1,
                                    -( event.clientY - offsetY ) / viewHeight * 2 + 1,
                                    0.5 );

En gros, ce que nous faisons, c'est calculer la position du clic de la souris par rapport au canevas (pour x : event.clientX - offsetX ). Ensuite, nous déterminons proportionnellement où le clic s'est produit (pour x : /viewWidth ) comme lorsque le canevas remplissait la fenêtre.

C'est tout, j'espère que ça vous aidera.

50voto

mrdoob Points 9416

Fondamentalement, vous devez projeter à partir de l'espace mondial 3D et de l'espace écran 2D.

Les restituteurs utilisent projectVector pour traduire les points 3D sur l'écran 2D. unprojectVector sert essentiellement à faire l'inverse, à déprojeter des points 2D dans le monde 3D. Pour les deux méthodes, vous passez la caméra à travers laquelle vous visualisez la scène.

Ainsi, dans ce code, vous créez un vecteur normalisé dans un espace 2D. Pour être honnête, je n'ai jamais été très sûr de l'utilité de l'expression "vecteur normalisé". z = 0.5 logique.

mouse3D.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse3D.y = -(event.clientY / window.innerHeight) * 2 + 1;
mouse3D.z = 0.5;

Ensuite, ce code utilise la matrice de projection de la caméra pour la transformer dans notre espace mondial 3D.

projector.unprojectVector(mouse3D, camera);

Le point mouse3D étant converti dans l'espace 3D, nous pouvons maintenant l'utiliser pour obtenir la direction, puis utiliser la position de la caméra pour lancer un rayon.

var ray = new THREE.Ray(camera.position, mouse3D.subSelf(camera.position).normalize());
var intersects = ray.intersectObject(plane);

21voto

Prabu Arumugam Points 477

A partir de la version r70, Projector.unprojectVector y Projector.pickingRay sont dépréciés. À la place, nous avons raycaster.setFromCamera qui facilite la recherche des objets situés sous le pointeur de la souris.

var mouse = new THREE.Vector2();
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1; 

var raycaster = new THREE.Raycaster();
raycaster.setFromCamera(mouse, camera);
var intersects = raycaster.intersectObjects(scene.children);

intersects[0].object donne l'objet sous le pointeur de la souris et intersects[0].point donne le point de l'objet où le pointeur de la souris a été cliqué.

1voto

pailhead Points 484

Projector.unprojectVector() traite le vec3 comme une position. Au cours du processus, le vecteur est translaté, c'est pourquoi nous utilisons la fonction .sub(camera.position) sur elle. De plus, nous devons le normaliser après cette opération.

J'ajouterai quelques graphiques à ce billet mais pour l'instant je peux décrire la géométrie de l'opération.

Nous pouvons considérer la caméra comme une pyramide en termes de géométrie. Nous la définissons en fait avec 6 volets - gauche, droite, haut, bas, proche et lointain (proche étant le plan le plus proche de la pointe).

Si nous nous trouvions dans un endroit quelconque en 3D et observions ces opérations, nous verrions cette pyramide dans une position arbitraire avec une rotation arbitraire dans l'espace. Disons que l'origine de cette pyramide est à sa pointe, et que son axe z négatif va vers le bas.

Tout ce qui est contenu dans ces 6 plans finira par être rendu sur notre écran si nous appliquons la séquence correcte de transformations matricielles. Ce qui, en Opengl, donne quelque chose comme ça :

NDC_or_homogenous_coordinates = projectionMatrix * viewMatrix * modelMatrix * position.xyzw; 

Cette opération fait passer notre maillage de l'espace objet à l'espace monde, puis à l'espace caméra, et enfin le projette à l'aide de la matrice de projection en perspective, qui place essentiellement tout dans un petit cube (NDC avec des plages de -1 à 1).

L'espace objet peut être un ensemble soigné de coordonnées xyz dans lequel vous générez quelque chose de manière procédurale ou, disons, un modèle 3D, qu'un artiste a modelé en utilisant la symétrie et qui est donc parfaitement aligné avec l'espace de coordonnées, par opposition à un modèle architectural obtenu à partir de quelque chose comme REVIT ou AutoCAD.

Un objectMatrix pourrait se trouver entre la matrice du modèle et la matrice de la vue, mais cela est généralement pris en charge à l'avance. Par exemple, inverser y et z, ou amener un modèle éloigné de l'origine dans les limites, convertir les unités, etc.

Si nous considérons notre écran plat 2d comme s'il avait de la profondeur, il pourrait être décrit de la même manière que le cube NDC, bien que légèrement déformé. C'est pourquoi nous fournissons le rapport d'aspect à la caméra. Si nous imaginons un carré de la taille de la hauteur de notre écran, le reste est le rapport d'aspect dont nous avons besoin pour mettre nos coordonnées x à l'échelle.

Revenons maintenant à l'espace 3D.

Nous sommes dans une scène 3D et nous voyons la pyramide. Si nous coupons tout ce qui entoure la pyramide, puis que nous prenons la pyramide et la partie de la scène qu'elle contient, que nous plaçons sa pointe à 0,0,0 et que nous orientons le bas vers l'axe -z, nous obtenons ce résultat :

viewMatrix * modelMatrix * position.xyzw

En multipliant ce résultat par la matrice de projection, on obtient la même chose que si l'on prenait la pointe et que l'on commençait à la séparer sur les axes x et y, créant ainsi un carré à partir de ce point, et transformant la pyramide en boîte.

Dans ce processus, la boîte est mise à l'échelle à -1 et 1 et nous obtenons notre projection de perspective et nous nous retrouvons ici :

projectionMatrix * viewMatrix * modelMatrix * position.xyzw; 

Dans cet espace, nous avons le contrôle sur un événement de souris à 2 dimensions. Puisqu'il se trouve sur notre écran, nous savons qu'il est bidimensionnel et qu'il se trouve quelque part dans le cube NDC. S'il est bidimensionnel, nous pouvons dire que nous connaissons X et Y mais pas le Z, d'où la nécessité du ray casting.

Ainsi, lorsque nous lançons un rayon, nous envoyons essentiellement une ligne à travers le cube, perpendiculairement à l'un de ses côtés.

Maintenant, nous devons déterminer si ce rayon frappe quelque chose dans la scène, et pour ce faire, nous devons transformer le rayon de ce cube, dans un espace approprié pour le calcul. Nous voulons que le rayon soit dans l'espace mondial.

Le rayon est une ligne infinie dans l'espace. Elle est différente d'un vecteur car elle a une direction, et elle doit passer par un point dans l'espace. Et en effet, c'est ainsi que le Raycaster prend ses arguments.

Ainsi, si nous comprimons le haut de la boîte avec la ligne, pour la ramener dans la pyramide, la ligne partira de la pointe, descendra et coupera le bas de la pyramide quelque part entre -- mouse.x * farRange et -mouse.y * farRange.

(-1 et 1 au début, mais l'espace de vue est à l'échelle mondiale, juste tourné et déplacé)

Puisque c'est l'emplacement par défaut de la caméra pour ainsi dire (c'est l'espace objet), si nous appliquons sa propre matrice monde au rayon, nous le transformerons en même temps que la caméra.

Comme le rayon passe par 0,0,0, nous n'avons que sa direction et THREE.Vector3 possède une méthode pour transformer une direction :

THREE.Vector3.transformDirection()

Il normalise également le vecteur dans le processus.

La coordonnée Z dans la méthode ci-dessus

Cela fonctionne essentiellement avec n'importe quelle valeur, et agit de la même manière en raison de la façon dont le cube NDC fonctionne. Le plan proche et le plan éloigné sont projetés sur -1 et 1.

Donc quand vous dites, tirez un rayon sur :

[ mouse.x | mouse.y | someZpositive ]

vous envoyez une ligne, passant par un point (mouse.x, mouse.y, 1) dans la direction de (0,0,someZpositive)

Si vous faites le lien avec l'exemple de la boîte/pyramide, ce point se trouve en bas, et comme la ligne part de la caméra, elle passe également par ce point.

MAIS, dans l'espace NDC, ce point est étiré à l'infini, et cette ligne finit par être parallèle aux plans gauche, haut, droit, bas.

La déprojection avec la méthode ci-dessus transforme cela en une position/point essentiellement. Le plan lointain est simplement mis en correspondance avec l'espace mondial, de sorte que notre point se trouve quelque part à z=-1, entre -camera aspect et + cameraAspect sur X et -1 et 1 sur y.

Comme il s'agit d'un point, l'application de la matrice du monde de la caméra entraînera non seulement sa rotation, mais aussi sa translation. D'où la nécessité de la ramener à l'origine en soustrayant la position de la caméra.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X