Simultaneous blended animations

I was trying to do weighted animation blending, and I thought I’d post it here. This allows a blend weight set per channel and/or per bone.



New additions in AnimChannel.java,

[java]

public static final float DEFAULT_BLEND_WEIGHT = 1f;



private boolean useBoneWeightsFrom = false;

private float blendWeight = DEFAULT_BLEND_WEIGHT;

private float blendWeightFrom = DEFAULT_BLEND_WEIGHT;

private float[] boneWeights = null;

private float[] boneWeightsFrom = null;



/**

  • @return the blend weight for the purpose of playing multiple simultaneous animations

    */

    public float getBlendWeight() {

    return blendWeight;

    }



    /**
  • @param weight Set the blend weight of the current animation

    */

    public void setBlendWeight(float weight) {

    blendWeight = weight;

    }



    /**
  • @return whether this channel has per-bone blend weights

    */

    public boolean hasWeightPerBone() {

    return useBoneWeightsFrom || boneWeights != null;

    }



    /**
  • create per-bone weights defaulting to default value; the weight array has the same number as the
  • affected bones

    */

    public void createBoneWeights() {

    createBoneWeights(DEFAULT_BLEND_WEIGHT);

    }



    /**
  • create per-bone weights defaulting to a weight value; the weight array has the same number as the
  • affected bones

    *
  • @param weight default weight value to fill the weight array

    */

    public void createBoneWeights(final float weight) {

    assert(weight >= 0f && weight <= 1f);



    if (boneWeights == null) {

    boneWeights = new float[affectedBones.size()];

    for (int i = 0; i < boneWeights.length; ++i) {

    if ( affectedBones.get(i) ) {

    boneWeights = weight;

    } else {

    boneWeights = 0;

    }

    }

    }

    }



    /**
  • @return The per-bone blend weight at the index
  • @param index the index, corresponding to bone index
  • @throws ArrayIndexOutOfBoundsException if index is out of bound
  • @throws NullPointerException if attempted access when <code>boneWeights</code>
  • have not been created

    */

    public float getBoneWeight(final int index) throws ArrayIndexOutOfBoundsException, NullPointerException

    if (useBoneWeightsFrom) {

    return boneWeightsFrom[index];

    }

    return boneWeights[index];

    }



    /**
  • replace bone weight array with input array

    *
  • @param weights source weight array

    */

    public void setBoneWeights(final float[] weights) {

    setBoneWeights(weights, false);

    }



    /**
  • set per-bone weights from source

    *
  • @param weights source weight array
  • @param copy whether to copy iteratively from the source; just assign and replace if false

    */

    public void setBoneWeights(final float[] weights, final boolean copy) {

    assert( weights.length == affectedBones.size() );



    if (copy) {

    assert(weights != null);

    if (boneWeights == null || boneWeights.length != weights.length) {

    boneWeights = new float[weights.length];

    }

    System.arraycopy(weights, 0, boneWeights, 0, weights.length);

    } else {

    boneWeights = weights;

    }

    }



    /**
  • clear per-bone weights by pointing the array to null

    */

    public void clearBoneWeights() {

    boneWeights = null;

    }

    [/java]



    Changes to AnimChannel.setAnim():

    [java]

    if (animation != null && blendTime > 0f){

    //…

    blendWeightFrom = blendWeight;

    boneWeightsFrom = boneWeights;

    [/java]



    Changes to AnimChannel.update():

    [java]

    //blendFrom.setTime(timeBlendFrom, 1f - blendAmount, control, this, vars);

    //replaced by:

    useBoneWeightsFrom = true;

    blendFrom.setTime(timeBlendFrom, 1f - blendAmount * blendWeightFrom, control, this, vars);

    useBoneWeightsFrom = false;



    //…

    //animation.setTime(time, blendAmount, control, this, vars);

    //replaced by:

    animation.setTime(time, blendAmount * blendWeight, control, this, vars);

    [/java]



    Changes to BoneTrack.setTime():

    [java]

    BitSet affectedBones = channel.getAffectedBones();

    if (affectedBones != null && !affectedBones.get(targetBoneIndex)) {

    return;

    }



    float adjustedWeight = weight;

    if ( channel.hasWeightPerBone() ) {

    adjustedWeight *= channel.getBoneWeight(targetBoneIndex);

    if (adjustedWeight == 0f) {

    return;

    }

    }

    //…skip to last line

    target.blendAnimTransforms(tempV, tempQ, scales != null ? tempS : null, adjustedWeight);

    [/java].
4 Likes

Cool, thanks!

@normen:

I’ve test this solution for multi animation blending for facial animation that I post in another topic, work pretty well and I just modify my local code.

I want to ask that will this be integrated into the core soon?

No idea

Why is there a weight per bone? There’s already support for blended animations by changing the bones that are influenced by an AnimChannel, I think that should be enough in most cases.

@Momoko_Fan: Most case I agreed, but the current solution do not cover an important special case like facial animation, (or may be I’m the only artist want to use multi bone blending for facial animation). Let say:

I want a face expess “Anger” emotion and also say “F”… So several lip bones has to blend from Normal pose to Anger pose in 1 second, also has to blend from Normal pose to “say_F” pose in 0.5 second. That’s the case!

@atomix said:
@Momoko_Fan: Most case I agreed, but the current solution do not cover an important special case like facial animation, (or may be I'm the only artist want to use multi bone blending for facial animation). Let say:
I want a face expess "Anger" emotion and also say "F"... So several lip bones has to blend from Normal pose to Anger pose in 1 second, also has to blend from Normal pose to "say_F" pose in 0.5 second. That's the case!

In that case, isn't it enough to set a weight for the whole animation channel rather than specific bones?
@Momoko_Fan said:
Why is there a weight per bone? There's already support for blended animations by changing the bones that are influenced by an AnimChannel, I think that should be enough in most cases.


Mostly for quick tuning of finer details without having additional channels. Say for example, a character's walk animation is on an AnimChannel that influences all bones. As the character's individual arms receive damage, they go increasingly limp and swing less and less while walking. So the weights for the corresponding bones gradually decrease. While I could do this with several channels, I felt it was more straightforward with one, and also, it simplifies the process if I decide to do the same thing with some other body parts.
@robokitty said:
Mostly for quick tuning of finer details without having additional channels. Say for example, a character's walk animation is on an AnimChannel that influences all bones. As the character's individual arms receive damage, they go increasingly limp and swing less and less while walking. So the weights for the corresponding bones gradually decrease. While I could do this with several channels, I felt it was more straightforward with one, and also, it simplifies the process if I decide to do the same thing with some other body parts.

OK, that makes sense. Actually the way the code is right now, doing any sort of channel-with-channel blending won't work, including what you posted here. The reason is that the current blend engine only supports blending between two animations over time on the same AnimChannel (see the setAnim call that takes a blendTime argument). In other words, only two animations can be blended, the rest are ignored. The relevant code is here: https://code.google.com/p/jmonkeyengine/source/browse/trunk/engine/src/core/com/jme3/animation/Bone.java#570

Ack, should’ve been more thorough in testing this. Something else to think about then. 8)