Monkanim: new animation system in the works

Transition already works
the problem is more having different parts of the body play different animation AND blend between them.
Classic use case would be: a walk animation and a wave animation. you want to mix them so that the character waves while walking, but you just want to apply the wave animation from the shoulder to the hand.
I have a working solution for this, but it doesn’t support blending, so it’s unnatural, so it’s unusable.
But if you want to blend between several anims using the whole skeleton it works pretty well.

4 Likes

Hi
I switched my game animation system to monkanim but I am getting weired blending behavior when blending from walk animation to run.

Is such behavior usual when blending ? Any way to smooth it ?

1 Like

Well this is still a work in progress, and it’s far from a usable state.
Not sure what’s going on in the video, but yeah there might be some weird issues

playing walk and run animation separately works OK.
But in “walk_run” animation character stucks when blending.

Yes I mean, I understood that, but I don’t know what the issue can be.
Maybe the original anims are really not in sync and the blending fails.

How many frame is there for both anims?

30 frames for “walk” and 22 for “run”.

mhh then maybe that’ the issue, in my example they both have 40 frames… I guess there could be some interpolation when blending…

Could you test to scale the anims in blender so that they have the same amount of frames?

Tested it and it fixed the problem. :grinning:
@nehon thanks so so much for your help. :slight_smile:

Cool.
Well it’s an issue that should be handled by the system though. I have to get back to this …

1 Like

Some bug report for Monkanim

No animation plays when adding more than one model (same model) to scene.

(I am using jme master branch with this commit applied)

but you will probably get something like this if you are using 3.1-beta2 or lower.

Edit:

Note, When importing different models animation works OK.

@nehon can you please test it to see if it happens for you, or I am doing some thing wrong ?

Here is the test

/**
 * Created by Nehon on 08/07/2016.
 */
public class AnimAppState extends BaseAppState {

    AnimationManager manager;
    AnimationManager manager2;
    private String currentState = "";
    private LinearBlendSpace blendSpace = new LinearBlendSpace();


