Class suggestion for new AnimSystem: BoneChannel

In this case, I would like for the joint to play the most recently played animation. For example, I have a rootMask that will often be playing the “walk” animation, and while walking, the upperBodyMask can play the “swing” animation, but currently I don’t think it works unless I stop the “walk” animation on the entire rootMask first since the rootMask is also affecting the joints in the upperBodyMask.

Hmm i must have been unaware of this. Can you point me to the relevent code or example? Or are you referring to the PhysicsLinks and BoneLink? In which case I’m not sure if I’m using them to their fullest potential. I just get the PhysicsLink or BoneLink associated with the armature mask immediately prior to setting dynamic mode.

Essentially, I am trying to organize my project in a way that I can do something like this within my class that manages the current animation state, to easily toggle a set of bones between running animations and operating in ragdoll/dynamic mode.

AnimationMask upperBodyMask;
AnimationMask legsMask;
AnimationMask rootMask;

public void upperBoyFlinch(){
     upperBodyAnimTimer  = 1.0f; //set timer so upper body won't run normal animation cycle while flinch is happening

    bonesList = getListOfBonesForMask(upperBodyMask);
    for(Joing joint : bonesList){
         selectedDynamicAnimControl.findBoneLink(joint.getName()).setDynamic(flinchVec);
    
    }

}

public void update(){
    if( upperBodyUniqueAnimTimer < 0){
           //play default walk animation if its not already playing
          animComposer.setCurrentAction(defaultWalkAnim, "upperBody");
     } else{
         upperBodyUniqueAnimTimer  -= tpf;
    }


}

I think it should work now if I use the code you suggested to get the list of joints in a getListOfBonesForMask() method. But this is the way my anim state manager is set up from using the old animation system with animChannels, where I had a unique timer for each channel to allow that channel to do non-default animation cycle things temporarily, and then when the timer is up the update loop takes care of resuming the default animation cycle on that layer

The difficulty with using animation masks in DynamicAnimControl is that bone links (in DAC) usually don’t correspond 1:1 with armature joints.

Can you point me to the relevant code or example?

The “gold standard” demo app for DynamicAnimControl is TestDac:

It’s complex, but that’s because it tries to demonstrate a LOT of features.

to easily toggle a set of bones between running animations and operating in ragdoll/dynamic mode

Sounds like a collection of bone links that you iterate over, plus maybe a boolean for the ragdoll’s root (TorsoLink):

if (includeRoot) {
    torsoLink.setDynamic(gravityVector);
}
for (BoneLink boneLink : boneLinks) {
    boneLink.setDynamic(gravityVector, false, false, false);
}
//...
if (includeRoot) {
    torsoLink.blendToKinematicMode(KinematicSubmode.animated, blendInterval, finalModelTransform);
}
for (BoneLink boneLink : boneLinks) {
    boneLink.blendToKinematicMode(KinematicSubmode.animated, blendInterval);
}
1 Like

Should we add this method to ArmatureMask?


    public List<Joint> getAffectedJoints(Armature armature) {
        return armature.getJointList()
                .stream()
                .filter(this::contains)
                .collect(Collectors.toList());
    }
3 Likes

Personally, I’ve never loved the current ArmatureMask class. Unfortunately this is a problem that we have inherited from previous engine versions.

  • Method signature is not homogeneous.
  • There are methods that return an ‘ArmatureMask’ and others void.
  • There is a method called addBones, when the whole API refers to Joint.
  • You have to pass the Armature as a parameter every time, which seems repetitive to me.

The only good thing is that the new animation system refers to the AnimationMask interface. So everyone is free to write their own implementation. IMO it would be better to deprecate the current one in favor of a better one in the next releases. If it can be useful as a starting point for an interesting discussion, here is my version with the addition of the ‘getAffectedJoints’ method suggested by @Ali_RS and @sgold :

package com.capdevon.anim;

import java.util.ArrayList;
import java.util.BitSet;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

import com.jme3.anim.AnimationMask;
import com.jme3.anim.Armature;
import com.jme3.anim.Joint;

