A tip for animation blending!

Hi

A small tip you might find useful when doing animation blending using BlendSpace.

In my case, it is the idle/walk/run anim blending which is controlled by player movement speed.

When using the raw speed directly as blend value if there is a sudden change in movement speed (e.x start to move) then I notice a disruption in animation.

Using a SimpleMovingMean filter solves the issue and makes the transition smooth.

/**
 * Calculates speed with a low-pass filter using a simple moving average by tracking attached spatial movement.
 *
 * @author Ali-RS
 */
public class SpeedFilter extends AbstractControl implements Cloneable {

    private final Vector3f lastLocation = new Vector3f();
    private final Vector3f velocity = new Vector3f();
  
    private double filteredValue;

    private final Filterd lowPass = new SimpleMovingMean(10); // 1/6th second of data


    public SpeedFilter() {
    }

    public double getFilteredValue() {
        return filteredValue;
    }

    @Override
    public void setSpatial(Spatial spatial) {
        super.setSpatial(spatial);
        lastLocation.set(spatial.getWorldTranslation());
    }

    @Override
    protected void controlUpdate(float tpf) {
        // See what kind of movement is happening
        velocity.set(spatial.getWorldTranslation()).subtractLocal(lastLocation);

        // We don't account for up/down right now
        velocity.y = 0;

        // Extrapolate the real velocity using tpf
        if (tpf > 0) {
            velocity.x /= tpf;
            velocity.y /= tpf;
            velocity.z /= tpf;
        }

        lowPass.addValue(velocity.length());
        filteredValue = lowPass.getFilteredValue();

        lastLocation.set(spatial.getWorldTranslation());
    }

    @Override
    protected void controlRender(RenderManager rm, ViewPort vp) {

    }
}

And using it in my animation

/**
 * Mobility effect for humanoid characters.
 *
 * @author Ali-RS
 */
public class HumanoidMovement extends EntityEffect {
  
    final BlendSpace space = new LinearBlendSpace(0, 1);
    final double baseSpeed = 2.8;

    Tween tween;

    public HumanoidMovement() {
        // Specify anim channel name
        super("BaseMobility");
    }

    @Override
    public void initialize(Application app, EntityId id, Spatial model, EntityData ed) {
        AnimComposer ac = AnimUtils.getAnimComposer(model);
        if (model.getControl(SpeedFilter.class) == null) {
            model.addControl(new SpeedFilter());
        }

        AnimComposer locomotion = AnimPacks.getInstance().locomotionPack;
        ac.addAnimClip(AnimUtils.retargetClip(locomotion.getAnimClip("idle_2"), model));
        ac.addAnimClip(AnimUtils.retargetClip(locomotion.getAnimClip("walking"), model));
        ac.addAnimClip(AnimUtils.retargetClip(locomotion.getAnimClip("running"), model));

        // Make a blend tween animation for idle/walk/run mobility state
        tween = stretch(0.8, blend(space, 0,
                ac.getAnimClip("idle_2"), ac.getAnimClip("walking"), ac.getAnimClip("running")));
    }

    @Override
    public Animation create(Spatial target, EffectInfo existing) {
        SpeedFilter filter = target.getControl(SpeedFilter.class);

        return new TweenAnimation(true, tween) {
            @Override
            public boolean animate(double tpf) {
                boolean animate = super.animate(tpf);
      
                double blendValue = filter.getFilteredValue() / baseSpeed;
                blendValue = Math.round(blendValue * 1000) / 1000.0;
                // Clamp it to [0,1]
                blendValue = Math.min(blendValue, 1);

                space.setValue((float) blendValue);
                return animate;
            }
        };
    }
}

Hope you find it usefull. :slightly_smiling_face:

8 Likes

hi @Ali_RS, I’m trying to figure out how to use LinearBlendSpace. I have 2 animations (walking and running) which are not the same length. The result is that both animations result in slow motion regardless of the value I set in the blendSpace.

LinearBlendSpace blendSpace = new LinearBlendSpace(0, 1);
BlendAction action = animComposer.actionBlended("BlendTree", blendSpace, "walking", "running");
animComposer.setCurrentAction("BlendTree");

