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.
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 propiedadaccion.setEffectiveWeight( 0.5 ); // ademas actualiza el efectivo y para el fadeIgual 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 encimamakeClipAdditive( 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.