Questions about using AnimComposer

Usually there are multiple AnimComposer animations in a single J3O file, and usually I just need to use

control =model1.getChild(0).getControl(AnimComposer.class);
control.setCurrentAction("Walk");
control.setGlobalSpeed(0.5f);

I can play it.
But now I have a problem
I now have many J3o files with animations, how do I load the animations in multiple J3o files and play the animations correctly.

image
I tried my ideas but they were wrong

Can you right click your Stand.j3o file and then 'edit in scenecomposer’ and then show a screenshot of what your AnimComposer looks like?



Maybe there is something wrong with me :persevere:, I need to get the animation of both files to load and play. I can load one file correctly but not both files at the same time.

There isn’t anything wrong with you haha- Yes so your walk just has an animation and then your stand has those two animation clips, both prolly the same one?

Let me clarify, however

  1. Are you wanting to have one file with multiple animations?
  2. Or do you want multiple files and be able to play an animation off each one?

If option two can you give the game context or gaming goal that you are after? for example are you trying to animate two different npcs?

1 Like

If I am understanding correctly you want to load animations from multiple files and add them to a single model?

For that, you need to load each animation file and get the AnimClip from it and retarget the clip to the model you want to play the animation on it.

See this topic:

1 Like

https://hub.jmonkeyengine.org/t/how-to-reuse-animations/44796
This is what I want :smile:

Thank you very much for your reply,
Yes, these animations all use the same skeleton

1 Like

You’re welcome!

By the way, in the code snippet, I posted on that topic, the “convert to in-place” part is dirty code. Better to use the function I posted here which is also integrated into the Wes library.

Edit:

Here it is updated to use the new “in-place animation” method:

Note that you must rename “root.x” to the root bone name on your model. In my case, I am using Auto Rig Pro addon to rig all my characters and the root bone name is “root.x” that is why I have hardcoded it in code.


import com.jme3.anim.*;
import com.jme3.math.Vector3f;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import com.jme3.util.SafeArrayList;

/**
 * @author Ali-RS
 */
public class AnimUtils {

    public static SkinningControl findSkinningControl(Spatial model) {
        Spatial s = getAnimRoot(model);
        SkinningControl ac = s == null ? null : s.getControl(SkinningControl.class);
        return ac;
    }

    public static AnimComposer findAnimComposer(Spatial model) {
        Spatial s = getAnimRoot(model);
        AnimComposer ac = s == null ? null : s.getControl(AnimComposer.class);
        return ac;
    }

    protected static Spatial getAnimRoot(Spatial model) {
        // Find spatial with the composer
        // For the moment, we'll guess
//    Spatial animRoot = ((Node) model).getChild("root");
//    if (animRoot == null) {
//        // Have to find it the hard way
//        animRoot = findAnimRoot(model);
//    }
        return findAnimRoot(model);
    }

    protected static Spatial findAnimRoot(Spatial s) {
        if (s.getControl(AnimComposer.class) != null) {
            return s;
        }
        if (s instanceof Node) {
            for (Spatial child : ((Node) s).getChildren()) {
                Spatial result = findAnimRoot(child);
                if (result != null) {
                    return result;
                }
            }
        }
        return null;
    }

    public static AnimClip retargetClip(AnimClip sourceClip, Spatial target, boolean inPlace, String newClipName) {
        Spatial animRoot = getAnimRoot(target);
        if (animRoot == null) {
            System.err.println("Anim root is null!");
            return null;
        }

        SkinningControl sc = animRoot.getControl(SkinningControl.class);
        if (target.getUserData("hasInitialPose") == null) {
            sc.getArmature().applyBindPose();
            sc.getArmature().saveInitialPose();
            target.setUserData("hasInitialPose", Boolean.TRUE);
        }

        SafeArrayList<AnimTrack> tracks = new SafeArrayList<>(AnimTrack.class);
        for (AnimTrack animTrack : sourceClip.getTracks()) {
            if (animTrack instanceof TransformTrack) {
                TransformTrack sourceTrack = (TransformTrack) animTrack;
                Joint sourceJoint = (Joint) sourceTrack.getTarget();
                // I am using a standard humanoid rig for all my characters so joint
                // names are the same on all rigs
                Joint targetJoint = sc.getArmature().getJoint(sourceJoint.getName());
                if (targetJoint == null) {
                    System.err.println( "Joint with name " + sourceJoint.getName() + " not fount on target");
                    continue;
                }

                // Only Rotations are cloned
                TransformTrack targetTrack = new TransformTrack(targetJoint, sourceTrack.getTimes(), null, sourceTrack.getRotations(), null);//sourceTrack.jmeClone();
                targetTrack.setTarget(targetJoint);

                if (targetJoint.getName().equals("root.x")) {
                    System.out.println("Transferring root motion...");
                    // Modify translations to fit in target rig
                    if (sourceTrack.getTranslations() != null) {
                        Vector3f[] translations = new Vector3f[sourceTrack.getTimes().length];
                        //boolean convertToInPlace = false;
                       /*Vector3f start = new Vector3f(sourceTrack.getTranslations()[0]);
                        Vector3f end = new Vector3f(sourceTrack.getTranslations()[sourceTrack.getTranslations().length - 1]);
                        start.y = 0;
                        end.y = 0;
                        if (start.distance(end) > 0.30f) {
                            convertToInPlace = true;
                            System.out.println("Converting to InPlace:" + sourceClip.getName());
                        }*/

                        float scale = targetJoint.getInitialTransform().getTranslation().getY() / sourceJoint.getInitialTransform().getTranslation().getY();
                        System.out.println("Scale factor= " + scale);
                        for (int i = 0; i < sourceTrack.getTranslations().length; i++) {
                            // delta translation = track translation - bind translation
                            Vector3f deltaTranslation = sourceTrack.getTranslations()[i].subtract(sourceJoint.getLocalTranslation());
                            deltaTranslation.multLocal(scale);
                            /*if (convertToInPlace) {
                                deltaTranslation.x = 0f;
                                deltaTranslation.z = 0f;
                            }*/
                            Vector3f targetTranslation = new Vector3f(targetJoint.getInitialTransform().getTranslation());
                            targetTranslation.addLocal(deltaTranslation);
                            translations[i] = targetTranslation;
                        }
                        if (inPlace) {
                            convertToInPlace(translations);
                        }
                        targetTrack.setKeyframesTranslation(translations);
                    }
                }
                tracks.add(targetTrack);
            }
        }

        AnimClip targetClip = new AnimClip(newClipName);
        targetClip.setTracks(tracks.getArray());
        return targetClip;
    }

