Three.js Proiettore e oggetti Ray

Ho provato a lavorare con le classi Proiettore e Ray per fare delle dimostrazioni sul rilevamento delle collisioni. Ho iniziato solo cercando di usare il mouse per selezionare oggetti o trascinarli. Ho visto esempi che usano gli oggetti, ma nessuno di loro sembra avere commenti che spiegano quali sono esattamente alcuni dei metodi di Projector e Ray. Ho un paio di domande che spero sia facile per qualcuno rispondere.

Che cosa sta succedendo esattamente e qual è la differenza tra Projector.projectVector () e Projector.unprojectVector ()? Noto che sembra che in tutti gli esempi che usano sia oggetti proiettore che raggio il metodo non proiettato viene chiamato prima della creazione del raggio. Quando useresti projectVector?

Sto usando il seguente codice in questa demo per girare il cubo quando viene trascinato con il mouse. Qualcuno può spiegare in termini semplici cosa sta succedendo esattamente quando unproject con mouse3D e camera e poi creo il Ray. Il raggio dipende dalla chiamata a 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); } 

Fondamentalmente, è necessario proiettare dallo spazio del mondo 3D e dallo spazio dello schermo 2D.

I renderer usano projectVector per tradurre punti 3D sullo schermo 2D. unprojectVector è fondamentalmente per fare i punti 2D inversi e non proiettanti nel mondo 3D. Per entrambi i metodi si passa attraverso la telecamera che si sta visualizzando la scena.

Quindi, in questo codice stai creando un vettore normalizzato nello spazio 2D. Ad essere onesti, non ero mai troppo sicuro della logica z = 0.5 .

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

Quindi, questo codice utilizza la matrice di proiezione della telecamera per trasformarlo nel nostro spazio mondiale 3D.

 projector.unprojectVector(mouse3D, camera); 

Con il punto mouse3D convertito nello spazio 3D, ora possiamo utilizzarlo per ottenere la direzione e quindi utilizzare la posizione della telecamera per lanciare un raggio.

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

Ho scoperto che avevo bisogno di andare un po ‘più in profondità sotto la superficie per lavorare al di fuori dello scopo del codice di esempio (come avere una canvas che non riempia lo schermo o che abbia effetti aggiuntivi). Ho scritto un post sul blog qui . Questa è una versione abbreviata, ma dovrebbe coprire praticamente tutto ciò che ho trovato.

Come farlo

Il seguente codice (simile a quello già fornito da @mrdoob) cambierà il colore di un cubo quando si fa clic:

  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 ); } 

Con le versioni più recenti di three.js (attorno alla r55 e successive), puoi utilizzare pickingRay che semplifica ulteriormente le cose in modo che diventi:

  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 ); } 

Rimaniamo con il vecchio approccio in quanto dà più informazioni su ciò che sta accadendo sotto il cofano. Puoi vederlo funzionare qui , fai semplicemente clic sul cubo per cambiarne il colore.

Cosa sta succedendo?

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

event.clientX è la coordinata x della posizione del clic. Dividere per window.innerWidth indica la posizione del clic in proporzione della larghezza dell’intera finestra. Fondamentalmente, questo si sta traducendo dalle coordinate dello schermo che iniziano da (0,0) in alto a sinistra fino a ( window.innerWidth , window.innerHeight ) in basso a destra, alle coordinate cartesiane con centro (0,0) e che vanno da (-1, -1) a (1,1) come mostrato di seguito:

traduzione dalle coordinate della pagina web

Si noti che z ha un valore di 0,5. A questo punto non entrerò troppo nel dettaglio del valore z, tranne per dire che questa è la profondità del punto lontano dalla telecamera che stiamo proiettando nello spazio 3D lungo l’asse z. Maggiori informazioni su questo più tardi.

Il prossimo:

  projector.unprojectVector( mouse3D, camera ); 

