Saltar al contenido

El objeto de intersección, campo a campo

Qué devuelve intersectObject, qué significa cada propiedad del resultado, cuáles pueden faltar y por qué, y cómo se usan uv, face, barycoord e instanceId en casos reales.

Los dos métodos y su forma

JS
// Un objeto y, por defecto, todos sus descendientes.const hits = raycaster.intersectObject( modelo, true );// Varios objetos. Aqui casi siempre quieres recursive en false.const hits = raycaster.intersectObjects( mallas, false );// Tercer argumento: array reutilizable para no generar basura.const buffer = [];function consultar() {	buffer.length = 0;	raycaster.intersectObjects( mallas, false, buffer );	return buffer;}

Los dos ordenan el resultado por distance ascendente antes de devolverlo. El tercer argumento existe precisamente para el caso de consultar cada frame: reutilizar el array evita crear uno nuevo sesenta veces por segundo. No elimina toda la basura —cada intersección crea objetos Vector3 y Vector2 nuevos— pero quita la parte más gruesa.

intersectObjects con una lista no ordena los objetos entre sí antes de intersecar: recorre la lista completa y ordena al final. No hay early-out por proximidad. Si te basta con el primer impacto y tienes muchos objetos, ese es exactamente el trabajo que ahorra un BVH, que verás en la lección siguiente.

Campo a campo

Campo Tipo Siempre presente
distance number Sí
point Vector3 Sí, en coordenadas de mundo
object Object3D Sí
face objeto Solo en Mesh
faceIndex number Solo en Mesh
barycoord Vector3 Solo en Mesh
uv Vector2 Solo si la geometría tiene atributo uv
uv1 Vector2 Solo si la geometría tiene atributo uv1
normal Vector3 Solo si la geometría tiene atributo normal
instanceId number Solo en InstancedMesh
distanceToRay number Solo en Points

point está en coordenadas de mundo. Es el error más frecuente al colocar algo en el punto de impacto: si el objeto tocado es hijo de un grupo transformado y añades el marcador a ese mismo grupo, aparecerá desplazado. O añades el marcador a la escena raíz, o conviertes el punto al espacio local del padre:

JS
const marcador = new THREE.Mesh( geoMarcador, matMarcador );grupo.add( marcador );// Convertir de mundo a local del padre.marcador.position.copy( grupo.worldToLocal( hit.point.clone() ) );

face describe el triángulo tocado: a, b, c son los índices de sus tres vértices en la geometría, normal es la normal geométrica del triángulo —calculada con el producto vectorial de sus lados, no interpolada— y materialIndex indica el grupo de material al que pertenece cuando la malla tiene varios.

normal, sin más, es la normal interpolada a partir del atributo normal de los tres vértices usando las coordenadas baricéntricas. Además, Three.js la voltea si apunta en el mismo sentido que el rayo, para que siempre mire hacia quien dispara.

La diferencia entre las dos importa mucho:

JS
// Colocar un objeto pegado a la superficie con orientacion suave.if ( hit.normal ) {	marcador.position.copy( hit.point ).addScaledVector( hit.normal, 0.01 );	marcador.lookAt( hit.point.clone().add( hit.normal ) );}// Rebotar una particula: aqui interesa la normal plana del triangulo,// pero hay que pasarla a mundo, porque face.normal esta en espacio local.const normalMundo = hit.face.normal	.clone()	.transformDirection( hit.object.matrixWorld );velocidad.reflect( normalMundo );

Ese detalle —face.normal está en espacio local de la malla y hit.normal también— es responsable de decals que se orientan mal en cuanto el objeto está rotado. transformDirection aplica solo la parte rotacional de la matriz y renormaliza, que es exactamente lo que necesita un vector de dirección.

barycoord son los pesos del punto de impacto respecto a los tres vértices del triángulo. Suman uno y sirven para interpolar cualquier atributo de vértice, no solo los que Three.js interpola por ti:

JS
// Interpolar un atributo propio, por ejemplo un peso de terreno.const attr = hit.object.geometry.getAttribute( 'humedad' );const { a, b, c } = hit.face;const { x, y, z } = hit.barycoord;const humedad =	attr.getX( a ) * x +	attr.getX( b ) * y +	attr.getX( c ) * z;

uv es la coordenada de textura del punto exacto, ya interpolada. Es la puerta a un conjunto de técnicas que sorprende lo poco que se usan:

JS
// Leer el color del mapa de la malla en el punto tocado, desde JavaScript.const textura = hit.object.material.map;const img = textura.image;const lienzo = document.createElement( 'canvas' );lienzo.width = img.width;lienzo.height = img.height;const ctx = lienzo.getContext( '2d', { willReadFrequently: true } );ctx.drawImage( img, 0, 0 );const px = Math.floor( hit.uv.x * img.width );const py = Math.floor( ( 1 - hit.uv.y ) * img.height );   // ojo al eje Vconst [ r, g, b ] = ctx.getImageData( px, py, 1, 1 ).data;

El mismo uv permite pintar sobre una textura de datos y marcar needsUpdate, que es el esqueleto de cualquier herramienta de pintado 3D en el navegador.

instanceId y el picking sobre instancias

Con InstancedMesh hay un único objeto y una única geometría, pero miles de copias. Mesh.raycast no serviría, así que InstancedMesh implementa el suyo: transforma el rayo al espacio de cada instancia, interseca, y añade instanceId al resultado.

JS
const malla = new THREE.InstancedMesh( geometria, material, 5000 );// ... rellenar matrices con setMatrixAt ...const hits = raycaster.intersectObject( malla, false );if ( hits.length > 0 ) {	const id = hits[ 0 ].instanceId;	// Cambiar el color solo de esa instancia.	malla.setColorAt( id, new THREE.Color( 0xff4444 ) );	malla.instanceColor.needsUpdate = true;	// O leer y modificar su matriz.	const m = new THREE.Matrix4();	malla.getMatrixAt( id, m );}

El coste, eso sí, es lineal en el número de instancias: el raycast prueba todas. Con cinco mil cubos son cinco mil transformaciones de rayo más la intersección con doce triángulos cada una. Es una de las situaciones donde más se nota la aceleración espacial, y three-mesh-bvh tiene soporte específico para ello.

hits[0].object en este caso es la propia InstancedMesh, no una malla individual. La instancia se identifica solo por instanceId, así que si necesitas asociar datos a cada instancia, mantén un array paralelo indexado por ese número.