web-dev-qa-db-fra.com

Projecteur Three.js et objets Ray

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

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

J'utilise le code suivant dans ce démo pour faire tourner le cube lorsque vous le faites glisser avec la souris. Quelqu'un peut-il expliquer en termes simples ce qui se passe exactement lorsque je dé-projette avec la souris3D et la caméra, puis crée le Ray. Le rayon dépend-il de l'appel à unfrajectVector ()

/** 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);
}
38
Cory Gross

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

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

Donc, dans ce code, vous créez un vecteur normalisé dans l'espace 2D. Pour être honnête, je n'ai jamais été trop sûr de la 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 en notre espace mondial 3D.

projector.unprojectVector(mouse3D, camera);

Le point 3D de la souris é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);
50
mrdoob

J'ai trouvé que je devais aller un peu plus loin sous la surface pour travailler en dehors de la portée 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 blog à ce sujet ici. Il s'agit d'une version raccourcie, mais devrait couvrir à peu près tout ce que j'ai trouvé.

Comment faire

Le code suivant (similaire à celui déjà fourni par @mrdoob) changera la couleur d'un cube lorsque vous cliquez 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 les plus récentes de three.js (vers r55 et versions ultérieures), vous pouvez utiliser pickingRay qui simplifie encore plus les choses pour que cela devienne:

    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 fidèles à l'ancienne approche, car elle donne un meilleur aperçu de ce qui se passe sous le capot. Vous pouvez voir ce fonctionnement ici, cliquez simplement sur le cube pour changer sa couleur.

Que ce passe-t-il?

    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 de clic. Division par window.innerWidth donne la position du clic proportionnellement à toute la largeur de la fenêtre. Fondamentalement, cela se traduit par des coordonnées d'écran qui commencent à (0,0) en haut à gauche jusqu'à (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 trop de détails sur la valeur z à ce stade, sauf pour dire que c'est la profondeur du point loin de la caméra que nous projetons dans l'espace 3D le long de l'axe z. Plus d'informations à ce sujet plus tard.

Prochain:

    projector.unprojectVector( mouse3D, camera );

Si vous regardez le code three.js, vous verrez qu'il s'agit vraiment 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). Nous faisons essentiellement 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/de la trajectoire qui nous intéresse. Le point exact dépend de la valeur z (nous le verrons plus tard).

À ce stade, il peut être utile de regarder l'image suivante:

Camera, unprojected value and ray

Le point que nous venons de calculer (mouse3D) est indiqué par le point vert. Notez que la taille des points est purement illustrative, ils n'ont aucune incidence sur la taille de la caméra ou du point 3D de la souris. Nous nous intéressons plus aux coordonnées au centre des points.

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

Heureusement, parce que 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 mouse3D, cela 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). Celui-ci est ensuite transformé en vecteur unitaire en le normalisant.

L'étape suivante consiste à créer un rayon (Raycaster) à partir 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 ou non par le rayon. Heureusement, tout est pris en charge dans les coulisses à l'aide de intersectsObjects.

La démo

OK, regardons donc une démo de mon site ici qui montre ces rayons projetés dans l'espace 3D. Lorsque vous cliquez n'importe où, la caméra pivote autour de l'objet pour vous montrer comment le rayon est projeté. Notez que lorsque la caméra revient à sa position d'origine, 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 à la vue par le point avant. Ceci est similaire à lorsque vous regardez la ligne d'une flèche pointant directement loin de vous - tout ce que vous voyez est la base. Bien sûr, la même chose s'applique lorsque vous regardez la ligne d'une flèche qui se déplace directement vers vous (vous ne voyez que la tête), ce qui est généralement une mauvaise situation.

La coordonnée z

Jetons un autre regard sur cette coordonnée z. Reportez-vous à cette démo pendant que vous lisez cette section et expérimentez différentes valeurs pour z.

OK, jetons un autre regard sur 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é plus tôt que la coordonnée z dicte la profondeur de la projection en 3D. Jetons donc un œil à différentes valeurs de z pour voir son effet. Pour ce faire, j'ai placé un point bleu à l'endroit où se trouve la caméra et une ligne de points verts de la caméra à la position non projetée. Ensuite, une fois les intersections calculées, je déplace la caméra vers l'arrière et sur le côté pour afficher le rayon. Mieux vu avec quelques exemples.

Tout d'abord, une valeur z de 0,5:

z value of 0.5

Notez la ligne verte de points de la caméra (point bleu) à la valeur non projetée (les coordonnées dans l'espace 3D). C'est comme le canon d'une arme à feu, pointant dans la direction où les rayons doivent être lancés. 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'est maintenant étendue dans l'espace 3D. 0.99 s'étend encore plus loin.

Je ne sais pas si la valeur de z est importante. Il semble qu'une valeur plus grande serait plus précise (comme un canon plus long), mais puisque 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 avec quoi je m'en tiendrai, sauf indication contraire.

Projection lorsque la toile n'est pas en plein écran

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

  • le div contenant le canevas three.js est offsetX à gauche et offsetY à partir du haut de l'écran.
  • la toile 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 );

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

Voilà, j'espère que ça aide.

72
acarlon

Depuis la version r70, Projector.unprojectVector et Projector.pickingRay sont obsolètes. Au lieu de cela, nous avons raycaster.setFromCamera qui facilite la recherche des objets 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 sur l'objet sur lequel le pointeur de la souris a été cliqué.

19
Prabu Arumugam

Projector.unprojectVector () traite le vec3 comme une position. Pendant le processus, le vecteur est traduit, nous utilisons donc . Sub (camera.position) dessus. De plus, nous devons le normaliser après cette opération.

J'ajouterai quelques graphiques à ce post 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 le définissons en fait avec 6 volets - gauche, droite, haut, bas, près et loin (près étant l'avion le plus proche de la pointe).

Si nous nous tenions dans quelque 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 à son extrémité et que son axe z négatif court vers le bas.

Tout ce qui finit par être contenu dans ces 6 plans finira par être rendu sur notre écran si nous appliquons la séquence correcte de transformations matricielles. Quel i opengl aller quelque chose comme ça:

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

Cela prend notre maillage de son espace objet dans l'espace monde, dans l'espace caméra et enfin il projette qu'il fait la matrice de projection en perspective qui met 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 façon procédurale ou dites, un modèle 3D, qu'un artiste modélise en utilisant la symétrie et se trouve ainsi parfaitement aligné avec l'espace de coordonnées, par opposition à un modèle architectural obtenu à partir de quelque chose comme REVIT ou AutoCAD.

Une objectMatrix peut se produire entre la matrice de modèle et la matrice de vue, mais cela est généralement pris en charge à l'avance. Dites, en inversant y et z, ou en apportant un modèle loin de l'origine en limites, en convertissant des 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 hauteur/largeur à 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 à l'échelle nos coordonnées x.

Revenons maintenant à l'espace 3D.

Nous nous tenons dans une scène 3D et nous voyons la pyramide. Si nous coupons tout autour de la pyramide, puis prenons la pyramide avec la partie de la scène qu'elle contient et mettons sa pointe à 0,0,0, et pointons le bas vers l'axe -z, nous nous retrouverons ici:

viewMatrix * modelMatrix * position.xyzw

La multiplication par la matrice de projection sera la même que si nous prenions la pointe et que nous commencions à la séparer dans les axes x et y en créant un carré à partir de ce point et en transformant la pyramide en boîte.

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

projectionMatrix * viewMatrix * modelMatrix * position.xyzw; 

Dans cet espace, nous contrôlons un événement de souris en 2 dimensions. Comme il est sur notre écran, nous savons qu'il est en deux dimensions et qu'il se trouve quelque part dans le cube NDC. Si c'est bidimensionnel, nous pouvons dire que nous connaissons X et Y mais pas le Z, d'où la nécessité de lancer des rayons.

Ainsi, lorsque nous lançons un rayon, nous envoyons essentiellement une ligne à travers le cube, perpendiculaire à 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 en un espace approprié pour le calcul. Nous voulons le rayon dans l'espace mondial.

Ray est une ligne infinie dans l'espace. Il est différent d'un vecteur car il a une direction et doit passer par un point de l'espace. Et c'est bien ainsi que le Raycaster prend ses arguments.

Donc, si nous pressons le haut de la boîte avec la ligne, dans la pyramide, la ligne proviendra de la pointe et descendra et intersectera 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 pour ainsi dire l'emplacement par défaut de la caméra (c'est l'espace objet) si nous appliquons sa propre matrice mondiale au rayon, nous la transformerons avec la caméra.

Puisque le rayon passe par 0,0,0, nous n'avons que sa direction et THREE.Vector3 a 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 du fonctionnement du cube NDC. Le plan proche et le plan lointain sont projetés sur -1 et 1.

Donc, quand vous dites, tirez un rayon sur:

[ mouse.x | mouse.y | someZpositive ]

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

Si vous associez cela à l'exemple de boîte/pyramide, ce point est en bas, et puisque la ligne provient 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, supérieur, droit et inférieur.

Unprojection avec la méthode ci-dessus transforme cela en une position/point essentiellement. Le plan éloigné est juste mappé dans l'espace mondial, donc notre point se situe quelque part à z = -1, entre l'aspect -camera et + cameraAspect sur X et -1 et 1 sur y.

puisqu'il s'agit d'un point, l'application de la matrice du monde des caméras non seulement la fera pivoter, mais la traduira également. D'où la nécessité de ramener cela à l'Origine en soustrayant la position des caméras.

1
pailhead