// in the update loop
protected void update(float tpf) {
	BlendAction action = (BlendAction) animComposer.getAction("BlendTree");
	action.getBlendSpace().setValue(FastMath.clamp(velocity, 0 ,1));
}

In the BlendAction class I noticed this comment:

//Blending effect maybe unexpected when blended animation don't have the same length
//Stretching any action that doesn't have the same length.

I didn’t understand if I have to stretch the animation or if the code already does it.

Then I remembered your post: what does this statement do?

how did you solve the problem? Could you help me please?

2 Likes

Hi

You can shrink the BlendAction using Tweens.stretch().

That is what I am doing in my code. I make a blend action by blending 3 animations then shrink it to a smaller length.

Yes, the code already does.

This a limitation in the current API. By default, it tries to stretch them so all have the same length but this may result in a slow-motion effect.

I have a raw idea in my head to add another mode to BlendAction that loops the animations instead of stretching them, which I believe should solve this issue.

3 Likes

thanks for your help @Ali_RS , you are a master of animations!

I think your idea is absolutely essential to solve this problem. Can’t wait to try it. :wink:

2 Likes

My experimental change is here in case anyone interested to try it out and give some feedback. :slightly_smiling_face:

I have not tested it myself yet. :stuck_out_tongue:

3 Likes

Just tested it on Jame. Blending “Idle” (7.45 second) , “Walk” (1.54 second) and “Run” (0.54 second) animations.

Seems to be working fine with Loop mode!

LoopMode:

StretchMode:

5 Likes

Great! Go ahead and submit a PR. This functionality must absolutely be added in the 3.4.0 release

I think 3.4.0 is already in feature freeze. But I think you can just copy this class as a custom action into your own code in the mean time. I don’t think it uses any private/protected access or anything and BlendAction is one of those that you add yourself to a model anyway.

4 Likes

ok, I’ll give it a try :+1:

1 Like

I tried to extend the BlendAction class but the code that stretches the animations is in the constructor, so I discarded this option. The MyBlendSpace class cannot implement the BlendSpace interface because the method setBlendAction(BlendAction action) expects a BlendAction which I cannot extend for the above reasons. In the end I opted for the fastest way, here are the 2 classes:

MyBlendAction

package com.capdevon.anim.fsm;

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

import com.jme3.anim.tween.action.Action;
import com.jme3.anim.tween.action.BlendableAction;
import com.jme3.anim.util.HasLocalTransform;
import com.jme3.math.Transform;

public class MyBlendAction extends BlendableAction {

    private int firstActiveIndex;
    private int secondActiveIndex;
    private final MyBlendSpace blendSpace;
    private final BlendMode blendMode;
    private float blendWeight;
    private double lastTime;
    // In stretch mode it contains time factor and in loop mode it contains the time
    final private double[] timeData;
    final private Map<HasLocalTransform, Transform> targetMap = new HashMap<>();

    public enum BlendMode {Stretch, Loop}

    public MyBlendAction(MyBlendSpace blendSpace, BlendableAction... actions) {
        this(blendSpace, BlendMode.Loop, actions);
    }

    public MyBlendAction(MyBlendSpace blendSpace, BlendMode blendMode, BlendableAction... actions) {
        super(actions);
        this.blendMode = blendMode;
        timeData = new double[actions.length];
        this.blendSpace = blendSpace;
        blendSpace.setBlendAction(this);

        for (BlendableAction action : actions) {
            if (action.getLength() > getLength()) {
                setLength(action.getLength());
            }
            Collection<HasLocalTransform> targets = action.getTargets();
            for (HasLocalTransform target : targets) {
                Transform t = targetMap.get(target);
                if (t == null) {
                    t = new Transform();
                    targetMap.put(target, t);
                }
            }
        }

        if (blendMode == BlendMode.Stretch) {
            //Blending effect maybe unexpected when blended animation don't have the same length
            //Stretching any action that doesn't have the same length.
            for (int i = 0; i < this.actions.length; i++) {
                this.timeData[i] = 1;
                if (this.actions[i].getLength() != getLength()) {
                    double actionLength = this.actions[i].getLength();
                    if (actionLength > 0 && getLength() > 0) {
                        this.timeData[i] = this.actions[i].getLength() / getLength();
                    }
                }
            }
        }
    }
    