    @Override
    protected void initialize(Application app) {
        Node s = (Node) app.getAssetManager().loadModel("Models/puppet.xbuf");
        Node s2 = (Node) app.getAssetManager().loadModel("Models/puppet.xbuf");//Models/Models/Characters/Doha/Doha.xbuf
        s2.setLocalTranslation(1, 0, 0);
        Node rootNode = ((SimpleApplication) app).getRootNode();
        rootNode.attachChild(s);
        rootNode.attachChild(s2);
        rootNode.addLight(new DirectionalLight(new Vector3f(-1, -1, -1).normalizeLocal()));
        rootNode.addLight(new DirectionalLight(new Vector3f(1, -1, 1).normalizeLocal(),new ColorRGBA(0.5f,0.5f,0.5f,1.0f)));

        dumpSceneGraph(rootNode, "");

        Spatial rig = s.getChild("rig");
        Spatial rig2 = s2.getChild("rig");//Armature
        AnimControl control = rig.getControl(AnimControl.class);
        AnimControl control2 = rig2.getControl(AnimControl.class);
//        AnimChannel channel = control.createChannel();
//        channel.setAnim("walk");
//        channel.setSpeed(1);
//        channel.setLoopMode(LoopMode.Loop);

        //create an AnnimationManager from an AnimController. This also createa a sequence for each animation
        manager = AnimMigrationUtil.fromAnimControl(control);
        manager2 = AnimMigrationUtil.fromAnimControl(control2);

        //removing old stuff and adding new stuff
        SkeletonControl skelControl = rig.getControl(SkeletonControl.class);
        SkeletonControl skelControl2 = rig2.getControl(SkeletonControl.class);

        Skeleton skeleton = skelControl.getSkeleton();
        System.err.println(skeleton.getBone(0));
        rig.removeControl(AnimControl.class);
        rig.removeControl(SkeletonControl.class);
        
        rig2.removeControl(AnimControl.class);
        rig2.removeControl(SkeletonControl.class);

        //Bone b = skelControl.getSkeleton().getBone("Foot_Roll.R");


        rig.addControl(manager);
        rig.addControl(skelControl);

        rig2.addControl(manager2);
        rig2.addControl(skelControl2);

        //Creating a blending sequence between walk, jog and run
        AnimationSequence sequence = manager.createAnimationSequence("walk_run", "walk", "run");
        sequence.setBlendSpace(blendSpace);
        
        AnimationSequence sequence2 = manager2.createAnimationSequence("walk_run", "walk", "run");
        //sequence.setBlendSpace(blendSpace);

        //Creating a blending sequence between several sequences
//        AnimationSequence seq = manager.createAnimationSequence("walk_jog", "walk", "jog");
//        ((LinearBlendSpace) seq.getBlendSpace()).setValue(0.5f);
//        sequence = manager.createAnimationSequence("walk_jog_nestedRun", "walk_jog", "run");
//        sequence.setBlendSpace(blendSpace);

        //TODO sequence and states could be the same thing actually... merging them would simplify the API without too much clutter of code.

        //state machine
        manager.createStateForSequence("idle");
        manager.createStateForSequence("walk");
        manager2.createStateForSequence("idle");
        manager2.createStateForSequence("walk");
        //manager.createStateForSequence("jog");
        //manager.createStateForSequence("kick");
        manager.createStateForSequence("run");
        manager2.createStateForSequence("run");
        //manager.createStateForSequence("wave").setMask(SkeletonMask.fromBone(skeleton, "shoulder.L"));
        manager.createStateForSequence("walk_run");
        manager2.createStateForSequence("walk_run");
        //manager.createStateForSequence("walk_jog");
        //manager.createStateForSequence("walk_jog_nestedRun");

        manager.startWith("idle");
        manager2.startWith("idle");

//        //Non functional and non java 8 API..
//        AnimState state = manager.getState(ANY_STATE);
//        if(state != null) {
//            AnimState targetState = manager.getState("idle");
//            if(targetState != null) {
//                InterruptingTransition it = new InterruptingTransition(targetState);
//                it.setTrigger(new TransitionTrigger() {
//                    @Override
//                    public boolean evaluate() {
//                        return currentState.equals("idle");
//                    }
//                });
//                state.addTransition(it);
//            }
//        }

        //Functional with java 8.
        manager.findState(ANY_STATE).interruptTo("idle").when(() -> currentState.equals("idle"));
        manager.findState(ANY_STATE).interruptTo("walk").when(() -> currentState.equals("walk"));
        manager2.findState(ANY_STATE).interruptTo("idle").when(() -> currentState.equals("idle"));
        manager2.findState(ANY_STATE).interruptTo("walk").when(() -> currentState.equals("walk"));

        //manager.findState(ANY_STATE).interruptTo("jog").when(() -> currentState.equals("jog"));
        //manager.findState(ANY_STATE).interruptTo("kick").when(() -> currentState.equals("kick"));
        manager.findState(ANY_STATE).interruptTo("run").when(() -> currentState.equals("run"));
        manager2.findState(ANY_STATE).interruptTo("run").when(() -> currentState.equals("run"));
       // manager.findState(ANY_STATE).interruptTo("wave").when(() -> currentState.equals("wave"));
        //manager.findState("wave").transitionTo("idle");
        manager.findState(ANY_STATE).interruptTo("walk_run").when(() -> currentState.equals("walk_run"));
        manager2.findState(ANY_STATE).interruptTo("walk_run").when(() -> currentState.equals("walk_run"));
        //manager.findState(ANY_STATE).interruptTo("walk_jog").when(() -> currentState.equals("walk_jog"));
        //manager.findState(ANY_STATE).interruptTo("walk_jog_nestedRun").when(() -> currentState.equals("walk_jog_nestedRun"));

        //Chain
//        manager.findState(ANY_STATE).interruptTo("walk").when(() -> currentState.equals("anim_chain"));
//        manager.findState("walk").transitionTo("jog").when(() -> currentState.equals("anim_chain"));
//        manager.findState("jog").transitionTo("run").when(() -> currentState.equals("anim_chain"));
//        manager.findState("run").transitionTo("kick").when(() -> currentState.equals("anim_chain")).in(0.5f);
//        manager.findState("kick").transitionTo("idle").when(() -> currentState.equals("anim_chain"));


        //Chain
//        manager.findState(ANY_STATE).interruptTo("kick").when(() -> currentState.equals("anim_chain"));
//        manager.findState("kick").transitionTo("idle").when(() -> currentState.equals("anim_chain"));
//        manager.findState("idle").transitionTo("kick").when(() -> currentState.equals("anim_chain"));


//
//        for (int i = 0; i < 50; i++) {
//            Spatial sp = rig.clone();
//            rootNode.attachChild(sp);
//            sp.move(i,0,0);
//        }


    }

