wandres.dev
EL GRAFO DE ESCENA · Object3D y la jerarquía

Matrices locales y globales: cuándo se actualizan

Qué hace exactamente updateMatrixWorld, qué significan matrixAutoUpdate y matrixWorldAutoUpdate, y cómo ahorrar miles de operaciones por fotograma en objetos que no se mueven.

⏱ 18 min

Cada nodo tiene dos matrices: la local, que expresa su transformación respecto a su padre, y la de mundo, que la expresa respecto al origen de la escena. Ninguna de las dos se recalcula cuando cambias la posición: se recalculan en un momento concreto del fotograma, y todo el código que lea una posición global fuera de ese momento leerá el valor anterior. Saber exactamente cuándo ocurre la actualización es lo que separa un desfase de un fotograma inexplicable de un bug de treinta segundos.

🎯 Al terminar esta lección sabrás
  • Describir el algoritmo de actualización de matrices y en qué momento lo dispara el renderer.
  • Distinguir matrixAutoUpdate de matrixWorldAutoUpdate y qué desactiva cada uno.
  • Forzar la actualización de un nodo concreto sin recorrer toda la escena.
  • Reducir el coste de actualización en escenas con muchos objetos estáticos.

El algoritmo, en orden

flowchart TB
cambio[Cambias position rotation o scale] --> pendiente[La matriz local queda obsoleta]
pendiente --> render[Llamada a render]
render --> escena[El renderer llama a updateMatrixWorld en la escena]
escena --> comp[Con matrixAutoUpdate activo se recompone la matriz local]
comp --> prod[matrixWorld es la del padre multiplicada por la local]
prod --> desc[Se repite el proceso en cada hijo]
desc --> fin[Matrices de mundo listas para dibujar]
lectura[Lees getWorldPosition antes de render] --> viejo[Obtienes el valor del fotograma anterior]
style cambio fill:#f9e2af,color:#11111b
style render fill:#89b4fa,color:#11111b
style escena fill:#89b4fa,color:#11111b
style comp fill:#cba6f7,color:#11111b
style prod fill:#cba6f7,color:#11111b
style fin fill:#a6e3a1,color:#11111b
style lectura fill:#f38ba8,color:#11111b
style viejo fill:#f38ba8,color:#11111b

Con más detalle, lo que hace cada nodo cuando le llega la actualización es esto. Primero, si tiene la actualización local automática activa, recompone su matriz local a partir de posición, cuaternión y escala, en el orden canónico de traslación por rotación por escala. Después, si esa matriz ha cambiado o si el padre le indica que debe recalcular, multiplica la matriz de mundo de su padre por su matriz local y guarda el resultado en su matriz de mundo. Por último, propaga la llamada a todos sus hijos, indicándoles que recalculen si él mismo ha recalculado.

El renderer dispara todo esto al principio de render: llama a la actualización sobre la escena y, por separado, sobre la cámara si esta no tiene padre. Ese es el único momento del fotograma en que las matrices de mundo son fiables sin intervención tuya.

De ahí la regla que ya apareció en el nivel de espacios: cualquier lectura de posición, orientación o escala globales debe hacerse después de render, o bien forzando la actualización explícitamente antes de leer.

import * as THREE from 'three';

const scene = new THREE.Scene();
const grupo = new THREE.Group();
const hijo = new THREE.Object3D();
grupo.add(hijo);
scene.add(grupo);

grupo.position.set(5, 0, 0);
hijo.position.set(0, 2, 0);

const p = new THREE.Vector3();

// Antes de actualizar, la matriz de mundo sigue siendo la identidad.
p.setFromMatrixPosition(hijo.matrixWorld);
console.log('sin actualizar:', p.toArray());        // (0, 0, 0)

scene.updateMatrixWorld(true);
p.setFromMatrixPosition(hijo.matrixWorld);
console.log('tras actualizar:', p.toArray());        // (5, 2, 0)

Hay un atajo que conviene conocer y que evita el error por completo: los métodos getWorldPosition, getWorldQuaternion, getWorldScale y getWorldDirection actualizan por su cuenta la cadena de matrices desde la raíz hasta el objeto antes de leer. Son seguros en cualquier momento del fotograma. Leer directamente de matrixWorld, en cambio, no lo es.

Actualizar un nodo sin recorrer toda la escena

updateMatrixWorld recorre todo el subárbol desde el nodo en que se llama. Sobre la escena entera, en una escena grande, eso es caro y casi nunca es lo que necesitas cuando solo quieres consultar un objeto.

Para ese caso existe updateWorldMatrix, con dos argumentos que indican en qué direcciones propagar: hacia los ancestros y hacia los descendientes. Llamarlo con el primero activo y el segundo desactivado actualiza exactamente la cadena que va desde la raíz hasta ese objeto, y nada más. Es lo que hacen internamente los métodos de consulta global.

import * as THREE from 'three';

// Actualiza solo la cadena de ancestros: lo minimo para leer este objeto.
hijo.updateWorldMatrix(true, false);