    @Override
	public boolean interpolate(double t) {
		boolean interpolate = super.interpolate(t);
		if (!interpolate) {
			lastTime = 0;
		}
		return interpolate;
	}

    @Override
    public void doInterpolate(double t) {
        blendWeight = blendSpace.getWeight();
        BlendableAction firstActiveAction = (BlendableAction) actions[firstActiveIndex];
        BlendableAction secondActiveAction = (BlendableAction) actions[secondActiveIndex];
        firstActiveAction.setCollectTransformDelegate(this);
        secondActiveAction.setCollectTransformDelegate(this);

        //only interpolate the first action if the weight if below 1.
        if (blendWeight < 1f) {
            firstActiveAction.setWeight(1f);
            //firstActiveAction.interpolate(t * timeFactor[firstActiveIndex]);
            interpolate(firstActiveAction, firstActiveIndex, t);
            if (blendWeight == 0) {
                for (HasLocalTransform target : targetMap.keySet()) {
                    collect(target, targetMap.get(target));
                }
            }
        }

        //Second action should be interpolated
        secondActiveAction.setWeight(blendWeight);
        //secondActiveAction.interpolate(t * timeFactor[secondActiveIndex]);
        interpolate(secondActiveAction, secondActiveIndex, t);

        firstActiveAction.setCollectTransformDelegate(null);
        secondActiveAction.setCollectTransformDelegate(null);

        lastTime = t;
    }

	private void interpolate(BlendableAction action, int index, double time) {
		if (blendMode == BlendMode.Stretch) {
			// In stretch mode timeData represents time factor
			action.interpolate(time * timeData[index]);
		} else { // Loop mode
			double tpf = time - lastTime;
			timeData[index] += tpf;
			// In loop mode timeData represents time
			if (!action.interpolate(timeData[index])) {
				timeData[index] = 0;
			}
		}
	}

    protected Action[] getActions() {
        return actions;
    }

    public MyBlendSpace getBlendSpace() {
        return blendSpace;
    }

    protected void setFirstActiveIndex(int index) {
        this.firstActiveIndex = index;
    }

    protected void setSecondActiveIndex(int index) {
        this.secondActiveIndex = index;
    }

    @Override
    public Collection<HasLocalTransform> getTargets() {
        return targetMap.keySet();
    }

    @Override
    public void collectTransform(HasLocalTransform target, Transform t, float weight, BlendableAction source) {

        Transform tr = targetMap.get(target);
        if (weight == 1) {
            tr.set(t);
        } else if (weight > 0) {
            tr.interpolateTransforms(tr, t, weight);
        }

        if (source == actions[secondActiveIndex]) {
            collect(target, tr);
        }
    }

    private void collect(HasLocalTransform target, Transform tr) {
        if (collectTransformDelegate != null) {
            collectTransformDelegate.collectTransform(target, tr, this.getWeight(), this);
        } else {
            if (getTransitionWeight() == 1) {
                target.setLocalTransform(tr);
            } else {
                Transform trans = target.getLocalTransform();
                trans.interpolateTransforms(trans, tr, getTransitionWeight());
                target.setLocalTransform(trans);
            }
        }
    }

}

MyBlendSpace

package com.capdevon.anim.fsm;

import com.jme3.anim.tween.action.Action;

public class MyBlendSpace {

    private MyBlendAction action;
    private float value;
    final private float maxValue;
    final private float minValue;
    private float step;