Se guardi il codice three.js vedrai che questa è davvero un’inversione della matrice di proiezione dal mondo 3D alla fotocamera. Ricorda che per ottenere dalle coordinate del mondo 3D una proiezione sullo schermo, il mondo 3D deve essere proiettato sulla superficie 2D della videocamera (che è ciò che vedi sullo schermo). Fondamentalmente facciamo l’inverso.

Nota che mouse3D ora conterrà questo valore non proiettato. Questa è la posizione di un punto nello spazio 3D lungo il raggio / traiettoria a cui siamo interessati. Il punto esatto dipende dal valore z (lo vedremo più avanti).

A questo punto, potrebbe essere utile dare un’occhiata all’immagine seguente:

Fotocamera, valore non proiettato e raggio

Il punto che abbiamo appena calcolato (mouse3D) è indicato dal punto verde. Si noti che le dimensioni dei punti sono puramente illustrative, non hanno alcuna influenza sulle dimensioni della fotocamera o del punto 3D del mouse. Siamo più interessati alle coordinate al centro dei punti.

Ora, non vogliamo solo un singolo punto nello spazio 3D, ma invece vogliamo un raggio / traiettoria (mostrato dai punti neri) in modo che possiamo determinare se un object è posizionato lungo questo raggio / traiettoria. Nota che i punti mostrati lungo il raggio sono solo punti arbitrari, il raggio è una direzione dalla telecamera, non un insieme di punti .

Fortunatamente, poiché abbiamo un punto lungo il raggio e sappiamo che la traiettoria deve passare dalla fotocamera a questo punto, possiamo determinare la direzione del raggio. Pertanto, il passo successivo è quello di sottrarre la posizione della telecamera dalla posizione mouse3D, questo darà un vettore direzionale piuttosto che un singolo punto:

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

Ora abbiamo una direzione dalla fotocamera a questo punto nello spazio 3D (mouse3D ora contiene questa direzione). Questo è poi trasformato in un vettore unitario normalizzandolo.

Il prossimo passo è creare un raggio (Raycaster) partendo dalla posizione della telecamera e usando la direzione (mouse3D) per lanciare il raggio:

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

Il resto del codice determina se gli oggetti nello spazio 3D sono intersecati dal raggio o meno. Fortunatamente ci prendiamo cura di noi dietro le quinte usando intersectsObjects .

La demo

OK, quindi diamo un’occhiata a una demo dal mio sito qui che mostra che questi raggi sono espressi nello spazio 3D. Quando fai clic ovunque, la videocamera ruota intorno all’object per mostrarti come viene proiettato il raggio. Si noti che quando la fotocamera ritorna nella sua posizione originale, viene visualizzato solo un punto. Questo perché tutti gli altri punti sono lungo la linea della proiezione e quindi bloccati dalla vista dal punto anteriore. Questo è simile a quando guardi lungo la linea di una freccia che punta direttamente lontano da te – tutto ciò che vedi è la base. Ovviamente, lo stesso vale quando si guarda la linea di una freccia che viaggia direttamente verso di te (vedi solo la testa), che in genere è una brutta situazione.

La coordinata z

Diamo un’altra occhiata a quella coordinata z. Fai riferimento a questa demo mentre leggi questa sezione e fai esperimenti con valori diversi per z.

OK, diamo un’altra occhiata a questa funzione:

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

Abbiamo scelto 0,5 come valore. Ho detto prima che la coordinata z detta la profondità della proiezione in 3D. Quindi, diamo un’occhiata a valori diversi per z per vedere quale effetto ha. Per fare questo, ho posizionato un punto blu dove si trova la fotocamera e una linea di punti verdi dalla fotocamera alla posizione non proiettata. Quindi, dopo aver calcolato gli incroci, sposto la fotocamera indietro e di lato per mostrare il raggio. Meglio visto con alcuni esempi.

Innanzitutto, un valore az di 0,5:

valore z di 0,5

Notare la linea verde di punti dalla fotocamera (punto blu) al valore non proiettato (la coordinata nello spazio 3D). È come la canna di una pistola, che indica nella direzione in cui devono essere lanciati i raggi. La linea verde rappresenta essenzialmente la direzione che viene calcasting prima di essere normalizzata.