/**
 * An AnimationMask to select joints from a single Armature.
 * @author capdevon
 */
public class AnimMaskBuilder implements AnimationMask {

    private static final Logger logger = Logger.getLogger(AnimMaskBuilder.class.getName());

    private final BitSet affectedJoints;
    private final Armature armature;

    /**
     * Instantiate a mask that affects no joints.
     *
     * @param armature
     */
    public AnimMaskBuilder(Armature armature) {
        this.armature = armature;
        this.affectedJoints = new BitSet(armature.getJointCount());
        logger.log(Level.INFO, "Joint count: {0}", armature.getJointCount());
    }

    /**
     * Add all the bones of the model's armature to be influenced by this
     * animation mask.
     *
     * @return AnimMaskBuilder
     */
    public AnimMaskBuilder addAllJoints() {
        int numJoints = armature.getJointCount();
        affectedJoints.set(0, numJoints);
        return this;
    }

    /**
     * Add joints to be influenced by this animation mask.
     *
     * @param jointNames
     * @return AnimMaskBuilder
     */
    public AnimMaskBuilder addJoints(String...jointNames) {
        for (String jointName: jointNames) {
            Joint joint = findJoint(jointName);
            affectedJoints.set(joint.getId());
        }
        return this;
    }

    private Joint findJoint(String jointName) {
        Joint joint = armature.getJoint(jointName);
        if (joint == null) {
            throw new IllegalArgumentException("Cannot find joint " + jointName);
        }
        return joint;
    }

    /**
     * Add a joint and all its sub armature joints to be influenced by this
     * animation mask.
     *
     * @param jointName the starting point (may be null, unaffected)
     * @return AnimMaskBuilder
     */
    public AnimMaskBuilder addFromJoint(String jointName) {
        Joint joint = findJoint(jointName);
        addFromJoint(joint);
        return this;
    }

    private void addFromJoint(Joint joint) {
        affectedJoints.set(joint.getId());
        for (Joint j: joint.getChildren()) {
            addFromJoint(j);
        }
    }

    /**
     * Remove a joint and all its sub armature joints to be influenced by this
     * animation mask.
     *
     * @param jointName the starting point (may be null, unaffected)
     * @return AnimMaskBuilder
     */
    public AnimMaskBuilder removeFromJoint(String jointName) {
        Joint joint = findJoint(jointName);
        removeFromJoint(joint);
        return this;
    }

    private void removeFromJoint(Joint joint) {
        affectedJoints.clear(joint.getId());
        for (Joint j: joint.getChildren()) {
            removeFromJoint(j);
        }
    }

    /**
     * Add the specified Joint and all its ancestors.
     *
     * @param jointName the starting point (may be null, unaffected)
     * @return AnimMaskBuilder
     */
    public AnimMaskBuilder addAncestors(String jointName) {
        Joint joint = findJoint(jointName);
        addAncestors(joint);
        return this;
    }

    private void addAncestors(Joint start) {
        for (Joint joint = start; joint != null; joint = joint.getParent()) {
            affectedJoints.set(joint.getId());
        }
    }

    /**
     * Remove the specified Joint and all its ancestors.
     *
     * @param jointName the starting point (may be null, unaffected)
     * @return AnimMaskBuilder
     */
    public AnimMaskBuilder removeAncestors(String jointName) {
        Joint joint = findJoint(jointName);
        removeAncestors(joint);
        return this;
    }

    private void removeAncestors(Joint start) {
        for (Joint joint = start; joint != null; joint = joint.getParent()) {
            affectedJoints.clear(joint.getId());
        }
    }

    /**
     * Remove the named joints.
     *
     * @param jointNames the names of the joints to be removed
     * @return AnimMaskBuilder
     */
    public AnimMaskBuilder removeJoints(String...jointNames) {
        for (String jointName: jointNames) {
            Joint joint = findJoint(jointName);
            affectedJoints.clear(joint.getId());
        }

        return this;
    }
    