    public MyBlendSpace(float minValue, float maxValue) {
        this.maxValue = maxValue;
        this.minValue = minValue;
    }

//    @Override
    public void setBlendAction(MyBlendAction action) {
        this.action = action;
        Action[] actions = action.getActions();
        step = (maxValue - minValue) / (actions.length - 1);
    }

//    @Override
    public float getWeight() {
        Action[] actions = action.getActions();
        float lowStep = minValue, highStep = minValue;
        int lowIndex = 0, highIndex = 0;
        for (int i = 0; i < actions.length && highStep < value; i++) {
            lowStep = highStep;
            lowIndex = i;
            highStep += step;
        }
        highIndex = lowIndex + 1;

        action.setFirstActiveIndex(lowIndex);
        action.setSecondActiveIndex(highIndex);

        if (highStep == lowStep) {
            return 0;
        }

        return (value - lowStep) / (highStep - lowStep);
    }

//    @Override
    public void setValue(float value) {
        this.value = value;
    }
}

Here are the new configuration parts of my test:

MyBlendSpace blendSpace = new MyBlendSpace(0, 1);
String[] clips = { "walking", "running" };
BlendableAction[] acts = new BlendableAction[clips.length];
for (int i = 0; i < acts.length; i++) {
	BlendableAction ba = (BlendableAction) animComposer.makeAction(clips[i]);
	acts[i] = ba;
}

MyBlendAction action = new MyBlendAction(blendSpace, acts);
animComposer.addAction("BlendTree", action);

And here the new parts of the player controller

public class PlayerMovementControl extends AbstractControl implements ActionListener {

	public float m_MoveSpeed = 4.5f;
	public float m_TurnSpeed = 10f;
	public float velocity = 0;
	public float acceleration = 0.4f;
	public float deceleration = 1f;

	private boolean _MoveForward, _MoveBackward, _TurnLeft, _TurnRight;

	...

	@Override
	public void onAction(String name, boolean isPressed, float tpf) {
		if (name.equals(InputMapping.MOVE_FORWARD)) {
			_MoveForward = isPressed;
		} else if (name.equals(InputMapping.MOVE_BACKWARD)) {
			_MoveBackward = isPressed;
		} else if (name.equals(InputMapping.MOVE_LEFT)) {
			_TurnLeft = isPressed;
		} else if (name.equals(InputMapping.MOVE_RIGHT)) {
			_TurnRight = isPressed;
		}
	}

	@Override
	public void controlUpdate(float tpf) {
		...

		if (isMoving) {
			velocity += acceleration * tpf;
		} else {
			velocity -= deceleration * tpf;
		}
		
		velocity = FastMath.clamp(velocity, 0, 1);
		
		MyBlendAction action = (MyBlendAction) animComposer.getAction("BlendTree");
		action.getBlendSpace().setValue(velocity );
	}

	...

}

But maybe there is still a problem with the execution of the two animations.
Sometimes the animation freezes or skips frames. It is not perfectly fluid. Am I forgetting something?

Could you paste your test here please?

Is your test and models on your github page so I can run it?

Yes, just a moment.

Edit:

/*
 * Copyright (c) 2017-2021 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 jme3test.model.anim;

import com.jme3.anim.*;
import com.jme3.anim.tween.action.BlendAction;
import com.jme3.anim.tween.action.LinearBlendSpace;
import com.jme3.anim.util.AnimMigrationUtils;
import com.jme3.app.ChaseCameraAppState;
import com.jme3.app.SimpleApplication;
import com.jme3.input.KeyInput;
import com.jme3.input.controls.ActionListener;
import com.jme3.input.controls.AnalogListener;
import com.jme3.input.controls.KeyTrigger;
import com.jme3.light.AmbientLight;
import com.jme3.light.DirectionalLight;
import com.jme3.math.*;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import com.jme3.scene.debug.custom.ArmatureDebugAppState;

import java.util.LinkedList;

/**
 * Created by Nehon on 18/12/2017.
 */
public class TestAnimMigration extends SimpleApplication {

    private ArmatureDebugAppState debugAppState;
    private AnimComposer composer;
    final private LinkedList<String> anims = new LinkedList<>();
    private boolean playAnim = false;
    private BlendAction action;
    private float blendValue = 1f;

    public static void main(String... argv) {
        TestAnimMigration app = new TestAnimMigration();
        app.start();
    }