OK, proviamo con un valore di 0.9:

valore z di 0,9

Come puoi vedere, la linea verde si è ora estesa ulteriormente nello spazio 3D. 0,99 si estende ancora di più.

Non so se ci sia importanza su quanto sia grande il valore di z. Sembra che un valore più grande sarebbe più preciso (come un cannone più lungo), ma dal momento che stiamo calcolando la direzione, anche una breve distanza dovrebbe essere abbastanza precisa. Gli esempi che ho visto usano 0.5, quindi è quello con cui mi attaccherò a meno che non sia detto altrimenti.

Proiezione quando la canvas non è a schermo intero

Ora che sappiamo un po ‘di più su cosa sta succedendo, possiamo capire quali dovrebbero essere i valori quando la canvas non riempie la finestra ed è posizionata sulla pagina. Dì, per esempio, che:

  • il div contenente la canvas three.js è offsetX da sinistra e offsetY dalla parte superiore dello schermo.
  • la canvas ha una larghezza uguale a viewWidth e height uguale a viewHeight.

Il codice sarebbe quindi:

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

In sostanza, ciò che stiamo facendo è calcolare la posizione del clic del mouse rispetto al canvas (per x: event.clientX - offsetX ). Quindi determiniamo proporzionalmente dove si è verificato il clic (per x: /viewWidth ) in modo simile a quando la canvas ha riempito la finestra.

Questo è tutto, si spera che aiuti.

A partire dalla release r70, Projector.unprojectVector e Projector.pickingRay sono deprecati. Invece, abbiamo raycaster.setFromCamera che raycaster.setFromCamera la vita nel trovare gli oggetti sotto il puntatore del mouse.

 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 dà l’object sotto il puntatore del mouse e intersects[0].point fornisce il punto sull’object su cui è stato fatto clic sul puntatore del mouse.

Projector.unprojectVector () tratta il vec3 come una posizione. Durante il processo il vettore viene tradotto, quindi usiamo .sub (camera.position) su di esso. Inoltre, dobbiamo normalizzarlo dopo questa operazione.

Aggiungerò alcuni elementi grafici a questo post ma per ora posso descrivere la geometria dell’operazione.

Possiamo pensare alla macchina da presa come a una piramide in termini di geometria. Lo definiamo infatti con 6 riquadri: a sinistra, a destra, in alto, in basso, vicino e lontano (vicino all’aereo più vicino alla punta).

Se fossimo in piedi in qualche 3d e osservando queste operazioni, vedremmo questa piramide in una posizione arbitraria con una rotazione arbitraria nello spazio. Diciamo che l’origine di questa piramide è alla sua punta, e che l’asse z negativo corre verso il fondo.

Qualsiasi cosa finisca per essere contenuta in quei 6 piani finirà per essere resa sul nostro schermo se applichiamo la corretta sequenza di trasformazioni di matrice. Che io opengl andare qualcosa di simile a questo:

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

Questo porta la nostra mesh dal suo spazio object nello spazio del mondo, nello spazio della telecamera e infine proietta la matrice di proiezione prospettica che essenzialmente mette tutto in un piccolo cubo (NDC con intervalli da -1 a 1).

Lo spazio dell’object può essere un insieme ordinato di coordinate xyz in cui si genera qualcosa proceduralmente o si dice, un modello 3d, che un artista modellato utilizzando la simmetria e quindi si collochi ordinatamente nello spazio delle coordinate, al contrario di un modello architettonico ottenuto da qualcosa di simile REVISIONE o AutoCAD.

Una objectMatrix potrebbe accadere tra la matrice del modello e la matrice di visualizzazione, ma di solito viene curata in anticipo. Dì, sfogliando y e z, o portando un modello lontano dall’origine in limiti, convertendo unità ecc.

Se pensiamo al nostro schermo 2d piatto come se avesse profondità, potrebbe essere descritto allo stesso modo del cubo NDC, anche se leggermente distorto. Questo è il motivo per cui forniamo le proporzioni alla fotocamera. Se immaginiamo un quadrato delle dimensioni della nostra altezza dello schermo, il resto è il rapporto di aspetto di cui abbiamo bisogno per ridimensionare le nostre coordinate x.

