wandres.dev
ANIMACIÓN DE MODELOS · AnimationMixer y clips

Pesos, crossfade y mezcla aditiva

Cómo se combinan varias acciones sobre la misma propiedad, qué hace exactamente crossFadeTo con y sin warp, y cuándo la mezcla aditiva resuelve lo que la normal no puede.

⏱ 20 min

Mezclar animaciones es donde el sistema de Three.js deja de ser un reproductor y pasa a ser una mesa de mezclas. Varias acciones pueden estar activas a la vez sobre las mismas propiedades, cada una con su peso, y el mixer combina sus contribuciones. Sobre esa base se construyen las dos técnicas que hacen que un personaje se vea vivo: el crossfade entre estados y la mezcla aditiva, que son cosas distintas y resuelven problemas distintos.

🎯 Al terminar esta lección sabrás
  • Explicar cómo el mixer combina varias acciones activas sobre una misma propiedad.
  • Usar setEffectiveWeight correctamente y saber por qué existe frente a la propiedad weight.
  • Implementar un crossfade y decidir si necesita warp.
  • Aplicar mezcla aditiva con makeClipAdditive y saber en qué se diferencia de la normal.

Peso y peso efectivo

Cada acción tiene un weight entre 0 y 1. El mixer normaliza: si dos acciones tienen peso 1 y 1, cada una aporta la mitad; si tienen 0.75 y 0.25, aportan tres cuartos y un cuarto.

accion.weight = 0.5;                  // asigna la propiedad
accion.setEffectiveWeight( 0.5 );     // ademas actualiza el efectivo y para el fade

Igual que con la escala temporal, el método hace más que asignar:

setEffectiveWeight( weight ) {
  this.weight = weight;
  this._effectiveWeight = this.enabled ? weight : 0;
  return this.stopFading();
}

La diferencia clave es stopFading(). Si hay un fadeIn en curso, asignar weight a pelo no lo cancela: el interpolante de desvanecimiento sigue multiplicando y tu valor será ignorado. Usa siempre el método si puede haber un fade activo.

Y hay una asimetría que confunde: getEffectiveWeight() devuelve el peso realmente aplicado en el último frame, incluido el factor del fade. Es lo que se lee para pintar un medidor; weight es lo que se escribe.

El patrón que aparece en el ejemplo oficial de blending y que conviene copiar tal cual:

function ponerPeso( accion, peso ) {
  accion.enabled = true;
  accion.setEffectiveTimeScale( 1 );
  accion.setEffectiveWeight( peso );
}

Las tres líneas están porque un crossfade previo puede haber dejado la acción con enabled = false y con un timeScale alterado por el warp. Sin restaurar los tres, el resultado es impredecible.

Crossfade

saliente.crossFadeTo( entrante, 0.4, false );

La implementación no tiene magia: crossFadeTo delega en crossFadeFrom, que hace un fadeOut en una y un fadeIn en la otra con la misma duración.

crossFadeFrom( fadeOutAction, duration, warp = false ) {
  fadeOutAction.fadeOut( duration );
  this.fadeIn( duration );
  if ( warp === true ) { /* ... */ }
  return this;
}

Con una condición que no se declara en la firma y que es la fuente número uno de crossfades que no funcionan: la acción entrante tiene que estar en play() y con peso 1 antes del crossfade. fadeIn interpola el multiplicador del peso de 0 a 1, pero ese multiplicador se aplica sobre weight. Si la acción entrante tenía peso 0, el resultado del fade es 0 por 1, es decir 0, y no aparece nunca.

La secuencia correcta y completa:

function transicionar( saliente, entrante, duracion ) {
  ponerPeso( entrante, 1 );     // peso 1, enabled, timeScale 1
  entrante.time = 0;            // empezar el clip nuevo desde el principio
  entrante.play();
  saliente.crossFadeTo( entrante, duracion, true );
}

El warp, y por qué importa entre andar y correr

El tercer parámetro cambia el resultado de forma sustancial. Sin warp, las dos animaciones corren a su velocidad natural mientras se mezclan. Con warp, cada una ajusta gradualmente su timeScale para que su duración converja a la de la otra:

// Lo que hace crossFadeFrom con warp = true:
const startEndRatio = fadeOutDuration / fadeInDuration;
const endStartRatio = fadeInDuration / fadeOutDuration;
fadeOutAction.warp( 1.0, startEndRatio, duration );
this.warp( endStartRatio, 1.0, duration );

Traducido: la saliente acelera o frena hasta ir al ritmo de la entrante, y la entrante empieza al ritmo de la saliente y converge al suyo propio. Durante toda la transición, las dos van al mismo ritmo.

Esto importa muchísimo entre dos ciclos de locomoción. Un ciclo de andar dura 1.2 segundos y uno de correr 0.7. Sin warp, durante los 0.4 segundos de la mezcla, un ciclo va a 0.83 pasos por segundo y el otro a 1.43: los pies se descoordinan y el personaje patina visiblemente. Con warp, los dos ciclos se mantienen en fase y la transición se ve como una aceleración natural.

