Multiple Animation Blending

I’ve been tweaking the animation system to allow for multiple weighted animation blending, so not blending between the end of one animation and the start of another, but rather a constant weighted blend of multiple animations . Why? …



walk animation



Blue: forward walk cycle animated in Blender - rotated 0°

Yellow: sideways walk cycle animated in Blender - rotated 90°

Green: diagonal walk cycle - procedural blend of the above - rotated 45°



Promising early results, hopefully I can find a way to contribute the tweaks, they are a bit raw atm.



Cheers

James

10 Likes

This gif is shown in Blender on in jME?

jME

Looks nice, the feet end up going through each other a little but I guess that’s unavoidable.

@zarch said:
Looks nice, the feet end up going through each other a little but I guess that's unavoidable.


Yeah that's probably just my crappy animation.

Another usage:

forward animations

Blue: walk cycle animated in Blender
Yellow: run cycle animated in Blender (2x speed of walk)
Green: jog cycle - procedural blend of the above
4 Likes

This looks just awsome! WOW

At first glance it looks like green and yellow re covering the same amount of ground. That made me go O_o but on closer inspection, they do cover more ground than the blue guy. :wink:

wow. excellent work!

:o holy! wicked! :o

Really nice and useful! Credible animations make a game more immersive. Best example: http://code.google.com/p/wowmodelviewer/



/dance

/wave



:smiley:

1 Like

This is very intrresting, following your work closely :slight_smile:

1 Like

Effin’ cool! Is this something you’re hoping to bring into core then?

I’ve been watching this one closely. This was something I was going to look into also when I got to animation.

@erlend_sh for sure, I’ve only changed a handful of lines in the core to get this up and running, and I’ve only got to finish working through a few more tasks.



The overall usefulness of animation blending, as with many aspects of jME, will come down to intelligent usage - my walking examples work because the cycles were hand built to work together like this, if the strides were out of sync or the sideways animation was different, the animation would fall apart and go all twitchy.

1 Like

@thetoucher

What? Only a comment? I was expecting a “Cartwheel based on two steps forward.” animated gif.



I am disapoint. :wink: :stuck_out_tongue:



Great work btw. XD

1 Like

@thetoucher questioning how you did this =D
I want to merge 2 animations together, and am I limited to using a static weight, or can I setup a variable rated weight?
I have a post, that goes more into detail what I want. hope you can help, regards. http://hub.jmonkeyengine.org/forum/topic/interpolation-in-models/

1 Like

I would love to see that code. It seems that, a year and a half later, JME still hasn’t implemented multiple animation blending in trunk.

1 Like

So I messed around with the AnimChannel class and tried this for myself. I put together a test where I blend between four different animation (walking forward and backward, sidestepping left and right). The weight of each animation varies depending on the rotation of the character. Some blend do look kinda weird but with better base animations I think it could be really useful.

[video]animblend 2014 10 13 21 26 21 - YouTube

For anyone interested, below is the code of the modified AnimChannel.java. There is still some work to be done but I think it retains the original functionality for regular animations so it should not break anything. Anyway, it would be really cool to see this functionality make it into core.

[java]
/*

  • Copyright © 2009-2012 jMonkeyEngine
  • All rights reserved.
  • Redistribution and use in source and binary forms, with or without
  • modification, are permitted provided that the following conditions are
  • met:
    • Redistributions of source code must retain the above copyright
  • notice, this list of conditions and the following disclaimer.
    • Redistributions in binary form must reproduce the above copyright
  • notice, this list of conditions and the following disclaimer in the
  • documentation and/or other materials provided with the distribution.
    • Neither the name of ‘jMonkeyEngine’ nor the names of its contributors
  • may be used to endorse or promote products derived from this software
  • without specific prior written permission.
  • THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
  • “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
  • TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
  • PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
  • CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
  • EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
  • PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
  • PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
  • LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
  • NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
  • SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
    */
    package com.jme3.animation;

import com.jme3.math.FastMath;
import com.jme3.util.TempVars;
import java.util.BitSet;