    public void setCurrentState(String currentState) {
        this.currentState = currentState;
    }

    private void dumpSceneGraph(Spatial n, String indent) {
        System.err.println(indent + n.toString());
        for (int i = 0; i < n.getNumControls(); i++) {
            System.err.println(indent + "  =>" + n.getControl(i).toString());
        }
        if (n instanceof Node) {
            Node node = (Node) n;
            for (Spatial spatial : node.getChildren()) {
                dumpSceneGraph(spatial, indent + "    ");
            }
        }
    }

    public void setBlendValue(float value) {
        blendSpace.setValue(value);
    }

    private void dumpSkeleton(Skeleton skeleton) {

        for (int i = 0; i < skeleton.getBoneCount(); i++) {
            System.err.println(skeleton.getBone(i));
        }
    }

    public AnimationManager getManager() {
        return manager;
    }

    @Override
    protected void cleanup(Application app) {

    }

    @Override
    protected void onEnable() {

    }

    @Override
    protected void onDisable() {

    }
}

Edit2:
I could solve the issue. It was a cloning problem.
I add this field clone.metaData = new AnimationMetaData(); to jmeClone() method in AnimationManager class.

1 Like

Don’t really have time right now… I’ll test that later.
Really this is still a very early wip, using it will get you in trouble as there are chances the API change a lot.

Okay, no problem.
Will change to default animation system for now.
I will be glad to help with testing and probably bug fixing your API at anytime.

Thanks

1 Like

Hey got a few questions about Monkanim,

Will there be any form of nodal based overlay similar to Unity Mechanim?
I understand that this is still very much a work in progress and the API can / will change allot.

Thank you for your time,
HeadClot

This is really what I’m waiting for

Since nehon already made the Node based Material System, it could be that he might add it.
Depending on the complexity and the way you can store these transitions, the sdk could also get such a functionality (actually the code for a node based mechanism is already there).

The problem is the state logic and such, though, I don’t know if this is really something you want to store in your .j3o. Maybe some Code Generation approach would be good?

But the future will see, I guess it’ll still be some months away.

I am using Freemind to script the animation transitions.

