AnimationClip y las pistas de keyframes
Qué contiene una pista, los seis tipos que existen y cuándo usar cada uno, los tres modos de interpolación, y cómo construir un clip a mano o recortar uno cargado.
Qué hay dentro de una pista
const pista = new THREE.VectorKeyframeTrack( 'cubo.position', // ruta de propiedad [ 0, 1, 2 ], // times, en segundos, ascendentes [ 0,0,0, 2,1,0, 0,0,0 ] // values, aplanados);Tres arrays y un nombre. times tiene un elemento por keyframe; values tiene times.length * valueSize elementos, donde valueSize sale del tipo: 3 para un vector, 4 para un cuaternión, 1 para un número.
El coste en memoria es directo: un personaje con 60 huesos animados a 30 fps durante 2 segundos son 60 pistas de posición, rotación y escala, con 60 keyframes cada una. La rotación es la cara: 60 huesos × 60 keys × 4 floats × 4 bytes son 57 KB solo de cuaterniones. Un set de veinte animaciones de personaje ronda el megabyte, y eso es lo que hay que tener en cuenta al decidir cuántas cargar de golpe.
Los seis tipos disponibles y su uso:
| Tipo | valueSize |
Para qué |
|---|---|---|
NumberKeyframeTrack |
1 | opacity, intensity, morphTargetInfluences[n] |
VectorKeyframeTrack |
2, 3 o 4 | position, scale |
QuaternionKeyframeTrack |
4 | quaternion |
ColorKeyframeTrack |
3 | material.color |
BooleanKeyframeTrack |
1 | visible |
StringKeyframeTrack |
1 | propiedades de texto |
La fila del cuaternión es la importante y ya viste por qué: el mixer usa el tipo para decidir si interpola linealmente o con slerp, y si mezcla por media ponderada o por slerp acumulativo. Una rotación tiene que ir en QuaternionKeyframeTrack, sin excepciones.
BooleanKeyframeTrack y StringKeyframeTrack fuerzan interpolación discreta por su naturaleza: no hay valor intermedio entre visible y no visible.
Los tres modos de interpolación
pista.setInterpolation( THREE.InterpolateSmooth );InterpolateDiscrete. El valor salta de un keyframe al siguiente sin transición. Es lo que se usa para animación por fotogramas, para conmutadores y para pistas de visibilidad.
InterpolateLinear. El valor por defecto de casi todas las pistas. Recta entre keyframes. Para cuaterniones, la implementación hace slerp, no lerp componente a componente.
InterpolateSmooth. Interpolación cúbica con tangentes calculadas a partir de los keyframes vecinos —una spline de Catmull-Rom—. Da un movimiento con aceleración y frenada natural, y por eso es lo que quieres para una cámara. El precio es que puede sobrepasar los valores de los keyframes: una pista que va de 0 a 1 puede pasar momentáneamente de 1. Con una escala o una opacidad eso produce artefactos.
QuaternionKeyframeTrack no admite InterpolateSmooth: no hay una spline cúbica bien definida sobre la esfera de cuaterniones en la implementación de Three.js. Si lo pides, se ignora.
glTF puede traer un cuarto modo, CUBICSPLINE, en el que las tangentes vienen guardadas explícitamente en el fichero en lugar de deducirse. GLTFLoader lo implementa con un interpolante propio, marcado internamente con isInterpolantFactoryMethodGLTFCubicSpline, que guarda tres valores por keyframe: tangente de entrada, valor y tangente de salida. Por eso una pista de glTF con cubic spline tiene un values tres veces más largo de lo que esperarías.
Construir un clip a mano
function clipDePuerta( nombre, duracion ) { const abrir = new THREE.QuaternionKeyframeTrack( 'Puerta.quaternion', [ 0, duracion ], [ ...new THREE.Quaternion().toArray(), // cerrada ...new THREE.Quaternion().setFromAxisAngle( new THREE.Vector3( 0, 1, 0 ), Math.PI / 2 ).toArray(), // abierta ] ); const chirrido = new THREE.NumberKeyframeTrack( 'PuertaSonido.volumen', [ 0, duracion * 0.2, duracion ], [ 0, 1, 0 ] ); return new THREE.AnimationClip( nombre, duracion, [ abrir, chirrido ] );}const clip = clipDePuerta( 'abrir', 1.2 );mixer.clipAction( clip ).play();Si pasas -1 como duración, AnimationClip la deduce del keyframe más tardío de todas sus pistas. Es lo que hace resetDuration(), y es útil cuando construyes las pistas por código y no quieres llevar la cuenta.
Para animaciones sencillas existe AnimationClipCreator en three/addons/animation/AnimationClipCreator.js, con métodos estáticos para rotaciones, pulsaciones, sacudidas y cambios de color. Es un atajo cómodo para prototipos.
Manipular clips cargados
Un caso muy frecuente: el modelo trae una sola animación larga con todos los movimientos concatenados, y tú necesitas trozos.
const largo = gltf.animations[ 0 ]; // 0 a 20 s, todo seguido// subclip( clipOrigen, nombre, frameInicio, frameFin, fps )const idle = THREE.AnimationUtils.subclip( largo, 'idle', 0, 60, 30 );const andar = THREE.AnimationUtils.subclip( largo, 'andar', 61, 120, 30 );const correr = THREE.AnimationUtils.subclip( largo, 'correr', 121, 180, 30 );subclip copia el clip, filtra los keyframes cuyo tiempo por fps cae en el rango, desplaza todas las pistas para que el nuevo clip empiece en cero y recalcula la duración. Los índices son frames, no segundos, y por eso hay que pasar el fps correcto: si el original se exportó a 24 y le pasas 30, los cortes caen en el sitio equivocado.
Otras operaciones útiles sobre un clip cargado:
// Buscar por nombre en un array de clips o en un objeto animado.const salto = THREE.AnimationClip.findByName( gltf.animations, 'Salto' );// Quitar keyframes redundantes: los que estan en la recta entre sus vecinos.// Reduce el tamano en memoria sin cambiar el resultado visible.clip.optimize();// Recortar los keyframes fuera del rango real de la animacion.clip.trim();// Comprobar que las pistas son coherentes: tiempos ascendentes,// longitudes de arrays correctas, sin NaN.console.log( clip.validate() );optimize() merece un comentario. Muchos exportadores escriben un keyframe por frame aunque el valor no cambie o cambie linealmente. Sobre una animación de personaje exportada de Blender sin optimizar, optimize() suele quitar entre el 30 y el 60 % de los keyframes sin diferencia perceptible. Es gratis y se hace una vez al cargar.