    @Override
    public void simpleInitApp() {
        setTimer(new EraseTimer());
        cam.setFrustumPerspective(45f, (float) cam.getWidth() / cam.getHeight(), 0.1f, 100f);
        viewPort.setBackgroundColor(ColorRGBA.DarkGray);
        rootNode.addLight(new DirectionalLight(new Vector3f(-1, -1, -1).normalizeLocal()));
        rootNode.addLight(new AmbientLight(ColorRGBA.DarkGray));

        Spatial model = assetManager.loadModel("Models/Jaime/Jaime.j3o");
        // Spatial model = assetManager.loadModel("Models/Oto/Oto.mesh.xml").scale(0.2f).move(0, 1, 0);
        //Spatial model = assetManager.loadModel("Models/Sinbad/Sinbad.mesh.xml");
        //Spatial model = assetManager.loadModel("Models/Elephant/Elephant.mesh.xml").scale(0.02f);

        AnimMigrationUtils.migrate(model);

        rootNode.attachChild(model);


        debugAppState = new ArmatureDebugAppState();
        stateManager.attach(debugAppState);

        setupModel(model);

        flyCam.setEnabled(false);

        Node target = new Node("CamTarget");
        //target.setLocalTransform(model.getLocalTransform());
        target.move(0, 1, 0);
        ChaseCameraAppState chaseCam = new ChaseCameraAppState();
        chaseCam.setTarget(target);
        getStateManager().attach(chaseCam);
        chaseCam.setInvertHorizontalAxis(true);
        chaseCam.setInvertVerticalAxis(true);
        chaseCam.setZoomSpeed(0.5f);
        chaseCam.setMinVerticalRotation(-FastMath.HALF_PI);
        chaseCam.setRotationSpeed(3);
        chaseCam.setDefaultDistance(3);
        chaseCam.setMinDistance(0.01f);
        chaseCam.setZoomSpeed(0.01f);
        chaseCam.setDefaultVerticalRotation(0.3f);

        initInputs();
    }

    public void initInputs() {
        inputManager.addMapping("toggleAnim", new KeyTrigger(KeyInput.KEY_RETURN));

        inputManager.addListener(new ActionListener() {
            @Override
            public void onAction(String name, boolean isPressed, float tpf) {
                if (isPressed) {
                    playAnim = !playAnim;
                    if (playAnim) {
                        String anim = anims.poll();
                        anims.add(anim);
                        composer.setCurrentAction(anim);
                        System.err.println(anim);
                    } else {
                        composer.reset();
                    }
                }
            }
        }, "toggleAnim");
        inputManager.addMapping("nextAnim", new KeyTrigger(KeyInput.KEY_RIGHT));
        inputManager.addListener(new ActionListener() {
            @Override
            public void onAction(String name, boolean isPressed, float tpf) {
                if (isPressed && composer != null) {
                    String anim = anims.poll();
                    anims.add(anim);
                    composer.setCurrentAction(anim);
                    System.err.println(anim);
                }
            }
        }, "nextAnim");
        inputManager.addMapping("toggleArmature", new KeyTrigger(KeyInput.KEY_SPACE));
        inputManager.addListener(new ActionListener() {
            @Override
            public void onAction(String name, boolean isPressed, float tpf) {
                if (isPressed) {
                    debugAppState.setEnabled(!debugAppState.isEnabled());
                }
            }
        }, "toggleArmature");

        inputManager.addMapping("mask", new KeyTrigger(KeyInput.KEY_M));
        inputManager.addListener(new ActionListener() {
            @Override
            public void onAction(String name, boolean isPressed, float tpf) {
                if (isPressed) {
                    composer.setCurrentAction("Wave", "LeftArm");
                }
            }
        }, "mask");

        inputManager.addMapping("blendUp", new KeyTrigger(KeyInput.KEY_UP));
        inputManager.addMapping("blendDown", new KeyTrigger(KeyInput.KEY_DOWN));

        inputManager.addListener(new AnalogListener() {

            @Override
            public void onAnalog(String name, float value, float tpf) {
                if (name.equals("blendUp")) {
                    blendValue += value;
                    blendValue = FastMath.clamp(blendValue, 1, 4);
                    action.getBlendSpace().setValue(blendValue);
                    //action.setSpeed(blendValue);
                }
                if (name.equals("blendDown")) {
                    blendValue -= value;
                    blendValue = FastMath.clamp(blendValue, 1, 4);
                    action.getBlendSpace().setValue(blendValue);
                    //action.setSpeed(blendValue);
                }
                //System.err.println(blendValue);
            }
        }, "blendUp", "blendDown");
    }

