Hi,
I've come across an animation blending issue which is related to the SkeletonAnimator.
Root of the problem is the way Spine blends animations together. Spine's blending happens incrementally, using weight to blend between the previous pose and the clip pose.
If you have two clips (A and B), and they have weights of 100% and 0% respectively, everything is fine. But when you're in a transition, the end result is wrong. Let's say the weights are A=50% and B=50%. The end result should be a mix between the clips, but it's not. The weights end up being 25% A, 50% B and ref pose 25%!
Let me illustrate the problem. When blending in the first clip A with 50%, we actually end up with ref pose 50% and clip A 50%.
Then, we blend 50% clip B, and we end up with ref pose 25%, clip A 25% and clip B 50%.
The problem becomes even worse with layers which affect blending. If we have layer with 50% blend weight, the clip A blends in 25% (50%50%), base 75%. Then the second clip B blends in 25% too, ending up base 75% * 75% = 56,25%, clip A =25%75% = 18,75% and clip B 25%.
To make it even worse, the evaluation order of clips affects the blending result. So it is not the same to have clips A and B <-> clip B and A.
The solution is to use method to calculate blend weights incrementally. We simply keep track of the amount of weighting accumulated so far (including the clip we're about to blend in).
So the example above becomes:
Clip A: weight = 0.5, totalWeight = 0.5 -> blendWeight = weight/totalWeight = 100%
Clip B: weight = 0.5, totalWeight = 0.5+0.5 -> blendWeight = weight/totalWeight = 50%
With the 50% layer mixing, it becomes:
Base: totalWeight = 0.5 (initial mix is 100% of course)
Clip A: weight = 0.25, totalWeight = 0.75 -> blendWeight = weight/totalWeight = 33% = 1/3
Clip B: weight = 0.25, totalWeight = 1 -> blendWeight = weight/totalWeight = 25% = 1/4
Resulting weights are: base = 1 * 2/3 * 3/4 = 2/4 (50%), Clip A = 1/3 * 3/4 = 1/4 (25%), Clip B = 1/4 (25%)
// Apply
for (int layer = 0, n = animator.layerCount; layer < n; layer++) {
float layerWeight = (layer == 0) ? 1 : animator.GetLayerWeight(layer); // Animator.GetLayerWeight always returns 0 on the first layer. Should be interpreted as 1.
AnimatorStateInfo stateInfo = animator.GetCurrentAnimatorStateInfo(layer);
AnimatorStateInfo nextStateInfo = animator.GetNextAnimatorStateInfo(layer);
bool hasNext = nextStateInfo.fullPathHash != 0;
AnimatorClipInfo[] clipInfo = animator.GetCurrentAnimatorClipInfo(layer);
AnimatorClipInfo[] nextClipInfo = animator.GetNextAnimatorClipInfo(layer);
MixMode mode = layerMixModes[layer];
if (mode == MixMode.AlwaysMix) {
// Always use Mix instead of Applying the first non-zero weighted clip.
for (int c = 0; c < clipInfo.Length; c++) {
var info = clipInfo[c]; float weight = info.weight * layerWeight; if (weight == 0) continue;
animationTable[NameHashCode(info.clip)].Apply(skeleton, 0, AnimationTime(stateInfo.normalizedTime, info.clip.length, stateInfo.loop), stateInfo.loop, null, weight, false, false);
}
if (hasNext) {
for (int c = 0; c < nextClipInfo.Length; c++) {
var info = nextClipInfo[c]; float weight = info.weight * layerWeight; if (weight == 0) continue;
animationTable[NameHashCode(info.clip)].Apply(skeleton, 0, nextStateInfo.normalizedTime * info.clip.length, nextStateInfo.loop, null, weight, false, false);
}
}
becomes
// Apply
for (int layer = 0, n = animator.layerCount; layer < n; layer++) {
float layerWeight = (layer == 0) ? 1 : animator.GetLayerWeight(layer); // Animator.GetLayerWeight always returns 0 on the first layer. Should be interpreted as 1.
AnimatorStateInfo stateInfo = animator.GetCurrentAnimatorStateInfo(layer);
AnimatorStateInfo nextStateInfo = animator.GetNextAnimatorStateInfo(layer);
bool hasNext = nextStateInfo.fullPathHash != 0;
AnimatorClipInfo[] clipInfo = animator.GetCurrentAnimatorClipInfo(layer);
AnimatorClipInfo[] nextClipInfo = animator.GetNextAnimatorClipInfo(layer);
MixMode mode = layerMixModes[layer];
// keep track of totalWeight to calculate propotional blend weights for each clip
float totalWeight = 1 - layerWeight; // take base in account
if (mode == MixMode.AlwaysMix) {
// Always use Mix instead of Applying the first non-zero weighted clip.
for (int c = 0; c < clipInfo.Length; c++) {
var info = clipInfo[c]; float weight = info.weight * layerWeight; if (weight == 0) continue;
// calculate blend weight using whatever weights we've accumulated so far
totalWeight += weight;
float blendWeight = weight / totalWeight;
animationTable[NameHashCode(info.clip)].Apply(skeleton, 0, AnimationTime(stateInfo.normalizedTime, info.clip.length, stateInfo.loop), stateInfo.loop, null, blendWeight, false, false);
}
if (hasNext) {
for (int c = 0; c < nextClipInfo.Length; c++) {
var info = nextClipInfo[c]; float weight = info.weight * layerWeight; if (weight == 0) continue;
totalWeight += weight;
float blendWeight = weight / totalWeight;
animationTable[NameHashCode(info.clip)].Apply(skeleton, 0, nextStateInfo.normalizedTime * info.clip.length, nextStateInfo.loop, null, blendWeight, false, false);
}
}
Any comments?