javascript - Proyector Three.js y objetos Ray
3d (4)
A partir del lanzamiento r70, Projector.unprojectVector
y Projector.pickingRay
están en desuso. En cambio, tenemos raycaster.setFromCamera
que facilita la búsqueda de objetos debajo del puntero 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
da el objeto bajo el puntero del mouse e intersects[0].point
le da el punto al objeto donde se hizo clic en el puntero del mouse.
He intentado trabajar con las clases Proyector y Ray para hacer algunas demostraciones de detección de colisión. Empecé simplemente tratando de usar el mouse para seleccionar objetos o arrastrarlos. He analizado ejemplos que usan los objetos, pero ninguno de ellos parece tener comentarios que expliquen exactamente qué están haciendo algunos de los métodos de Proyector y Ray. Tengo un par de preguntas que espero sean fáciles de responder para alguien.
¿Qué está pasando exactamente y cuál es la diferencia entre Projector.projectVector () y Projector.unprojectVector ()? Observé que parece que en todos los ejemplos que utilizan objetos de proyector y de rayo se llama al método sin proyecto antes de que se cree el rayo. ¿Cuándo usarías projectVector?
Estoy usando el siguiente código en esta demostración para hacer girar el cubo cuando lo arrastro con el mouse. ¿Alguien puede explicar en términos simples qué es exactamente lo que sucede cuando desinstalo con mouse3D y la cámara y luego creo el Rayo? ¿El rayo depende de la llamada 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);
}
Básicamente, debe proyectar desde el espacio mundial 3D y el espacio de la pantalla 2D.
Los projectVector
usan projectVector
para traducir puntos 3D a la pantalla 2D. unprojectVector
es básicamente para hacer los puntos 2D inversos que no se proyectan en el mundo 3D. Para ambos métodos pasas la cámara por la que estás viendo la escena.
Entonces, en este código estás creando un vector normalizado en el espacio 2D. Para ser sincero, nunca estuve demasiado seguro sobre la lógica z = 0.5
.
mouse3D.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse3D.y = -(event.clientY / window.innerHeight) * 2 + 1;
mouse3D.z = 0.5;
Entonces, este código usa la matriz de proyección de la cámara para transformarlo a nuestro espacio mundial 3D.
projector.unprojectVector(mouse3D, camera);
Con el mouse3D point convertido en el espacio 3D, ahora podemos usarlo para obtener la dirección y luego usar la posición de la cámara para lanzar un rayo.
var ray = new THREE.Ray(camera.position, mouse3D.subSelf(camera.position).normalize());
var intersects = ray.intersectObject(plane);
Descubrí que tenía que ir un poco más allá de la superficie para trabajar fuera del alcance del código de muestra (como tener un lienzo que no llena la pantalla o que tiene efectos adicionales). Escribí una publicación en el blog aquí . Esta es una versión abreviada, pero debe cubrir casi todo lo que encontré.
Cómo hacerlo
El siguiente código (similar al ya proporcionado por @mrdoob) cambiará el color de un cubo al hacer 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 las versiones más recientes de three.js (alrededor de r55 y posteriores), puede usar pickingRay, que simplifica aún más las cosas para que esto se convierta en:
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 );
}
Mantengámonos con el enfoque anterior, ya que da más información sobre lo que está sucediendo bajo el capó. Puedes ver que funciona aquí , simplemente haz clic en el cubo para cambiar su color.
¿Qué esta pasando?
var mouse3D = new THREE.Vector3( ( event.clientX / window.innerWidth ) * 2 - 1, //x
-( event.clientY / window.innerHeight ) * 2 + 1, //y
0.5 ); //z
event.clientX
es la coordenada x de la posición de clic. Dividir por window.innerWidth
proporciona la posición del clic en proporción al ancho total de la ventana. Básicamente, esto se traduce desde coordenadas de pantalla que comienzan en (0,0) en la window.innerWidth
superior izquierda hasta ( window.innerWidth
, window.innerHeight
) en la parte inferior derecha, a las coordenadas cartesianas con centro (0,0) y que van desde (-1, -1) a (1,1) como se muestra a continuación:
Tenga en cuenta que z tiene un valor de 0.5. No entraré en demasiados detalles sobre el valor z en este punto, excepto para decir que esta es la profundidad del punto alejado de la cámara que estamos proyectando en el espacio 3D a lo largo del eje z. Más sobre esto más tarde.
Siguiente:
projector.unprojectVector( mouse3D, camera );
Si miras el código three.js, verás que esto es realmente una inversión de la matriz de proyección del mundo 3D a la cámara. Tenga en cuenta que para pasar de las coordenadas del mundo 3D a una proyección en la pantalla, el mundo 3D debe proyectarse en la superficie 2D de la cámara (que es lo que ve en la pantalla). Básicamente estamos haciendo lo inverso.
Tenga en cuenta que mouse3D contendrá ahora este valor no proyectado. Esta es la posición de un punto en el espacio tridimensional a lo largo del rayo / trayectoria que nos interesa. El punto exacto depende del valor z (lo veremos más adelante).
En este punto, puede ser útil echar un vistazo a la siguiente imagen:
El punto que acabamos de calcular (mouse3D) se muestra con el punto verde. Tenga en cuenta que el tamaño de los puntos es puramente ilustrativo, no tienen relación con el tamaño de la cámara o el punto mouse3D. Estamos más interesados en las coordenadas en el centro de los puntos.
Ahora, no solo queremos un único punto en el espacio tridimensional, sino que queremos un rayo / trayectoria (que se muestra con los puntos negros) para poder determinar si un objeto está posicionado a lo largo de este rayo / trayectoria. Tenga en cuenta que los puntos que se muestran a lo largo del rayo son solo puntos arbitrarios, el rayo es una dirección de la cámara, no un conjunto de puntos .
Afortunadamente, como tenemos un punto a lo largo del rayo y sabemos que la trayectoria debe pasar de la cámara a este punto, podemos determinar la dirección del rayo. Por lo tanto, el siguiente paso es restar la posición de la cámara de la posición mouse3D, esto dará un vector direccional en lugar de solo un punto:
mouse3D.sub( camera.position );
mouse3D.normalize();
Ahora tenemos una dirección desde la cámara hasta este punto en el espacio 3D (mouse3D ahora contiene esta dirección). Esto se convierte en un vector unitario normalizándolo.
El siguiente paso es crear un rayo (Raycaster) comenzando desde la posición de la cámara y usando la dirección (mouse3D) para lanzar el rayo:
var raycaster = new THREE.Raycaster( camera.position, mouse3D );
El resto del código determina si los objetos en el espacio 3D se cruzan con el rayo o no. Afortunadamente, todo se solucionó detrás de escena con intersectsObjects
.
La demo
Bien, veamos una demostración de mi sitio que muestra estos rayos en el espacio 3D. Cuando hace clic en cualquier lugar, la cámara gira alrededor del objeto para mostrarle cómo se proyecta el rayo. Tenga en cuenta que cuando la cámara vuelve a su posición original, solo ve un punto. Esto se debe a que todos los demás puntos se encuentran a lo largo de la línea de la proyección y, por lo tanto, están bloqueados por el punto delantero. Esto es similar a cuando miras hacia abajo la línea de una flecha que apunta directamente lejos de ti; todo lo que ves es la base. Por supuesto, lo mismo se aplica al mirar hacia abajo la línea de una flecha que está viajando directamente hacia usted (solo ve la cabeza), que generalmente es una mala situación en la que estar.
La coordenada z
Echemos otro vistazo a esa coordenada z. Consulte esta demostración mientras lee esta sección y experimenta con diferentes valores para z.
Bien, echemos otro vistazo a esta función:
var mouse3D = new THREE.Vector3( ( event.clientX / window.innerWidth ) * 2 - 1, //x
-( event.clientY / window.innerHeight ) * 2 + 1, //y
0.5 ); //z
Elegimos 0.5 como el valor. Mencioné anteriormente que la coordenada z dicta la profundidad de la proyección en 3D. Entonces, echemos un vistazo a diferentes valores de z para ver qué efecto tiene. Para hacer esto, he colocado un punto azul donde está la cámara y una línea de puntos verdes desde la cámara hasta la posición no proyectada. Luego, después de calcular las intersecciones, muevo la cámara hacia atrás y hacia un lado para mostrar el rayo. Mejor visto con algunos ejemplos.
Primero, un valor de az de 0.5:
Tenga en cuenta la línea verde de puntos de la cámara (punto azul) al valor no proyectado (la coordenada en el espacio 3D). Esto es como el cañón de un arma de fuego, apuntando en la dirección en que se deben lanzar los rayos. La línea verde esencialmente representa la dirección que se calcula antes de ser normalizada.
Bien, probemos un valor de 0.9:
Como puede ver, la línea verde se ha ampliado aún más en el espacio tridimensional. 0.99 se extiende aún más.
No sé si hay alguna importancia en cuanto a qué tan grande es el valor de z. Parece que un valor mayor sería más preciso (como un cañón de arma más largo), pero dado que estamos calculando la dirección, incluso una distancia corta debería ser bastante precisa. Los ejemplos que he visto usan 0.5, así que eso es lo que voy a mantener a menos que se indique lo contrario.
Proyección cuando el lienzo no es pantalla completa
Ahora que sabemos un poco más sobre lo que está sucediendo, podemos averiguar cuáles deberían ser los valores cuando el lienzo no llena la ventana y se coloca en la página. Diga, por ejemplo, que:
- el div que contiene el lienzo three.js es offsetX desde la izquierda y offsetY desde la parte superior de la pantalla.
- el lienzo tiene un ancho igual a viewWidth y height igual a viewHeight.
El código sería entonces:
var mouse3D = new THREE.Vector3( ( event.clientX - offsetX ) / viewWidth * 2 - 1,
-( event.clientY - offsetY ) / viewHeight * 2 + 1,
0.5 );
Básicamente, lo que estamos haciendo es calcular la posición del clic del mouse relativo al lienzo (para x: event.clientX - offsetX
). Luego determinamos proporcionalmente dónde se produjo el clic (para x: /viewWidth
) similar a cuando el lienzo llenó la ventana.
Eso es todo, espero que ayude.
Projector.unprojectVector () trata al vec3 como una posición. Durante el proceso, el vector se traduce, por lo tanto usamos .sub (camera.position) en él. Además, debemos normalizarlo después de esta operación.
Agregaré algunos gráficos a esta publicación, pero por ahora puedo describir la geometría de la operación.
Podemos pensar en la cámara como una pirámide en términos de geometría. De hecho, lo definimos con 6 paneles: izquierdo, derecho, superior, inferior, cercano y lejano (cerca del plano más cercano a la punta).
Si estuviéramos parados en algún 3d y observando estas operaciones, veríamos esta pirámide en una posición arbitraria con una rotación arbitraria en el espacio. Digamos que el origen de esta pirámide está en su punta, y su eje z negativo corre hacia la parte inferior.
Lo que sea que termine estando contenido dentro de esos 6 planos terminará siendo renderizado en nuestra pantalla si aplicamos la secuencia correcta de transformaciones de matriz. Lo que abriré será algo como esto:
NDC_or_homogenous_coordinates = projectionMatrix * viewMatrix * modelMatrix * position.xyzw;
Esto lleva nuestra malla desde su espacio de objetos al espacio mundial, al espacio de la cámara y finalmente proyecta que tiene la matriz de proyección en perspectiva que esencialmente coloca todo en un cubo pequeño (NDC con rangos de -1 a 1).
El espacio de objeto puede ser un conjunto ordenado de coordenadas xyz en el que se genera algo de procedimiento o, dicho de otra manera, un modelo 3D, que un artista modeló utilizando simetría y se alinea con el espacio de coordenadas, en oposición a un modelo arquitectónico obtenido de decir algo así como REVIT o AutoCAD.
Un ObjectMatrix podría ocurrir entre la matriz del modelo y la matriz de vista, pero esto normalmente se soluciona antes de tiempo. Digamos, voltear yy z, o traer un modelo que está lejos del origen en límites, convertir unidades, etc.
Si pensamos en nuestra pantalla 2D plana como si tuviera profundidad, podría describirse del mismo modo que el cubo NDC, aunque ligeramente distorsionado. Es por eso que suministramos la relación de aspecto a la cámara. Si imaginamos un cuadrado del tamaño de nuestra altura de pantalla, el resto es la relación de aspecto que necesitamos para escalar nuestras coordenadas x.
Ahora volvemos al espacio 3d.
Estamos parados en una escena 3d y vemos la pirámide. Si cortamos todo alrededor de la pirámide, y luego tomamos la pirámide junto con la parte de la escena que contiene y ponemos su punta en 0,0,0, y señalamos la parte inferior hacia el eje -z, terminaremos aquí:
viewMatrix * modelMatrix * position.xyzw
Multiplicar esto por la matriz de proyección será el mismo que si tomamos la punta, y comenzamos a tirarla appart en el eje xey, creando un cuadrado de ese punto y convirtiendo la pirámide en una caja.
En este proceso, la caja se escala a -1 y 1 y obtenemos nuestra proyección de perspectiva y terminamos aquí:
projectionMatrix * viewMatrix * modelMatrix * position.xyzw;
En este espacio, tenemos control sobre un evento de ratón bidimensional. Como está en nuestra pantalla, sabemos que es de dos dimensiones, y que está en algún lugar dentro del cubo NDC. Si es bidimensional, podemos decir que conocemos X e Y pero no Z, de ahí la necesidad de lanzar rayos.
Entonces, cuando lanzamos un rayo, básicamente estamos enviando una línea a través del cubo, perpendicular a uno de sus lados.
Ahora necesitamos averiguar si ese rayo impacta algo en la escena, y para hacerlo necesitamos transformar el rayo de este cubo en un espacio adecuado para el cálculo. Queremos el rayo en el espacio mundial.
Ray es una línea infinita en el espacio. Es diferente de un vector porque tiene una dirección, y debe pasar por un punto en el espacio. Y de hecho, así es como Raycaster toma sus argumentos.
Entonces, si exprimimos la parte superior de la caja junto con la línea, volvemos a la pirámide, la línea se originará desde la punta y correrá hacia abajo e intersectará la parte inferior de la pirámide en algún lugar entre - mouse.x * farRange y -mouse.y * farRange.
(-1 y 1 al principio, pero el espacio de la vista está en escala mundial, simplemente gira y se mueve)
Dado que esta es la ubicación predeterminada de la cámara por así decirlo (es espacio de objetos) si aplicamos su propia matriz mundial al rayo, la transformaremos junto con la cámara.
Como el rayo pasa a 0,0,0, solo tenemos su dirección y THREE.Vector3 tiene un método para transformar una dirección:
THREE.Vector3.transformDirection()
También normaliza el vector en el proceso.
La coordenada Z en el método anterior
Esto funciona esencialmente con cualquier valor, y actúa de la misma manera por la forma en que funciona el cubo NDC. El plano cercano y el plano lejano se proyectan en -1 y 1.
Entonces cuando dices, dispara un rayo a:
[ mouse.x | mouse.y | someZpositive ]
envía una línea, a través de un punto (mouse.x, mouse.y, 1) en la dirección de (0,0, someZpositive)
Si relacionas esto con el ejemplo de caja / pirámide, este punto está en la parte inferior, y dado que la línea se origina en la cámara, atraviesa ese punto también.
PERO, en el espacio NDC, este punto se estira hasta el infinito, y esta línea termina siendo paralela a los planos izquierdo, superior, derecho e inferior.
Al no realizar el proyecto con el método anterior, esto se convierte esencialmente en una posición / punto. El plano lejano simplemente se mapea en el espacio mundial, por lo que nuestro punto se ubica en algún lugar en z = -1, entre el aspecto de la cámara y + la vista de la cámara en X y -1 y 1 en y.
ya que es un punto, la aplicación de la matriz mundial de cámaras no solo lo rotará sino que lo traducirá también. De ahí la necesidad de traer esto de vuelta al origen restando la posición de las cámaras.