Saltar al contenido

El Raycaster y su modelo

Qué es realmente un Raycaster en Three.js, cómo se construye un rayo, qué controlan near y far, y por qué la clase delega en cada objeto en lugar de intersecar ella misma.

Un rayo, dos límites, una capa

El constructor acepta cuatro argumentos y todos tienen un valor por defecto razonable:

JS
import * as THREE from 'three';const origen = new THREE.Vector3( 0, 5, 0 );const direccion = new THREE.Vector3( 0, - 1, 0 );  // debe estar normalizadaconst raycaster = new THREE.Raycaster( origen, direccion, 0, 50 );

La dirección tiene que estar normalizada. No es una recomendación: el cálculo de distancia asume módulo uno, y con un vector sin normalizar todas las distancias que devuelve el raycaster quedan escaladas por su módulo. Si construyes la dirección restando dos puntos, no olvides el .normalize().

near y far recortan el segmento útil del rayo, y se miden desde el origen, no desde la cámara ni desde el plano near de la proyección. Una intersección más cercana que near o más lejana que far se descarta después de haberse calculado, no antes: el filtro está al final de la comprobación de cada triángulo. Eso significa que reducir far no acelera el raycast contra una malla concreta, aunque sí evita que aparezcan resultados irrelevantes. near no puede ser negativo y far no puede ser menor que near.

Puedes cambiar el rayo en cualquier momento sin recrear el objeto:

JS
raycaster.set( nuevoOrigen, nuevaDireccion );// O tocar el Ray directamente.raycaster.ray.origin.copy( jugador.position );raycaster.ray.direction.set( 0, - 1, 0 );

raycaster.ray es un Ray, la clase de matemáticas que hace el trabajo geométrico real: intersectSphere, intersectBox, intersectTriangle, intersectPlane, closestPointToPoint. Si lo que necesitas es una comprobación geométrica pura y no tocar la escena, usa Ray directamente y ahórrate la ceremonia del raycaster.

La tercera propiedad de estado es layers, un Layers que se compara con el de cada objeto antes de intentar la intersección. Es el filtro más barato que existe porque es una operación de bits y ocurre antes de tocar geometría; lo verás a fondo en la lección de optimización.

params: el umbral de las cosas sin volumen

Un triángulo tiene área y un rayo lo atraviesa o no. Una línea y un punto no tienen área: la probabilidad de que un rayo infinitamente fino los toque exactamente es cero. Por eso Raycaster lleva un objeto de parámetros con un umbral en unidades de mundo para esos casos:

JS
raycaster.params = {	Mesh: {},	Line: { threshold: 1 },	LOD: {},	Points: { threshold: 1 },	Sprite: {}};

threshold es la distancia máxima entre el rayo y la línea —o el punto— para considerar que hay impacto. El valor por defecto de uno es enorme para casi cualquier escena: en una escena en metros significa que aciertas cualquier punto que pase a menos de un metro del rayo. Si tu sistema de puntos tiene partículas de dos centímetros, el umbral debería estar en el orden de 0.02, no en uno.

JS
// Puntos pequeños: umbral proporcional al tamaño visual.raycaster.params.Points.threshold = 0.05;// Lineas gruesas dibujadas con LineSegments: un poco mas que su ancho aparente.raycaster.params.Line.threshold = 0.1;

El detalle que muerde es que el umbral está en unidades de mundo y es constante con la distancia, mientras que el tamaño aparente de un punto en pantalla depende de la perspectiva. Un umbral que funciona bien para puntos cercanos es demasiado pequeño para los lejanos y viceversa. Si necesitas precisión de picking constante en pantalla, la solución no es ajustar el umbral sino escalar el umbral por la distancia a la cámara antes de cada consulta, o pasarse directamente a picking por GPU.

Las entradas Mesh, LOD y Sprite están vacías porque esos objetos no necesitan tolerancia: tienen superficie. Existen en el objeto para que puedas añadirles propiedades desde un raycast personalizado.

Delegación: quién hace el trabajo

Este es el punto que cambia el modelo mental. intersectObject no interseca nada. Lo que hace es esto:

JS
// Version simplificada de lo que ocurre en Raycaster.function intersect( object, raycaster, intersects, recursive ) {	let propagar = true;	if ( object.layers.test( raycaster.layers ) ) {		const resultado = object.raycast( raycaster, intersects );		if ( resultado === false ) propagar = false;	}	if ( propagar && recursive ) {		for ( const hijo of object.children ) {			intersect( hijo, raycaster, intersects, true );		}	}}

Tres consecuencias, todas útiles.

La primera: el raycaster recorre el grafo de escena, no una lista plana. Con recursive activado visita cada descendiente, comprueba sus capas y llama a su raycast. Ese recorrido tiene coste propio, proporcional al número de nodos, independientemente de que ninguno acierte.

La segunda: cada tipo de objeto implementa su propia geometría de intersección. Mesh.raycast compara contra triángulos, Points.raycast contra puntos con umbral, Line.raycast contra segmentos, LOD.raycast elige el nivel de detalle según la distancia y delega en él, y Object3D.raycast está vacío —un Group nunca interseca, solo propaga—. Si escribes tu propia clase derivada de Object3D, definir raycast es cómo la integras en el sistema.

La tercera, y la menos conocida: si raycast() devuelve false, el recorrido no baja a los hijos de ese objeto. Es un mecanismo de poda explícito, y es el gancho para implementar una jerarquía de volúmenes envolventes a mano:

JS
class GrupoConVolumen extends THREE.Group {	constructor() {		super();		this.volumen = new THREE.Sphere();	}	raycast( raycaster ) {		// Si el rayo no toca la esfera del grupo, no hace falta mirar dentro.		if ( raycaster.ray.intersectsSphere( this.volumen ) === false ) {			return false;		}	}}

Ese return false corta de raíz la exploración de un subárbol entero. Con una escena de mil objetos agrupados en veinte grupos con su esfera envolvente, un rayo que solo toca un grupo pasa de comprobar mil objetos a comprobar veinte esferas y cincuenta objetos. Es la optimización estructural más barata que existe y no requiere ninguna librería.