    /**
     * Get the list of joints affected by this animation mask.
     *
     * @return
     */
    public List<Joint> getAffectedJoints() {
        List<Joint> lst = new ArrayList<>();
        for (Joint joint : armature.getJointList()) {
            if (contains(joint)) {
                lst.add(joint);
            }
        }
        return lst;
    }

    @Override
    public boolean contains(Object target) {
        Joint joint = (Joint) target;
        return affectedJoints.get(joint.getId());
    }

}

Here is a use case:

    private void testAnimMaskBuilder(SkinningControl skinningControl) {
        Armature armature = skinningControl.getArmature();

        // upperBody
        AnimationMask upperBody = new AnimMaskBuilder(armature)
                .addFromJoint("Spine");

        // lowerBody
        AnimationMask lowerBody = new AnimMaskBuilder(armature)
                .addJoints("Hips")
                .addFromJoint("RightUpLeg")
                .addFromJoint("LeftUpLeg");

        // allBody
        AnimationMask allBody = new AnimMaskBuilder(armature)
                .addAllJoints();

        // noHeadBody
        AnimationMask noHeadBody = new AnimMaskBuilder(armature)
                .addAllJoints()
                .removeJoints("Head", "HeadTop_End", "RightEye", "LeftEye");

        // noHeadBody2
        new AnimMaskBuilder(armature)
                .addAllJoints()
                .removeFromJoint("Neck");
    }

Edit:

  • Method signature is consistent.
  • You can chain all methods.
  • The ‘Armature’ is passed only once in the constructor.
  • Joints’ are passed to methods as strings instead of objects.
3 Likes

I like AnimMaskBuilder a lot, but not enough to add it to jme3-core myself. If someone else wants to add it to jme3-core, I won’t stop them.

The discussion about ArmatureMask seems off-topic to me. DynamicAnimControl interposes itself between the AnimComposer and the SkinningControl. It has full control over all joint transforms the SkinningControl receives. It can pass the ones generated by AnimComposer through unchanged (kinematic mode) or replace them with something totally different (dynamic mode).

For the use case of a brief upper-body flinch in a humanoid character, it’s not necessary to do anything to the AnimComposer. What’s needed are mechanisms to specify the affected bone links in the DynamicAnimControl. Assuming those links form a complete subtree of the ragdoll hierarchy, we already have:

public void setDynamicSubtree(PhysicsLink rootLink,
            Vector3f uniformAcceleration, boolean lockAll);

and

public void animateSubtree(PhysicsLink rootLink, float blendInterval);

The thing that especially bugged me about this part is that if you pass a totally foreign armature to these methods then it will return nonsense results.

On the other hand, I understand the desire not to have a generally redundant reference inside of ArmatureMask… especially since there really are cases where the same AnimationMask instance can be used for many different Armature instances. (ie: shared masks across multiple instances of the same model).

2 Likes

Thank you all for the help, I believe I have all the information I need to get things working how I’d like now.

Since an ArmatureMask isn’t made to work with DAC, i will keep on my current path of using my custom BoneChannel class to store the DAC’s BoneLinks that best correspond to the ArmatureMask for that channel. So I will also be mindful when using the DACWizard to setup a model and its bone links so that they correspond cleanly to the respective ArmatureMask’s joints.

I also notice no other jme users have seemed to show any interest in my idea of making a class that works like AnimChannel from the old system, I’m guessing not many others are in a similar position where they need to upgrade from the old anim system to the new, so the BoneChannel class I made likely looks like more clutter than it does help. But if anyone by chance is lurking and wants a complete version of the class feel free to leave a message and I’ll post it once its cleaned up and done, it saved me from refactoring at least a days worth of code when upgrading from the old system, and more importantly allows my anim state manager class to remain small since I can put much of the ArmatureMask related code for keeping track of the time/speed/loopmode/blending into the BoneChannel class instead.

2 Likes

Add-on libraries are always welcome—and if they prove popular, they can be incorporated into JME at a later date.

2 Likes

Another thing to consider when messing around with abstractions and designs is the difference between “things I will use to wire this all together” and “things the engine will use to animate my spatial as quickly as possible”.