La regla: warp para ciclos, sin warp para poses. Entre andar y correr, warp; entre estar de pie y saludar, no hace falta y a veces estorba.

Un aviso que está en el propio ejemplo oficial: crossFadeTo deja la acción saliente con enabled = false y con el timeScale alterado. Por eso la función ponerPeso de arriba restaura las tres cosas antes de volver a usarla.

Mezcla aditiva

La mezcla normal es una interpolación: dos acciones al 50 % dan la pose intermedia. Eso es lo que quieres entre andar y correr, y es exactamente lo que no quieres para superponer un gesto.

Si tienes un ciclo de correr y una animación de girar la cabeza, mezclarlas al 50 % da un personaje que corre a media velocidad con la cabeza a medio girar. Lo que querías es correr al 100 % y además girar la cabeza.

La mezcla aditiva resuelve eso. Un clip aditivo no guarda poses absolutas sino diferencias respecto a una pose de referencia, y el mixer las suma en lugar de interpolarlas.

// Convertir un clip a aditivo, restandole el frame de referencia.
// El frame 0 suele ser la pose neutra del personaje.
THREE.AnimationUtils.makeClipAdditive( clipMirarArriba );

const correr = mixer.clipAction( clipCorrer );
const mirar  = mixer.clipAction( clipMirarArriba );

correr.play();

mirar.play();
mirar.setEffectiveWeight( 0.8 );   // 80 % del gesto, sumado encima

makeClipAdditive( targetClip, referenceFrame = 0, referenceClip = targetClip, fps = 30 ) recorre las pistas y resta el valor del frame de referencia a todos los keyframes. Para cuaterniones no resta: multiplica por el conjugado, que es la operación equivalente en el grupo de rotaciones. Y al terminar pone targetClip.blendMode = AdditiveAnimationBlendMode, que es lo que hace que el mixer use la ruta de acumulación aditiva.

Detalles que hay que tener presentes:

La conversión modifica el clip en el sitio. Si vas a usar el mismo clip en los dos modos, clónalo antes.

El frame de referencia tiene que ser la pose neutra correcta. Si el clip aditivo se hizo restando el frame 0 de sí mismo pero la pose neutra real está en otro clip, hay que pasarla como referenceClip. Un frame de referencia equivocado produce un desplazamiento constante que deforma al personaje.

El peso se comporta distinto. En aditivo, el peso es la intensidad del gesto, no una fracción de mezcla. Puede tener sentido pasarse de 1 para exagerar.

💡
Cuándo aditivo y cuándo normal

Normal para estados mutuamente excluyentes: idle, andar, correr, saltar. Solo uno es verdad a la vez y la transición es una interpolación. Aditivo para modificadores que se superponen a cualquier estado: respirar, apuntar, mirar a un punto, cojear, el retroceso de un disparo. Un sistema de personaje bien montado tiene una capa base normal con crossfades y varias capas aditivas encima con pesos independientes.

Los pesos no suman 1 y esa es la fuente de las poses encogidas

Aquí está el fallo que produce el artefacto más desconcertante del sistema, y que ocurre casi siempre en la primera implementación de una máquina de estados. El mixer normaliza los pesos dividiendo por su suma. Con dos acciones a peso 1 cada una, cada aporta la mitad y el resultado es la pose intermedia, que es lo esperado. El problema aparece cuando la suma es menor que uno: si haces un fadeOut de la única acción activa y no arrancas ninguna otra, el peso total baja hacia cero, y el resultado no es «la pose se mantiene» sino que la contribución de la animación desaparece y las propiedades vuelven a su valor de reposo. Para un personaje con esqueleto, ese valor de reposo es la pose de enlace del rig, que suele ser la T-pose o algo peor si el modelador no la limpió. Lo que se ve es el personaje colapsando bruscamente a una postura rígida durante el desvanecimiento, y como ocurre justo en la transición, la gente culpa al crossfade. Hay un segundo caso, más sutil: en escenas con muchas acciones que se activan y desactivan por eventos, es fácil llegar a un frame en que todas están desvaneciéndose a la vez y ninguna ha llegado todavía a peso pleno; el resultado es un espasmo de un frame. Las dos disciplinas que lo evitan son sencillas de enunciar y hay que respetarlas siempre: nunca dejes que el peso total llegue a cero, lo que en la práctica significa que un crossfade se hace siempre entre dos acciones y nunca haciendo fadeOut a secas; y arranca la entrante con peso pleno antes de iniciar la transición, que es el motivo de la función ponerPeso de esta lección. La mezcla aditiva, por cierto, está exenta: se acumula por encima de la base y no participa en la normalización, así que una capa aditiva a peso 0 simplemente no aporta nada y no arrastra a las demás.