and using this tool GitHub - klaus7/freemind2gdxai: Converts freemind files to the gdx-ai format. (LibGDX-AI, see https://github.com/libgdx/gdx-ai and https://github.com/libgdx/gdx-ai/wiki/Behavior-Trees for an example of the file format) to convert it to Gdx-AI behavior tree and run the tasks.

May not be a proper solution but I am already using Gdx-AI btree for my game AI and was excited to use it also for scripting animation transitions.

7 Likes

Soooo… Back to this.
First I removed the Sequence class, and merged it with AnimState. It’s simpler.

I also made a major breakthrough this week end and I finally got rid of this thing that was killing my motivation for this project.
I initially thought to have some mask system where you could play an animation but only for a sub set of bones (like we have channels), but since the beginning it felt like trying to fit a square peg in a round hole.
So I’ve changed route and took the problem the other way around : Animation layers. Pretty much what there is in Mecanim in the end.
So a layer is just this another overlay of animation with a subset of bones and only those bones will be affected by animations played on this layer. The nice part is that the other bones are still playing the animation of lower levels.

In practice it’s like the channels but more flexible and with less work to do to get to the same result.
Here is a video

As you can see I have a wave state that uses the entire skeleton, but, I’ve setup a waveLayer state that is set on a layer that only affects the left arm.

With this I can set the walk animation for exaple and just run the waveLayer animation over it and only the left arm is affected.
Here is the code of the entire setup of animations in this video:

        //creating a layer that affects the left arm.
        //Note that there is a default layer that affects the entire skeleton
        manager.addLayer("wave").withMask(SkeletonMask.fromBone(skeleton, "shoulder.L"));

        //state machine
        //creating a state for each animation (optional of course but for the demo 
        manager.createState("idle").forAnims("idle");
        manager.createState("walk").forAnims("walk");
        manager.createState("jog").forAnims("jog");
        manager.createState("kick").forAnims("kick");
        manager.createState("run").forAnims("run");
        manager.createState("wave").forAnims("wave");

        //creating a wave anim on the wave layer
        manager.createState("waveLayer").forAnims("wave").onLayer("wave");
        
        //creating a blended anim composed of the walk, jog and run anims. (will blend according to speed
        manager.createState("walk_jog_run").forAnims("walk", "jog", "run").withBlendSpace(blendSpace);
        //creating a blended anim composed of walk and jog anims, but with a fied blend value of 0.5
        ((LinearBlendSpace) (manager.createState("walk_jog").forAnims("walk", "jog").getBlendSpace())).setValue(0.5f);
        //Mixed blending between a previously defined state and an animation clip.
        manager.createState("walk_jog_nestedRun").forAnims("walk_jog", "run").withBlendSpace(blendSpace);

        //bootstrap state
        manager.startWith("idle");

        //defining transitions and events
        //Any state will intrrupt to the idel state when the currentState local variable is "idle". Note that the condition can be enything
        manager.findState(ANY_STATE).interruptTo("idle").when(() -> currentState.equals("idle"));
        manager.findState(ANY_STATE).interruptTo("walk").when(() -> currentState.equals("walk"));
        manager.findState(ANY_STATE).interruptTo("jog").when(() -> currentState.equals("jog"));
        manager.findState(ANY_STATE).interruptTo("kick").when(() -> currentState.equals("kick"));
        manager.findState(ANY_STATE).interruptTo("run").when(() -> currentState.equals("run"));
        manager.findState(ANY_STATE).interruptTo("wave").when(() -> currentState.equals("wave"));
        manager.findState(ANY_STATE).interruptTo("walk_jog_run").when(() -> currentState.equals("walk_jog_run"));
        manager.findState(ANY_STATE).interruptTo("walk_jog").when(() -> currentState.equals("walk_jog"));
        manager.findState(ANY_STATE).interruptTo("walk_jog_nestedRun").when(() -> currentState.equals("walk_jog_nestedRun"));
        
        //Special case, when the wave state ends, it will return to "anystate", in that case the state from which wave was triggered 
        //Foexample if you go from walk to wave, when wave ends, it will transition bac to walk.
        manager.findState("wave").transitionTo(ANY_STATE);
        
        manager.findState(ANY_STATE).interruptTo("waveLayer").when(() -> currentState.equals("waveLayer"));
        //Here again a special case if a layered state transition back to ANY_STATE, if will just fade out to the previous layer.        
        manager.findState("waveLayer").transitionTo(ANY_STATE);

I like this functional style (if you hate it it will also be available as plain old java 7 style).
I’d like to be able to script all this in a groovy file. It would make the “code and save the state machine” pretty easy, in a portable/readable way.

EDIT : forgot to say @Ali_RS since you are the only user I’m aware of, there has been a couple of changes in the API. please feel free to ping me if you want to migrate, but I also tagged the version you use for easy git checkout.

13 Likes

I am curious if this will also playing the animation backwards when running backwards?

No it doesn’t, but I guess it can be added

Ok, thx for the info. I think I will check it out and see what needs to be done to change my locomotion stuff to be based on this;)