    private void setupModel(Spatial model) {
        if (composer != null) {
            return;
        }
        composer = model.getControl(AnimComposer.class);
        if (composer != null) {

            SkinningControl sc = model.getControl(SkinningControl.class);
            debugAppState.addArmatureFrom(sc);

            anims.clear();
            for (String name : composer.getAnimClipsNames()) {
                anims.add(name);
            }
            composer.actionSequence("Sequence1",
                    composer.makeAction("Walk"),
                    composer.makeAction("Run"),
                    composer.makeAction("Jumping")).setSpeed(1);

            composer.actionSequence("Sequence2",
                    composer.makeAction("Walk"),
                    composer.makeAction("Run"),
                    composer.makeAction("Jumping")).setSpeed(-1);

            action = composer.actionBlended("Blend", new LinearBlendSpace(1, 4),
                    "Walk", "Run");
            action.setTransitionLength(0);

            action.getBlendSpace().setValue(1);

            composer.action("Walk").setSpeed(-1);

            composer.makeLayer("LeftArm", ArmatureMask.createMask(sc.getArmature(), "shoulder.L"));

            anims.addFirst("Blend");
            anims.addFirst("Sequence2");
            anims.addFirst("Sequence1");

            if (anims.isEmpty()) {
                return;
            }
            if (playAnim) {
                String anim = anims.poll();
                anims.add(anim);
                composer.setCurrentAction(anim);
                System.err.println(anim + " , lenght=" + composer.getAction(anim).getLength());
            }

        } else {
            if (model instanceof Node) {
                Node n = (Node) model;
                for (Spatial child : n.getChildren()) {
                    setupModel(child);
                }
            }
        }

    }
}
1 Like

Not yet, give me some time so I can create one. I have already uploaded the model. You can find it here. https://github.com/capdevon/jme-capdevon-examples/tree/main/src/main/resources/Models/Rifle

The name of the animations are these:


	private interface AnimDefs {

		final String MODEL = "Models/Rifle/rifle.glb";
		final String RifleIdle = "RifleIdle";
		final String RifleWalk = "RifleWalk";
		final String RifleRun = "RifleRun";
		final String WalkWithRifle = "WalkWithRifle";
		final String ThrowGrenade = "ThrowGrenade";
		final String Reloading = "Reloading";
		final String RifleAimingIdle = "RifleAimingIdle";
		final String FiringRifleSingle = "FiringRifleSingle";
		final String FiringRifleAuto = "FiringRifleAuto";
		final String DeathFromRight = "DeathFromRight";
		final String DeathFromHeadshot = "DeathFromHeadshot";
		final String TPose = "TPose";

	}

ok, maybe it depends on the animations and not on the code. But I’m not sure.

Here is the test case:
https://github.com/capdevon/jme-capdevon-examples/blob/main/src/main/java/com/capdevon/demo/Test_BlendActionLoop.java

1 Like

Yes, I noticed that but not sure why that is happening. It happens with Jame animation as well. It will be less noticeable if the transition is fast. (for example with minThreshold = 0 and maxThreshold = 1 in BlendSpace in the above test)

But another option is to use the default Stretch mode and just uncomment these lines and it will fix the slow-motion effect also animation blending will look fluid :slightly_smiling_face:

and

use something like action.setSpeed(1 + blendValue / 4); or so.

2 Likes

Ok, I probably solved it using the normal BlendAction class. I clean up the code and publish the new BlendTree feature on AnimatorStateMachine on github. Thanks for the help @Ali_RS , it was an interesting exchange of ideas. I hope it will be useful to the community and inspire new features. :grinning:

3 Likes