Skinning lineal por mezcla y el candy wrapper
Por qué promediar matrices de rotación produce el colapso característico de las articulaciones torcidas, cómo demostrarlo en cinco líneas y qué técnicas lo mitigan.
El skinning que usa Three.js —y Unity, y Unreal, y prácticamente todo lo que corre en tiempo real— se llama skinning lineal por mezcla, y tiene un defecto matemático conocido desde los años noventa que nadie ha conseguido eliminar sin pagar por otro lado. El defecto se ve mejor que se explica: gira una muñeca ciento ochenta grados y observa cómo el antebrazo se estrangula hasta desaparecer. Esta lección demuestra por qué ocurre, con números, y repasa las cuatro salidas reales.
- Demostrar numéricamente por qué la media de dos matrices de rotación no es una rotación.
- Reproducir el artefacto del candy wrapper y el de la pérdida de volumen en el codo.
- Evaluar las cuatro mitigaciones habituales y su coste.
- Explicar por qué Three.js no incluye skinning con cuaterniones duales.
El problema está en la palabra lineal
El skinning lineal por mezcla hace, para cada vértice, una media ponderada de las posiciones que le asignaría cada hueso por separado. Como la transformación es afín y la suma ponderada conmuta con ella, eso es equivalente a transformar el vértice por la media ponderada de las matrices. Y ahí está el problema, porque el conjunto de las matrices de rotación no es convexo: la media de dos rotaciones no es una rotación.
Con dos huesos y peso 0.5 cada uno, el vértice se transforma por 0.5 * A + 0.5 * B. Si A es la identidad y B es una rotación de ciento ochenta grados alrededor del eje Y, la media es esta:
import * as THREE from 'three';
const a = new THREE.Matrix4(); // identidad
const b = new THREE.Matrix4().makeRotationY( Math.PI ); // 180 grados en Y
const media = new THREE.Matrix4();
for ( let i = 0; i < 16; i ++ ) {
media.elements[ i ] = 0.5 * a.elements[ i ] + 0.5 * b.elements[ i ];
}
const v = new THREE.Vector3( 1, 0, 0 ).applyMatrix4( media );
console.log( v ); // Vector3 { x: 0, y: 0, z: 0 }
El punto que estaba a una unidad del eje acaba exactamente sobre el eje. Y no es un caso patológico aislado: la matriz media es diag( 0, 1, 0 ), de rango uno. Todo el plano XZ colapsa sobre la recta Y. Cualquier vértice del anillo central del antebrazo, sea cual sea su ángulo, se va al centro. La sección transversal del brazo se convierte en un punto, y el resultado tiene el aspecto exacto del extremo retorcido de un caramelo envuelto en papel. De ahí el nombre.
El mismo defecto en pequeño: la pérdida de volumen
El candy wrapper es el caso extremo, pero la misma matemática produce un artefacto mucho más común y más difícil de ver: la pérdida de volumen en cualquier flexión.
Toma el codo doblado noventa grados. Un vértice de la cara interna con pesos 0.5 y 0.5 se transforma por la media de la identidad y una rotación de noventa grados. La media de dos matrices ortonormales tiene, en general, determinante menor que uno: está entre la identidad y la rotación, pero por la cuerda, no por el arco. El vértice acaba dentro de la circunferencia que debería recorrer, y el codo se hunde.
const identidad = new THREE.Matrix4();
const giro = new THREE.Matrix4().makeRotationZ( Math.PI / 2 );
const m = new THREE.Matrix4();
for ( let i = 0; i < 16; i ++ ) {
m.elements[ i ] = 0.5 * identidad.elements[ i ] + 0.5 * giro.elements[ i ];
}
const p = new THREE.Vector3( 1, 0, 0 ).applyMatrix4( m );
console.log( p.length() ); // 0.7071..., no 1
La regla general es sencilla: con pesos iguales y un ángulo t entre los dos huesos, el radio superviviente vale cos( t / 2 ). A cuarenta y cinco grados pierdes un 8 %, a noventa un 29 %, a ciento ochenta lo pierdes todo. Un 29 % de radio perdido en un solo pliegue. Es exactamente el mismo fenómeno que el candy wrapper, solo que a noventa grados en vez de a ciento ochenta, y por eso todos los personajes de videojuego tienen ese aspecto ligeramente desinflado en las articulaciones cuando no hay correcciones encima.
Las cuatro salidas
Repartir el giro entre más huesos. Si el problema es que ciento ochenta grados entre dos huesos colapsa, mete tres huesos intermedios y que cada uno gire cuarenta y cinco. La media de dos rotaciones de cuarenta y cinco grados pierde un 8 % de radio en vez de un 100 %. Es la técnica de los huesos de torsión, y es la razón por la que un rig humano decente tiene dos o tres huesos en el antebrazo en vez de uno. Coste: más huesos, cero coste de shader adicional por vértice, y la responsabilidad de mantener las rotaciones intermedias sincronizadas.
// Reparto de la torsion de la muneca a lo largo del antebrazo
const torsionTotal = muneca.rotation.y;
torsion1.rotation.y = torsionTotal * 0.33;
torsion2.rotation.y = torsionTotal * 0.66;
Limitar el rango. Si la articulación nunca llega a los ángulos que rompen, el artefacto no aparece. Es la solución del animador, no del programador, y es gratis. También es la que impone que ciertos movimientos sean imposibles.
Blend shapes correctivas. Un morph target que se activa en función del ángulo de la articulación y que devuelve el volumen perdido. Es lo que hace la industria del personaje, y Three.js lo soporta mezclando skinning y morph targets en el mismo material. Coste: memoria para la geometría adicional y trabajo de artista para esculpir cada corrección.
// Morph correctivo conducido por el angulo del codo
const anguloCodo = antebrazo.rotation.x;
const indice = mesh.morphTargetDictionary[ 'codo_flexionado' ];
mesh.morphTargetInfluences[ indice ] = THREE.MathUtils.clamp(
anguloCodo / ( Math.PI / 2 ), 0, 1
);
Skinning con cuaterniones duales. El único arreglo que ataca la raíz. Un cuaternión dual codifica una rotación y una traslación juntas, y su interpolación normalizada sí produce una transformación rígida: sin colapso y sin pérdida de volumen. A cambio introduce su propio artefacto, el abultamiento de las articulaciones, que en un hombro se nota bastante.
Por qué Three.js no trae cuaterniones duales
No es olvido. Un cuaternión dual representa una transformación rígida: rotación más traslación, nada más. No puede representar escala, y menos aún escala no uniforme. Y glTF permite escala arbitraria en cualquier nodo, incluidos los huesos. Un cargador que convirtiera a cuaterniones duales tendría que descartar silenciosamente la escala de los huesos o mantener dos caminos de código en paralelo.
Además, la conversión no es gratis: hay que convertir cada matriz de hueso a cuaternión dual en CPU, subir ocho flotantes por hueso en vez de dieciséis —eso sí ahorra— y en el shader hay que normalizar el dual acumulado y volver a construir la matriz. Y hay un detalle traicionero: los cuaterniones tienen doble cobertura, así que q y -q representan la misma rotación pero interpolan por caminos opuestos. Hay que alinear el signo de cada influencia con la primera antes de sumar, o los vértices se van al otro lado.
Si lo necesitas, se implementa como material propio con onBeforeCompile o como ShaderMaterial reescribiendo el chunk. No es un proyecto de una tarde, pero está documentado desde el artículo original de Kavan de 2007.
Cuando aparece un candy wrapper, el reflejo es repintar pesos. Casi siempre es inútil, porque el colapso no depende de los pesos: a ciento ochenta grados y con pesos 0.5 y 0.5 el radio se va a cero, y con 0.9 y 0.1 todavía pierdes un 20 %, pero con cualquier reparto tienes algún vértice cerca del 50 % si la transición es continua, que es precisamente lo que un buen pintado de pesos garantiza. Lo que sí lo arregla es la topología: la clave está en cuántos anillos de vértices hay repartiendo la torsión y cómo están espaciados. Un antebrazo con dos anillos entre codo y muñeca colapsa sí o sí; con ocho anillos y un hueso de torsión intermedio, el peor vértice está en el 50 % de una rotación de cuarenta y cinco grados y no se nota. Por eso los modeladores de personajes añaden bucles de arista en las articulaciones antes de pintar un solo peso, y por eso un modelo optimizado a base de decimar triángulos uniformemente se deforma mucho peor que uno con la misma cuenta de triángulos pero repartidos con criterio. Si heredas un modelo que hace candy wrapper y no puedes tocar la malla, la única salida rápida es el hueso de torsión: no arregla la matemática, pero divide el ángulo máximo por dos y eso baja la pérdida de radio de forma cuadrática.