Class suggestion for new AnimSystem: BoneChannel

I’ve been in the process of converting to the new anim system for a while now and one of the things that has stood out to me is how the new ArmatureMask works a lot differently than the old AnimChannel.

I see the advantages of ArmatureMask being a lighter weight class, and allows the developer to run a channel on a specific layer using its String name, rather than requiring a reference to the whole ArmatureMask or AnimChannel object.

But ArmatureMask unfortunately does not store the list of joints (something I have found necessary when using DynamicAnimControl for ragdoll effects on specific bones). I’ve also noticed from my testing in an editor so far (please correct me if I’m wrong) that a bone cannot be in more than one ArmatureMask at a time, so I wrote this class so that boneChannels can be nested within other bone channels, this way you don’t have to constantly run the animation on the the torso, left arm, right arm, and head layers individually, and could instead group those into an upperBodyBoneChannel that contains all of them.

The other advantage of using this in place of ArmatureMask is that it can be used as a 1-to-1 drop in replacement for AnimChannel. I had a lot of code from the old anim system that handles running animations on different limbs effectively using AnimChannels, so I wrote BoneChannel to have all of these same methods and greatly reduce the amount of refactoring I needed to do.

Does anyone else think this class would be useful?

import com.jme3.anim.AnimClip;
import com.jme3.anim.AnimComposer;
import com.jme3.anim.Armature;
import com.jme3.anim.ArmatureMask;
import com.jme3.anim.SkinningControl;
import com.jme3.anim.tween.action.Action;
import com.jme3.animation.LoopMode;
import java.util.ArrayList;

/**
 *
 * @author ryan
 */
public class BoneChannel {
    
    //this class is basically just an ArmatureMask that stores the name of the bones in the mask, since the mask is needed for AnimComposer but the bone names are needed for DynamicAnimControl. so this way
    // you can easily switch a whole channel from running an anim to stopping the anim and applying ragDoll or dynamic force with DAC
    
    
    public ArrayList<BoneChannel> subBoneChannels; //list of other channels that are a part of this one, e.i. store the left arm, right arm, and torso boneChannels into this list for a new upperBody boneChannel
                                                // that re-uses the masks for those without creating new masks and causing a bone to exist in more than one mask? (still need more testing, but fairly sure a bone cannot be in more than 1 mask layer at a time)


private String layerName;
public ArrayList<String> bones;
public ArmatureMask armatureMask;

private AnimComposer animComposer;
private SkinningControl skinningControl;
private Armature armature;


public LoopMode loopMode;
public String currentAnimation;
public float speed;    

public String getLayerName() {        return layerName;    }

public void setSpeed(float speed) {        this.speed = speed;    }
public float getSpeed() {        return speed;    }
    
public void setLoopMode(LoopMode loopMode) {        this.loopMode = loopMode;    }
public LoopMode getLoopMode() {        return loopMode;    }

public String getAnimationName() {        return currentAnimation;    }



public BoneChannel(String layerName, SkinningControl skinningControl, AnimComposer animComposer) {
    this.skinningControl = skinningControl;        
    this.animComposer = animComposer;
    armature = skinningControl.getArmature();
}



public void addBonesStrings(String... boneNames){
    addBones(boneNames);
}

public void clear(){
    animComposer.removeLayer(layerName); //clear existent layer 
}

public void addBones(String[] boneNames){
    
    if(armatureMask == null){
        armatureMask = new ArmatureMask(armature);           
    }
     armatureMask.addBones(armature, boneNames);
    
    for(int b = 0; b < boneNames.length; b++){
        String boneName = boneNames[b];
        if(!bones.contains(boneName)){
            bones.add(boneName);
        }
    }
    
    animComposer.makeLayer(layerName, armatureMask);
    
}

public void setArmatureMask(ArmatureMask armatureMask){
    clear();
    this.armatureMask = armatureMask;
    
    animComposer.makeLayer(layerName, armatureMask);
}




public void setAnim(String animName, float blendTime) { //is blend time relevent to new anim system in a way that can be incorporated here like it was with old animChannel?
    currentAnimation = animName;
    animComposer.setCurrentAction(currentAnimation, layerName);

}
public void setAnim(String animName) {
    currentAnimation = animName;
    animComposer.setCurrentAction(currentAnimation, layerName);
}


public AnimClip getCurrentAnimClip(){        return animComposer.getAnimClip(currentAnimation);   }

public void setTime(double newTime) {  animComposer.setTime(layerName, newTime);        }
public double getTime() {     return animComposer.getTime(layerName);    }

public double getAnimMaxTime() {
    AnimClip animClip = getCurrentAnimClip();
    if(animClip != null){
        return animClip.getLength();
    }
    
    return 0;
    }
}

I should also mention that this class not entirely done, but I wanted to run this idea past some other JME users with more experience in the new animation system to see if this sounds useful or if I’m wasting my time reinventing something that already exists or isn’t necessary.

2 Likes

I mean… it effectively does because it keeps a bitset of the joints. It knows what joints it applies to. It can’t directly tell you what they are but they could be derived from the bitset.

Nothing in the code is preventing this. You will have to post a test case. In fact, the code looks specifically designed to allow a joint to be in as many different masks as you like.

Seems to me like dynamic anim control needs to be able to work with ArmatureMasks or something.

1 Like

That is true, I guess I should have said more specifically that there didn’t appear to be a simple way to get the joints without a PR or custom branch since it appears to have entirely private access.

That’s good to hear, I expect I was doing something wrong then so I’ll play around with the code where I was testing armature masks and see if there’s something strange I was doing to make me think it worked otherwise.

That’s a good point, I probably should have started off tagging @sgold as well to ask his opinion on this and to see if its possible to add a method to DynamicAnimControl that takes in the ArmatureMask instead of list of joints, or something similar. But i suspect this would also require a getJoints() method in ArmatureMask to work too

That may be true in any case. A lot of these classes never got some useful getters.

Edit: though truthfully what we really need is a way to pass an armature to the mask and get back the list of joints to which it applies. Sort of a reverse of some of the other methods.

1 Like

a bone cannot be in more than one ArmatureMask at a time

Actually, it can. The real question is: what do you want the joint (“bone”) to do when 2 layers (with overlapping masks) are telling it to do 2 different things?

Seems to me like dynamic anim control needs to be able to work with ArmatureMasks or something.

DynamicAnimControl has its own mechanisms for configuring armature joints and groups of joints. If those mechanisms are insufficient, I’m willing to extend them. But first I’d need to understand the proposed use case.

there didn’t appear to be a simple way to get the joints

The solution is to loop over all joints in the Armature and test them using AnimationMask.contains(). Something like this:

        AnimComposer composer = spatial.getControl(AnimComposer.class);
        AnimLayer layer = composer.getLayer("DEFAULT");
        AnimationMask mask = layer.getMask();

        SkinningControl skinner = spatial.getControl(SkinningControl.class);
        Armature armature = skinner.getArmature();
        List<Joint> allJoints = armature.getJointList();

        List<Joint> layerJoints = new ArrayList<>();
        for (Joint joint : allJoints) {
            if (mask.contains(joint)) {
                layerJoints.add(joint);
            }
        }
2 Likes

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.