For example, a joint list is nice to have at wiring time and can be used as the source for making armature masks, DACs, whatever… while at runtime, the ArmatureMask has a lot going for it.

I wonder if in the original case it would have been enough to hold onto your joint lists and create everything you need (armature masks, whatever) from that… rather than trying to reverse engineer armature masks.

I don’t know if the importers are already muddying this issue or not.

2 Likes

Coming back to this now that I’ve made more progress:

So I worded it wrong since technically a bone can be in more than one mask, but I should have instead said that the result is buggy if you do put a bone in two masks.

For example, if there is a root Mask (containing every joint) and a Head mask (containing just head joints), and the root mask is playing walk, If I then play a talk animation on the head, it will just keep playing the walk animation and occasionally twitch (as if it is trying to play the talk animation for 1 frame everytime the walk animation ends and is about to loop again).

So is this intended use case? If so then I think I was originally correct to say that I will need to keep a list of sub-masks to put masks within masks so that a bone can operate correctly while located in more than 1 mask like I mentioned in my original post.

Because it appears the only way to play “talk” on the head while the “root” is walking is to not use a root mask, and to instead split into “head” and “non-head” mask. And then make the root mask actually a dumby class that stores a reference to the “head” and “non-head” mask so I can make the whole body walk still.

I should also note the old animation system handled this without any problems when using AnimChannels. If the channel I’m trying to play an animation on contained a bone that is already playing an animation on another channel, it would simply listen to the most recently played channel. But the new system appears to try to play both animations from 2 different masks on the same bone at the same time.

2 Likes

The person who designed the new animation system is no longer active. He integrated his code while it was unfinished and inadequately documented. Since that day, we’ve been struggling to recover: to figure out how it works, what’s broken, and what’s missing.

I won’t speculate about his design intent for the situation where multiple layers try to animate the same joint.

3 Likes

But then the last one should win. So I don’t understand why it’s “twitching”.

2 Likes

Most of the time the second one wins actually, but still is buggy and won’t blend to the next animation correctly since it still thinks the first animation needs played again as soon as the recently played one is finished.

The only time the first animation appears to be playing over the more recently played animation one is rare cases where my code is calling setTime() for the first animation while its still playing on the root mask and that seems to make that animation take over on the head as well since the head joint is in both masks.

I guess it can be okay to have the functionality work this way, but then it is just important that users know that if a joint is in two masks, they need to stop the animation from playing on that mask prior to running an animation on another mask that contains that joint (i.e achieving a non-loop mode and easy way to cancel anims). Or make sure that each joint is only located in a single mask at a time. I think I will likely do both to make sure things work as clean as possible.

1 Like

My understanding is that the first one still applies its transforms to the joint, and then right after that the next one comes and operates based on those modified values.

“Right after” but in the same frame before anything is displayed.

The tracks are interpolating between some known start and end and aren’t applying things in some relative way.

I actually believe there is a bug in here somewhere. I will end up hitting this, too, when I go to add head movement… so we will see.

No, afaik it is applying in relative way that is controlled by transitionWeight.

“walk” will move joint A to some interpolated value between rotation 1 and rotation 2, location 1 and location 2.

“turn head” will move joint A to some separate interpolated value between rotation 1 and rotation 2, location 1 and location 2.

It won’t add it on top of the one from “walk”. It completely replaces the Vector3f and the Quaternion each time.

…unless for some reason “turn head” only does rotation or only does location and then it’s behaving exactly as it should.

From what I get, this is only when the weight is 1, otherwise, it will interpolate based on the local transform.

I guess I would need to understand what setup was trying to animate head turning on top of walk but only halfway. And either way, some layer is going to reset that transform every frame or there is already problems without layers or masks. (Edit: so it wouldn’t jump around, it would be consistently mixed.)

Someone needs to setup a full simple test case so we can see if the test case is crazy or the code is buggy.

1 Like

@yaRnMcDonuts can you try setting TransitionLength to 0 in both animations. You can do it with action.setTransitionLength(0).