// Actualiza el objeto y todo su subarbol, sin tocar los ancestros.
grupo.updateWorldMatrix(false, true);

La distinción importa cuando tienes un sistema que consulta posiciones globales de muchos objetos por fotograma —un sistema de audio espacial, uno de proximidad, uno de etiquetas— y estás llamando a la actualización completa una vez por consulta. La solución correcta suele ser una sola actualización de la escena al principio del fotograma y consultas directas después.

Las dos banderas y qué apaga cada una

matrixAutoUpdate controla el primer paso: si está desactivado, el nodo deja de recomponer su matriz local a partir de posición, rotación y escala. La matriz local pasa a ser responsabilidad tuya: o la escribes directamente, o llamas a updateMatrix cuando cambies las propiedades.

matrixWorldAutoUpdate controla el segundo y el tercero: si está desactivado, el recorrido automático no entra en ese nodo y su matriz de mundo no se recalcula. Es la forma de congelar un subárbol entero.

Las dos tienen sus valores por defecto en propiedades estáticas de la clase, Object3D.DEFAULT_MATRIX_AUTO_UPDATE y Object3D.DEFAULT_MATRIX_WORLD_AUTO_UPDATE, que se pueden cambiar globalmente. Cambiarlas afecta solo a los objetos creados después.

import * as THREE from 'three';

const estatico = new THREE.Mesh(
  new THREE.BoxGeometry(),
  new THREE.MeshNormalMaterial()
);

estatico.position.set(3, 0, -2);
estatico.rotation.y = 0.4;
estatico.updateMatrix();              // se compone una vez
estatico.matrixAutoUpdate = false;    // y ya no se recompone nunca mas

// Si mas adelante hay que moverlo, hay que pedirlo explicitamente.
function moverEstatico(objeto, x, y, z) {
  objeto.position.set(x, y, z);
  objeto.updateMatrix();              // sin esto, el cambio no tiene ningun efecto
}
Desactivar matrixAutoUpdate es la optimización con mejor relación entre esfuerzo y resultado en escenas grandes, y la que más silenciosamente rompe cosas

La cuenta es fácil de hacer y sorprende. Componer una matriz local a partir de un vector, un cuaternión y una escala son unas cuarenta operaciones de coma flotante; multiplicarla por la del padre son sesenta y cuatro multiplicaciones y cuarenta y ocho sumas. Con diez mil nodos, y da igual que sean visibles, que estén dentro de cámara o que se muevan, eso es más de un millón de operaciones por fotograma dedicadas exclusivamente a recalcular transformaciones que en su mayoría no han cambiado. En una escena arquitectónica o en un mapa, donde el noventa y nueve por ciento de los objetos son estáticos, desactivar la actualización automática en todo lo que no se mueve elimina prácticamente ese coste entero, y es una línea por objeto dentro del bucle de carga. Ahora la parte que hay que saber antes de aplicarlo, porque produce un bug que nadie relaciona con la optimización. Una vez desactivado, cambiar position deja de tener cualquier efecto, en silencio, sin error ni aviso. El objeto se queda donde estaba y el desarrollador que lo mueve seis meses después no tiene forma de saber por qué. Peor todavía: si el objeto lo mueve un sistema genérico —una animación, una interpolación, un motor de física— ese sistema funcionará correctamente con todos los objetos salvo con los marcados como estáticos, y el fallo parecerá aleatorio. Tres precauciones que lo hacen manejable: marca el objeto con algo visible, por ejemplo una bandera en userData, para que cualquiera que lo inspeccione lo vea; encapsula el movimiento en una función que llame siempre a updateMatrix, de modo que no haya forma de moverlo mal; y no lo apliques por defecto a toda la escena, solo a los subárboles donde el ahorro esté medido. Como todas las optimizaciones que cambian una invariante del sistema, el coste no está en implementarla sino en que alguien la desconozca.

Cuánto cuesta de verdad

Merece la pena poner números para no optimizar a ciegas. El coste de actualización es lineal en el número de nodos y no depende de si son visibles, de si están dentro del volumen de la cámara ni de si tienen geometría.

Escena Nodos Operaciones de matriz por fotograma
Interfaz con un modelo 50 100
Escena de producto con detalles 800 1600
Arquitectura completa 12000 24000
Mapa con vegetación instanciada mal 60000 120000

La última fila es reconocible: alguien creó una malla por planta en lugar de usar instanciación, y la escena va lenta con la GPU al treinta por ciento. El perfil muestra tiempo en la actualización de matrices y no en el dibujado. Es un problema de CPU con una solución estructural, y desactivar la actualización automática lo alivia pero no lo resuelve: lo que hay que quitar son los sesenta mil nodos.

Esa es, de hecho, la conclusión general del tema. Las banderas de actualización sirven para afinar; la decisión que de verdad determina el coste es cuántos nodos hay en el árbol, y esa se toma al diseñar la escena. Los grupos y los pivotes, que son la razón más común para añadir nodos, son el siguiente tema.