/**

  • AnimChannel provides controls, such as play, pause,

  • fast forward, etc, for an animation. The animation

  • channel may influence the entire model or specific bones of the model’s

  • skeleton. A single model may have multiple animation channels influencing

  • various parts of its body. For example, a character model may have an

  • animation channel for its feet, and another for its torso, and

  • the animations for each channel are controlled independently.

  • @author Kirill Vainer
    */
    public final class AnimChannel {

    private static final float DEFAULT_BLEND_TIME = 0.15f;

    private AnimControl control;

    private BitSet affectedBones;

    //private Animation animation;
    //private float weight;

    private String currentName;
    private AnimContainer[] animations;
    private AnimContainer[] blendFroms;

    //private Animation blendFrom;
    private float time;
    private float speed;
    private float timeBlendFrom;
    private float blendTime;
    private float speedBlendFrom;
    private boolean notified=false;

    private LoopMode loopMode, loopModeBlendFrom;

    private float blendAmount = 1f;
    private float blendRate = 0;

    private static float clampWrapTime(float t, float max, LoopMode loopMode){
    if (t == 0) {
    return 0; // prevent division by 0 errors
    }

     switch (loopMode) {
         case Cycle:
             boolean sign = ((int) (t / max) % 2) != 0;
             float result;
    

// if (t < 0){
// result = sign ? t % max : -(max + (t % max));
// } else {
// NOTE: This algorithm seems stable for both high and low
// tpf so for now its a keeper.
result = sign ? -(max - (t % max)) : t % max;
// }

// if (result <= 0 || result >= max) {
// System.out.println("SIGN: " + sign + ", RESULT: " + result + ", T: " + t + ", M: " + max);
// }

            return result;
        case DontLoop:
            return t &gt; max ? max : (t &lt; 0 ? 0 : t);
        case Loop:
            return t % max;
    }
    return t;

// if (max == Float.POSITIVE_INFINITY)
// return t;
//
// if (t < 0f){
// //float tMod = -(-t % max);
// switch (loopMode){
// case DontLoop:
// return 0;
// case Cycle:
// return t;
// case Loop:
// return max - t;
// }
// }else if (t > max){
// switch (loopMode){
// case DontLoop:
// return max;
// case Cycle:
// return -(2f * max - t) % max;
// case Loop:
// return t % max;
// }
// }
//
// return t;
}

AnimChannel(AnimControl control){
    this.control = control;
    //this.animations = new ArrayList&lt;AnimContainer&gt;();
}

/**
 * Returns the parent control of this AnimChannel.
 * 
 * @return the parent control of this AnimChannel.
 * @see AnimControl
 */
public AnimControl getControl() {
    return control;
}

/**
 * @return The name of the currently playing animation, or null if
 * none is assigned.
 *
 * @see AnimChannel#setAnim(java.lang.String) 
 */
//public String getAnimationName() {
//    return animation != null ? animation.getName() : null;
//}

public String getAnimationName() {
	if(animations == null)
		return null;
	return currentName;
}

/**
 * @return The loop mode currently set for the animation. The loop mode
 * determines what will happen to the animation once it finishes
 * playing.
 * 
 * For more information, see the LoopMode enum class.
 * @see LoopMode
 * @see AnimChannel#setLoopMode(com.jme3.animation.LoopMode)
 */
public LoopMode getLoopMode() {
    return loopMode;
}

/**
 * @param loopMode Set the loop mode for the channel. The loop mode
 * determines what will happen to the animation once it finishes
 * playing.
 *
 * For more information, see the LoopMode enum class.
 * @see LoopMode
 */
public void setLoopMode(LoopMode loopMode) {
    this.loopMode = loopMode;
}

/**
 * @return The speed that is assigned to the animation channel. The speed
 * is a scale value starting from 0.0, at 1.0 the animation will play
 * at its default speed.
 *
 * @see AnimChannel#setSpeed(float)
 */
public float getSpeed() {
    return speed;
}

/**
 * @param speed Set the speed of the animation channel. The speed
 * is a scale value starting from 0.0, at 1.0 the animation will play
 * at its default speed.
 */
public void setSpeed(float speed) {
    this.speed = speed;
    float longest = 0f;
    for(int i = 0; i &lt; animations.length; i++) {
    		if(longest &lt; animations[i].anim.getLength())
    			longest = animations[i].anim.getLength();
    }
    
    if(blendTime&gt;0){
        this.speedBlendFrom = speed;
        blendTime = Math.min(blendTime, longest / speed);  
        blendRate = 1/ blendTime;
    }
}

/**
 * @return The time of the currently playing animation. The time
 * starts at 0 and continues on until getAnimMaxTime().
 *
 * @see AnimChannel#setTime(float)
 */
public float getTime() {
    return time;
}

/**
 * @param time Set the time of the currently playing animation, the time
 * is clamped from 0 to {@link #getAnimMaxTime()}. 
 */
public void setTime(float time) {
    this.time = FastMath.clamp(time, 0, getAnimMaxTime());
}

/**
 * @return The length of the currently playing animation, or zero
 * if no animation is playing.
 *
 * @see AnimChannel#getTime()
 */
public float getAnimMaxTime(){
    if(animations == null)
    		return 0f;
    float longest = 0f;
    for(int i = 0; i &lt; animations.length; i++) {
        if(longest &lt; animations[i].anim.getLength())
        	longest = animations[i].anim.getLength();
    }
    return longest;
    
    //return animation != null ? animation.getLength() : 0f;
}

/**
 * Set the current animation that is played by this AnimChannel.
 * &lt;p&gt;
 * This resets the time to zero, and optionally blends the animation
 * over <code>blendTime</code> seconds with the currently playing animation.
 * Notice that this method will reset the control's speed to 1.0.
 *
 * @param name The name of the animation to play
 * @param blendTime The blend time over which to blend the new animation
 * with the old one. If zero, then no blending will occur and the new
 * animation will be applied instantly.
 */
public void setAnim(String name, float blendTime){
    String[] names = {name}; 
    float[] weights = {1f};
    setAnim(name, names, weights, blendTime);
}

public void setAnim(String name, String[] names, float[] weights, float blendTime) {
  if (name == null)
        throw new IllegalArgumentException("name cannot be null");

	if(names.length != weights.length)
        throw new IllegalArgumentException("number of names and weights does not match");

	if (blendTime &lt; 0f)
        throw new IllegalArgumentException("blendTime cannot be less than zero");

  AnimContainer[] tmp = new AnimContainer[names.length];
  float longest = 0;
  for(int i = 0; i &lt; tmp.length; i++) {
  
    if (names[i] == null)
        throw new IllegalArgumentException("name cannot be null");
  
  	Animation anim = control.animationMap.get(names[i]);
  	
  	if (anim == null)
        throw new IllegalArgumentException("Cannot find animation named: '"+names[i]+"'");
  	
  	tmp[i] = new AnimContainer(anim, 1f, weights[i]);
  	
  	if(longest &lt; anim.getLength())
  		longest = anim.getLength();
  }
  
  currentName = name;

	control.notifyAnimChange(this, name);

  if(blendTime &gt; 0f) {
  	this.blendTime = blendTime;
  	blendTime = Math.min(blendTime, longest / speed); 
  	blendFroms = animations;
  	timeBlendFrom = time;
  	speedBlendFrom = speed;
  	loopModeBlendFrom = loopMode;
  	blendAmount = 0f;
  	blendRate   = 1f / blendTime;
  } else {
  	blendFroms = null;
  }
  
  animations = tmp;
  loopMode = LoopMode.Loop;
  notified = false;
}

public void setWeights(float[] weights) {
if(animations.length != weights.length)
	throw new IllegalArgumentException("number of animations and weights does not match");

	for(int i = 0; i &lt; animations.length; i++) {
		animations[i].weight = weights[i];
	}
}

/**
 * Set the current animation that is played by this AnimChannel.
 * &lt;p&gt;
 * See {@link #setAnim(java.lang.String, float)}.
 * The blendTime argument by default is 150 milliseconds.
 * 
 * @param name The name of the animation to play
 */
public void setAnim(String name){
    setAnim(name, DEFAULT_BLEND_TIME);
}

/**
 * Add all the bones of the model's skeleton to be
 * influenced by this animation channel.
 */
public void addAllBones() {
    affectedBones = null;
}

/**
 * Add a single bone to be influenced by this animation channel.
 */
public void addBone(String name) {
    addBone(control.getSkeleton().getBone(name));
}

/**
 * Add a single bone to be influenced by this animation channel.
 */
public void addBone(Bone bone) {
    int boneIndex = control.getSkeleton().getBoneIndex(bone);
    if(affectedBones == null) {
        affectedBones = new BitSet(control.getSkeleton().getBoneCount());
    }
    affectedBones.set(boneIndex);
}

/**
 * Add bones to be influenced by this animation channel starting from the
 * given bone name and going toward the root bone.
 */
public void addToRootBone(String name) {
    addToRootBone(control.getSkeleton().getBone(name));
}

/**
 * Add bones to be influenced by this animation channel starting from the
 * given bone and going toward the root bone.
 */
public void addToRootBone(Bone bone) {
    addBone(bone);
    while (bone.getParent() != null) {
        bone = bone.getParent();
        addBone(bone);
    }
}

/**
 * Add bones to be influenced by this animation channel, starting
 * from the given named bone and going toward its children.
 */
public void addFromRootBone(String name) {
    addFromRootBone(control.getSkeleton().getBone(name));
}

/**
 * Add bones to be influenced by this animation channel, starting
 * from the given bone and going toward its children.
 */
public void addFromRootBone(Bone bone) {
    addBone(bone);
    if (bone.getChildren() == null)
        return;
    for (Bone childBone : bone.getChildren()) {
        addBone(childBone);
        addFromRootBone(childBone);
    }
}

BitSet getAffectedBones(){
    return affectedBones;
}

public void reset(boolean rewind){
    if(rewind){
        setTime(0);        
        if(control.getSkeleton()!=null){
            control.getSkeleton().resetAndUpdate();
        }else{
            TempVars vars = TempVars.get();
            update(0, vars);
            vars.release();    
        }
    }
    animations = null;
   // System.out.println("Setting notified false");
    notified = false;
}

void update(float tpf, TempVars vars) {
    if (animations == null)
        return;
    
    
    if (blendFroms != null &amp;&amp; blendAmount != 1.0f){
        
        for(int i = 0; i &lt; blendFroms.length; i++) {
        	AnimContainer ac = blendFroms[i];
        	ac.anim.setTime(ac.time, ac.weight * (1f - blendAmount), control, this, vars);
        	ac.time += tpf * speedBlendFrom;
        	ac.time = clampWrapTime(ac.time, ac.anim.getLength(), loopModeBlendFrom);
        	
        	if(ac.time &lt; 0) {
        		ac.time = -ac.time;
        		speedBlendFrom = -speedBlendFrom;
        	}
        }
        
        blendAmount += tpf * blendRate;
        if (blendAmount &gt; 1f){
            blendAmount = 1f;
            blendFroms = null;
        }
    }
    
    //animation.setTime(time, weight, control, this, vars);
    //time += tpf * speed;
    
    for(int i = 0; i &lt; animations.length; i++) {
    		AnimContainer ac = animations[i];
    		ac.anim.setTime(ac.time, ac.weight * blendAmount, control, this, vars);
    		ac.time += tpf * speed;
    		ac.time = clampWrapTime(ac.time, ac.anim.getLength(), loopMode);
    }
    
    // When should cycle done be called?
    if (animations[0].anim.getLength() &gt; 0){
        if (!notified &amp;&amp; (time &gt;= animations[0].anim.getLength() || time &lt; 0)) {
            if (loopMode == LoopMode.DontLoop) {
                // Note that this flag has to be set before calling the notify
                // since the notify may start a new animation and then unset
                // the flag.
                notified = true;
            }
            control.notifyAnimCycleDone(this, animations[0].anim.getName());
        } 
    }
    //time = clampWrapTime(time, animation.getLength(), loopMode);
    if (time &lt; 0){
        // Negative time indicates that speed should be inverted
        // (for cycle loop mode only)
        time = -time;
        speed = -speed;
    }
}

private class AnimContainer {
	public Animation anim;
	public float speed;
	public float weight;
	public float time;
	
	public AnimContainer(Animation anim, float speed, float weight) {
		this.anim = anim;
		this.speed = speed;
		this.weight = weight;
		this.time = 0f;
	}
}

}
[/java]

3 Likes