Recorrer la escena: traverse y las recetas que resuelve
En qué orden visita traverse los nodos, qué diferencia hay con traverseVisible y traverseAncestors, y las cuatro operaciones que todo proyecto acaba escribiendo.
El orden del recorrido
traverse visita en profundidad y en preorden: primero el nodo sobre el que se llama, después su primer hijo con todo su subárbol, después el segundo, y así sucesivamente. El nodo de partida se incluye en el recorrido, cosa que sorprende la primera vez que alguien llama a objeto.traverse esperando visitar solo los descendientes.
Los números indican el orden de visita. Que sea preorden importa en un caso concreto: cuando la operación que haces sobre un nodo depende de algo que has calculado en su padre, el preorden garantiza que el padre ya se ha procesado. Si necesitas lo contrario —procesar los hijos antes que el padre, por ejemplo para acumular volúmenes envolventes— traverse no te sirve y hay que escribir el recorrido a mano.
Las otras dos variantes cambian qué se visita. traverseVisible se salta los nodos con visible desactivado y todos sus descendientes, que es exactamente lo que hace el renderer al recoger objetos, así que es la que hay que usar cuando quieres razonar sobre lo que se está dibujando. traverseAncestors va en dirección contraria, de padre en padre hasta la raíz, y no incluye el nodo de partida; sirve para preguntar cosas como en qué grupo está contenido algo o si algún ancestro está oculto.
import * as THREE from 'three';const scene = new THREE.Scene();const grupo = new THREE.Group();grupo.name = 'vehiculo';const malla = new THREE.Mesh(new THREE.BoxGeometry(), new THREE.MeshNormalMaterial());malla.name = 'carroceria';grupo.add(malla);scene.add(grupo);const visitados = [];scene.traverse((nodo) => visitados.push(nodo.type));console.log('recorrido completo:', visitados); // Scene, Group, Meshconst ancestros = [];malla.traverseAncestors((nodo) => ancestros.push(nodo.type));console.log('ancestros:', ancestros); // Group, Scenegrupo.visible = false;const visibles = [];scene.traverseVisible((nodo) => visibles.push(nodo.type));console.log('solo visibles:', visibles); // SceneLas cuatro recetas
Contar el coste real de la escena. Antes de optimizar nada conviene saber qué hay dentro. Este recorrido da las cifras que importan y no depende de ninguna herramienta externa.
import * as THREE from 'three';export function inventario(raiz) { const datos = { nodos: 0, mallas: 0, triangulos: 0, geometrias: new Set(), materiales: new Set(), texturas: new Set(), }; raiz.traverse((nodo) => { datos.nodos += 1; if (!nodo.isMesh) return; datos.mallas += 1; datos.geometrias.add(nodo.geometry); const posicion = nodo.geometry.attributes.position; const cuenta = nodo.geometry.index ? nodo.geometry.index.count : posicion.count; const instancias = nodo.isInstancedMesh ? nodo.count : 1; datos.triangulos += (cuenta / 3) * instancias; for (const material of [].concat(nodo.material)) { datos.materiales.add(material); for (const clave of Object.keys(material)) { const valor = material[clave]; if (valor && valor.isTexture) datos.texturas.add(valor); } } }); return { nodos: datos.nodos, mallas: datos.mallas, triangulos: datos.triangulos, geometriasUnicas: datos.geometrias.size, materialesUnicos: datos.materiales.size, texturasUnicas: datos.texturas.size, };}El uso de conjuntos no es un detalle: la diferencia entre el número de mallas y el de geometrías únicas te dice cuánta reutilización hay, y la diferencia entre mallas y materiales únicos es un indicador directo de cuántos cambios de estado va a tener que hacer el renderer.
Aplicar una configuración a todo. Activar sombras, ajustar el filtrado de texturas o poner una capa concreta es siempre el mismo patrón.
export function configurarSombras(raiz, { emitir = true, recibir = true } = {}) { raiz.traverse((nodo) => { if (!nodo.isMesh) return; nodo.castShadow = emitir; nodo.receiveShadow = recibir; });}Liberar recursos. Ya apareció en el nivel anterior y es el caso donde el recorrido es imprescindible, porque hay que tocar cada geometría, cada material y cada textura del subárbol.
Construir un índice. Y esta es la que cambia el rendimiento de una aplicación entera.
export function indexar(raiz) { const porNombre = new Map(); const mallas = []; const animables = []; raiz.traverse((nodo) => { if (nodo.name) porNombre.set(nodo.name, nodo); if (nodo.isMesh) mallas.push(nodo); if (nodo.userData.animable) animables.push(nodo); }); return { porNombre, mallas, animables };}Un recorrido al cargar, y a partir de ahí las consultas son de coste constante. La alternativa —llamar a getObjectByName dentro del bucle de render— es una búsqueda lineal sobre toda la escena por cada consulta y por cada fotograma, y es una de las causas más habituales de que una aplicación con un modelo grande vaya lenta sin que la GPU esté haciendo nada.
// Mal: modifica el arbol mientras lo recorre.scene.traverse((nodo) => { if (nodo.userData.temporal) nodo.removeFromParent();});// Bien: recoger primero, actuar despues.const aEliminar = [];scene.traverse((nodo) => { if (nodo.userData.temporal) aEliminar.push(nodo);});for (const nodo of aEliminar) nodo.removeFromParent();Cierre del bloque de fundamentos
Con esto termina el recorrido por los cimientos: qué es un motor de render y qué no, cómo la GPU convierte triángulos en píxeles, la geometría vectorial que describe el espacio, las matrices que transforman entre espacios, las rotaciones sin singularidades, el primer proyecto que se mueve como debe, y el grafo que organiza la escena.
Merece la pena señalar qué se ha ganado exactamente, porque no es visible. No hay nada terminado que enseñar. Lo que hay es la capacidad de leer código ajeno de Three.js y entender por qué está escrito así; de mirar un shader y saber qué espacio es cada variable; de ver un objeto negro, deformado o ausente y tener una hipótesis en lugar de una lista de cosas que probar; y de reconocer, cuando algo va lento, si el problema está antes o después del rasterizador.
Todo lo que viene a partir de aquí —geometrías, materiales, luz, texturas, shaders, post-procesado, interacción, rendimiento— se apoya en estas siete piezas. Ninguna se volverá a explicar, y ninguna hará falta explicarla otra vez.