    private static void convertToInPlace(Vector3f[] translations) {
        Vector3f start = translations[0];
        Vector3f end = translations[translations.length - 1];

        Vector3f delta = end.subtract(start);
        Vector3f factor = delta.divide(translations.length - 1);
        Vector3f vTemp = new Vector3f();
        for (int index = 0; index < translations.length; index++) {
            Vector3f translation = translations[index];
            vTemp.set(factor).multLocal(index);
            translation.subtractLocal(vTemp);
        }
    }
}

and to use it:

AnimClip newClip = AnimUtils.retargetClip(clip, targetModel, inPlace, newClipName);
AnimComposer ac = AnimUtils.findAnimComposer(targetModel);
ac.addAnimClip(newClip);
1 Like
AnimClip newClip = AnimUtils.retargetClip(clip, targetModel, inPlace, newClipName);

clip
What value should Clip be passed in?
Or where the AnimClip came from?

From your animation files. Walk.j3o, Stand.j3o,…

You can get it using “AnimComposer.getAnimClip(clipName)”

Thank you for reminding me that I successfully loaded the animation of two different files :laughing: :laughing:

 AnimClip newClip = AnimUtils.retargetClip(control1.getAnimClip("Walk"), model, false, "Walk");
control.addAnimClip(newClip);

I added the “Walk” animation to the “Stand” and made the model very small and I tried to scale it and it didn’t show up properly

The “Stand” animation will be normal, so if I load “Stand” first and then play “Walk” then it will be normal
https://youtu.be/srr-pHL_xbQ
As shown in the video, the “Walk” model I loaded first appeared very small

Hmm… not sure what is going wrong, perhaps an issue with my code snippet that does not cover every case.

Please try to use the Wes library created by Stephen.


public static AnimClip retargetAnimation(AnimClip sourceClip, Armature sourceArmature, Armature targetArmature,
        SkeletonMapping map, String clipName) 

Using Wes for the first time
Could you give me a hint
SkeletonMapping mapWhat value should map be passed in?
Or where the SkeletonMapping came from?
Sorry to trouble you :face_exhaling: :face_exhaling: :persevere:
Reading the comments in the code I know that I should need the new SkeletonMapping but I don't know how I should create this object No documentation found in GitHub :persevere:

1 Like

No problem at all :slightly_smiling_face:

The code for SkeletonMapping is here:

You need it to tell which bone from the source armature should be mapped to which bone on the target armature by specifying the joint names. In your case because they are using the same skeleton then the source and target joint names are the same so you can do something like this:

targetArmature.getJointList().forEach(joint -> 
           skeletonMapping.addMapping(new BoneMapping(joint.getName(), joint.getName()))
);
1 Like

Problem description
Every time “Wlak” is played, the model shrinks

The difference is that even playing “Stand” doesn’t make the “Walk” size normal compared to the previous one

If you use the retargetClip in AnimUtils then the size of “Walk” will become normal after the target animation “Stand” is played

It makes me feel confused

This seems like a lot of potential issues, Icy. Why not just have all the animations that you want on the model that you want?

I think it would be easier to have a bunch of different models than one set of animations that you try to use for all your models if that makes sense

1 Like

Since it is an animation file produced by several different action designers, I need to convert these animation files into j3o for splicing

1 Like

oooo designers, gotchya

When donuts and pspeed helped me equip a weapon donuts shared that when you attach a weapon to a hand it almost always shrinks unless everything has a local scale of 1. Might not be helpful at at but perhaps you could look into the scales of all these different pieces and for example try increasing the scale of the Walk file. Or what did you do to try to increase its scale?