Saltar al contenido

Object3D: la anatomía del nodo

Todo lo que lleva dentro el objeto base de Three.js, qué propiedades son de solo lectura aunque no lo parezca, y qué comparte realmente un clon con su original.

Los cuatro grupos de propiedades

Identidad y estructura. id es un entero autoincremental por instancia; uuid es un identificador único que se genera solo y que se usa en la serialización; name es una cadena libre y no tiene que ser única; type es la cadena que identifica la subclase. Y la estructura del árbol: parent, que es uno o ninguno, y children, que es un array.

Transformación. position, rotation, quaternion y scale describen la transformación local. up es el vector de referencia vertical que usa lookAt, con valor inicial tomado de Object3D.DEFAULT_UP, que es el eje Y.

Matrices. matrix es la transformación local ya compuesta; matrixWorld es la acumulada desde la raíz. Hay dos más que rellena el renderer justo antes de dibujar y que no debes tocar: modelViewMatrix y normalMatrix, las que se estudiaron en el nivel de matrices.

Banderas de dibujado. visible decide si el objeto y sus descendientes se dibujan; frustumCulled activa el descarte por volumen de visión; renderOrder fuerza el orden dentro de su cola; castShadow y receiveShadow controlan la participación en las sombras; layers permite filtrar qué cámaras lo ven y qué rayos lo alcanzan.

Y una propiedad que no encaja en ningún grupo y que se usa constantemente: userData, un objeto vacío donde colgar lo que quieras. Es el sitio correcto para asociar datos de tu aplicación a un nodo sin ensuciar el espacio de nombres de la biblioteca.

JS
import * as THREE from 'three';const malla = new THREE.Mesh(  new THREE.BoxGeometry(),  new THREE.MeshStandardMaterial({ color: 0x89b4fa }));malla.name = 'caja-principal';malla.userData.tipo = 'obstaculo';malla.userData.vida = 100;console.log('id:', malla.id, 'tipo:', malla.type);console.log('es un Object3D:', malla.isObject3D);      // trueconsole.log('es una malla:', malla.isMesh);            // trueconsole.log('padre:', malla.parent);                    // null hasta que se anade

Las propiedades del estilo isMesh e isObject3D merecen un apunte: Three.js las usa internamente en lugar de instanceof porque instanceof falla cuando hay dos copias de la biblioteca cargadas en la misma página, cosa que ocurre más de lo que parece con dependencias mal resueltas. Si escribes código que comprueba tipos, usa esas banderas por la misma razón.

Las propiedades que no se pueden reasignar

Este es el primer tropiezo de mucha gente:

JS
// Esto NO funciona como esperas.malla.position = new THREE.Vector3(1, 2, 3);

position, rotation, quaternion y scale están declaradas como propiedades de solo lectura: la referencia al objeto no se puede cambiar, aunque su contenido sí. La razón es de diseño interno: rotation y quaternion están enlazadas por sendas funciones de notificación que las mantienen sincronizadas, como se vio en el nivel de rotaciones, y sustituir el objeto entero rompería ese enlace dejando dos representaciones desincronizadas.

La forma correcta es modificar el contenido:

JS
malla.position.set(1, 2, 3);malla.position.copy(otroVector);malla.position.x = 5;malla.scale.setScalar(2);

En modo estricto, que es el que se aplica a los módulos de JavaScript, la asignación directa lanza un error; fuera de él, falla en silencio, que es peor. Merece la pena conocer el detalle porque es exactamente el tipo de código que se escribe sin pensar cuando se viene de otra biblioteca.

Lo que un clon comparte con su original

clone crea un nuevo objeto y, por defecto, clona también recursivamente sus descendientes. Lo que no duplica es lo caro: la geometría y el material se copian por referencia.

Es la decisión correcta, porque el caso normal es dibujar cien copias del mismo árbol y duplicar cien veces sus vértices sería absurdo. Pero produce un comportamiento que sorprende la primera vez.

JS
import * as THREE from 'three';const original = new THREE.Mesh(  new THREE.SphereGeometry(1, 32, 16),  new THREE.MeshStandardMaterial({ color: 0xa6e3a1 }));const copia = original.clone();copia.position.x = 3;console.log('misma geometria:', copia.geometry === original.geometry);   // trueconsole.log('mismo material:', copia.material === original.material);     // true// Y por tanto, cambiar el color de uno cambia el de los dos.copia.material.color.set(0xf38ba8);console.log('color del original:', original.material.color.getHexString());  // f38ba8// Si de verdad quieres materiales independientes, hay que clonarlos aparte.const independiente = original.clone();independiente.material = original.material.clone();independiente.material.color.set(0xcba6f7);console.log('ahora si difieren:', original.material.color.getHexString());

De aquí salen dos reglas operativas. La primera: si vas a personalizar el material de una copia, clónalo explícitamente, y asume el coste de una permutación de shader adicional si las características del material cambian. La segunda, más peligrosa: liberar la geometría o el material de una copia los libera para todas, porque el recurso de GPU es uno solo. Un bucle de limpieza que recorra la escena llamando a dispose sobre cada malla puede dejar inservibles objetos que siguen vivos si comparten recursos con los que se están destruyendo.

Hay un detalle más del clonado que conviene conocer: userData sí se copia en profundidad, pero mediante una serialización a JSON. Eso significa que las funciones, las clases y las referencias circulares que hubieras guardado ahí no sobreviven al clonado. Si necesitas asociar objetos vivos a un nodo, guárdalos en un mapa externo indexado por uuid en lugar de en userData.

Los que dibujan y los que no

Un Object3D puro no dibuja nada. Tampoco un Group. Los objetos dibujables son los que aportan geometría y material: Mesh para superficies, Line y sus variantes para trazos, Points para nubes de puntos, Sprite para cuadriláteros siempre orientados a cámara, y los especializados como InstancedMesh, SkinnedMesh y BatchedMesh.

La distinción importa por algo más que la clasificación: las funciones de intercepción que el renderer invoca antes y después de dibujar cada objeto —onBeforeRender y onAfterRender— solo se ejecutan en los objetos dibujables. Colocarlas en un grupo esperando que se disparen cuando se dibuje su contenido es un error silencioso y bastante frecuente.

Con la anatomía del nodo clara, lo siguiente es lo que convierte una colección de nodos en un grafo: las relaciones de padre e hijo.