Ora torniamo allo spazio 3d.

Siamo in una scena 3D e vediamo la piramide. Se tagliamo tutto intorno alla piramide e poi prendiamo la piramide insieme alla parte della scena in essa contenuta e mettiamo la sua punta a 0,0,0, e puntiamo il fondo verso l’asse -z finiremo qui:

 viewMatrix * modelMatrix * position.xyzw 

Moltiplicando questo valore per la matrice di proiezione sarà come se avessimo preso la punta e iniziato a tirarlo nell’asse xey creando un quadrato da quel punto e trasformando la piramide in una scatola.

In questo processo la casella viene ridimensionata a -1 e 1 e otteniamo la nostra proiezione prospettica e finiamo qui:

 projectionMatrix * viewMatrix * modelMatrix * position.xyzw; 

In questo spazio, abbiamo il controllo su un evento mouse bidimensionale. Dal momento che è sul nostro schermo, sappiamo che è bidimensionale, e che è da qualche parte all’interno del cubo NDC. Se è bidimensionale, possiamo dire che conosciamo X e Y ma non la Z, quindi il bisogno di ray casting.

Quindi, quando lanciamo un raggio, stiamo essenzialmente inviando una linea attraverso il cubo, perpendicolare ad uno dei lati.

Ora dobbiamo capire se quel raggio colpisce qualcosa nella scena, e per farlo dobbiamo trasformare il raggio da questo cubo, in uno spazio adatto per il calcolo. Vogliamo il raggio nello spazio mondiale.

Ray è una linea infinita nello spazio. È diverso da un vettore perché ha una direzione e deve passare attraverso un punto nello spazio. E in effetti è così che il Raycaster prende le sue argomentazioni.

Quindi se comprimiamo la parte superiore della scatola insieme alla linea, di nuovo nella piramide, la linea avrà origine dalla punta e scorrerà verso il basso e interseca la parte inferiore della piramide da qualche parte tra: mouse.x * farRange e -mouse.y * farRange.

(-1 e 1 all’inizio, ma lo spazio vista è in scala mondiale, appena ruotato e spostato)

Poiché questa è la posizione predefinita della fotocamera per così dire (è lo spazio degli oggetti) se applichiamo la propria matrice del mondo al raggio, la trasformsremo insieme alla fotocamera.

Poiché il raggio passa attraverso 0,0,0, abbiamo solo la sua direzione e THREE.Vector3 ha un metodo per trasformare una direzione:

 THREE.Vector3.transformDirection() 

Normalizza anche il vettore nel processo.

La coordinata Z nel metodo sopra

Questo funziona essenzialmente con qualsiasi valore e agisce allo stesso modo a causa del modo in cui il cubo NDC funziona. Il piano vicino e il piano lontano sono proiettati su -1 e 1.

Quindi quando dici, spara un raggio a:

 [ mouse.x | mouse.y | someZpositive ] 

si invia una linea, attraverso un punto (mouse.x, mouse.y, 1) nella direzione di (0,0, someZpositive)

Se si mette in relazione questo con l’esempio box / piramide, questo punto è in fondo, e poiché la linea proviene dalla telecamera, passa anche attraverso quel punto.

MA, nello spazio NDC, questo punto è allungato all’infinito e questa linea finisce per essere parallela ai piani sinistro, superiore, destro e inferiore.

Unprojecting con il metodo precedente lo trasforma sostanzialmente in una posizione / punto. Il piano lontano viene mappato nello spazio del mondo, quindi il nostro punto si trova da qualche parte a z = -1, tra l’aspetto -camera e + cameraAspect su X e -1 e 1 su y.

dal momento che è un punto, l’applicazione della matrice del mondo delle telecamere non solo la ruoterà ma la tradurrà. Da qui la necessità di riportare questo all’origine sottraendo la posizione della telecamera.