Minie DynamicAnimControl and Combining Animations

Hi,

I have couple of questions about the workings of Minie and about animations, maybe @sgold, this question is most easily answered by you.

When I assign a DynamicAnimControl to a spatial, I can animate the spatial, and the collision shape is correctly updated during the animation. However, the spatial seems to have no mass, as the spatial is not falling. Hence, I thought I needed to add a RigidBodyControl as well to make it falling, but that collision shape does not move with the animations. So what should I do here?

LinkConfig defaultConfig = new LinkConfig();
RangeOfMotion defaultRom = new RangeOfMotion(1f);
DynamicAnimControl dynamicAnimControl = new DynamicAnimControl();
dynamicAnimControl.link("Back", defaultConfig, defaultRom);
dynamicAnimControl.link("Neck", defaultConfig, defaultRom);
dynamicAnimControl.link("Wing1.L", defaultConfig, defaultRom);
dynamicAnimControl.link("Wing1.R", defaultConfig, defaultRom);
dynamicAnimControl.setMass("Back", 4f);
dynamicAnimControl.setMass("Neck", 4f);
spatial.addControl(dynamicAnimControl);
World.BULLET_APP_STATE.getPhysicsSpace().add(dynamicAnimControl);

Second question, I would like to combine/merge animations. For instance, I would my dragon to continue it’s flying animation, whilst executing a bite/fire breath animation. (without creating a problem of m x n animation combinations in Blender :stuck_out_tongue: ). I think this should be done somehow by having a base animation such as flying and overriding some of the joints by a second animation using some weight per joint. What are your thoughts about this?

2 Likes

If your model with DynamicAnimControl is not affected by gravity, that’s probably because the control is in kinematic mode. In that mode, animation overrides physical forces like gravity.

To combine 2 animations (such as bite and fly) you should use multiple channels in the AnimControl (old animation system) or multiple layers in the AnimComposer (new animation system).

3 Likes

Thanks for your answer. You’re right that if I enabled ragdoll control, the gravity does work.

World.INSTANCE.enqueue(dynamicAnimControl::setRagdollMode);

However, I still can’t put head my around it all. Please bear with me.
I would like to have my birds/dragons perform animations, have dynamically updated collisions based on their animations, have their heads behave with ragdoll physics (the look at target effect), and the rest of the body be kinematic for most of the time. Do you think DynamicAnimControl is the right fit for my spatial, and if yes, can I apply regular (rigidbody) physics - e.g. applyCentralForce to the root to move it up - on the kinematic spatial? Would I need to extend DynamicAnimControl to simulate this behavior?

My current implementation is: using DynamicAnimControl with applying central force to the torsolink. However the result is a spiraling bird :stuck_out_tongue:

@Override
public void prePhysicsTick(PhysicsSpace space, float timeStep) {
    DynamicAnimControl dac = this.getSpatial().getControl(DynamicAnimControl.class);
    if (dac != null) {
        dac.getTorsoLink().getRigidBody().applyCentralForce(new Vector3f(0, 200,0));
    }
}

I’m sure I’m close to the answer, and I am eager to find out the solution.

1 Like

Clear thinking requires clear terminology:

  • When DynamicAnimControl is added to a model, it breaks the model down into pieces called physics links. It creates a physics body (a type of collision object) for each PhysicsLink.
  • A PhysicsLink is either in dynamic mode or kinematic mode. Initially, all are kinematic. Forces (such as gravity) operate only on dynamic links.
  • Ragdoll mode is a special case of dynamic mode. setRagdollMode() essentially puts all links into dynamic mode and applies gravity force to each link.

It’s possible to mix modes, such that some links are dynamic and some are kinematic. However, it turns out that physics joints work best when both end bodies are dynamic. So Minie v3 introduced pose matching, a way to apply canned animation to dynamic links.

If you put the physics links of the head and neck into dynamic mode and set their gravity vectors, the head and neck should go limp and droop.

If you put the torso into dynamic mode and apply upward force to it, the bird should ascend.

If you put the physics links of the wings in dynamic mode, you can use “pose matching” with canned animation to make them flap. (In simple situations, you could simply use kinematic mode to flap the wings, of course.)

So dynamic mode would seem like the solution to everything. However…

Dynamic physics is tricky. For one thing, centers and relative masses become important: to raise a bird by applying force only to the torso, the force must be strong enough and the torso must comprise most of the bird’s mass. Also, dynamic bodies tend to drift and spin: to prevent the bird from spinning, you may have to provide stabilizing torques and/or increase the rotational damping of the bodies.

To experiment with DynamicAnimControl, see the TestDac application in the “MinieExamples” subproject. If you press the left-bracket key ("[") you can see the effect of putting the model’s left arm into dynamic mode and applying a downward force. If you press the spacebar followed by the six key (Keyboard-6 not NumPad-6) you can see the effect of pose matching on a ragdoll.

Since TestDac is open-source, you can see how pose matching is applied. You can tinker with the code and/or add your own models.

2 Likes

Thanks a lot for your detailed answer, this weekend I’ll try to implement your suggestions. Will keep you updated on my progress.

1 Like

Hi again :), I have spent some time working further on the game. I’m running into a couple of issues still related to this topic.

  • If I put the head/neck into dynamic mode (torso mass 20, dynamic with 0 gravity, neck mass 5 dynamic 100 up-gravity), it seems to complete be loose from the parent body. It doesn’t really hang anymore, it just floats away haha upwards or downwards depending on the gravity direction. A video of this effect is shown here: https://youtu.be/cJ7lbLdt7a8 . Do you know what could be wrong?

The Dynamic Anim Control is shown here: (maybe important, I load the bird with its controls directly from a j3o file).

BirdControl - DAC
package jmeanimator.models;

import com.jme3.anim.AnimComposer;
import com.jme3.bullet.PhysicsSpace;
import com.jme3.bullet.RotationOrder;
import com.jme3.bullet.animation.*;
import com.jme3.bullet.objects.PhysicsRigidBody;
import com.jme3.math.Vector3f;
import jmeanimator.controls.Face;

public class BirdControl extends DynamicAnimControl implements Face {

    enum Action {
        FLYING ("Bird.Wingclap");

        String actionName;
        Action (String actionName) {
            this.actionName = actionName;
        }
    }

    private static float MAX_SPEED = 13f;
    Vector3f velocity = new Vector3f();

    private boolean isInitialized = false;
    private Action currentAction = Action.FLYING;
    private Action latestActivatedAction = null;

    public BirdControl () {
        LinkConfig hull = new LinkConfig(1f, MassHeuristic.Density,
            ShapeHeuristic.VertexHull, new Vector3f(1f, 1f, 1f),
            CenterHeuristic.Mean, RotationOrder.XZY);

        super.setConfig(torsoName, hull);

        super.link("TailEnd", hull,
            new RangeOfMotion(0f, 0f, 0f, 0f, 0f, 0f));
        super.link("Head", hull,
            new RangeOfMotion(0f, -0.58f, 0f, 0f, 0f, 0f));
        super.link("Wing1.R", hull,
            new RangeOfMotion(0.26f, 0f, 0f, -0.52f, 1.05f, 0f));
        super.link("Wing2.R", hull,
            new RangeOfMotion(0.26f, 0f, 0.26f, 0f, 0.26f, -0.35f));
        super.link("Tail", hull,
            new RangeOfMotion(0f, -0.02f, 0.52f, -0.31f, 0f, -0.07f));
        super.link("Back", hull,
            new RangeOfMotion(0f, 0f, 0f, 0f, 0f, 0f));
        super.link("Neck", hull,
            new RangeOfMotion(0.26f, 0f, 0f, 0f, 0f, 0f));
        super.link("Wing1.L", hull,
            new RangeOfMotion(0.26f, 0f, 0.52f, 0f, 0f, -0.94f));
        super.link("Wing2.L", hull,
            new RangeOfMotion(0.26f, 0f, 0f, -0.35f, 0.26f, -0.26f));

    }

    @Override
    public String faceCenterSpec() {
        return null;
    }

    @Override
    public Vector3f faceDirection(Vector3f storeResult) {
        return null;
    }

    @Override
    public void prePhysicsTick(PhysicsSpace space, float timeStep) {
        super.prePhysicsTick(space, timeStep);
        this.getTorsoLink().getRigidBody().getLinearVelocity(velocity);
        if (velocity.length() > MAX_SPEED) {
            this.getTorsoLink().getRigidBody().setLinearVelocity(velocity.normalize().mult(MAX_SPEED));
        }
    }

    @Override
    public void physicsTick(PhysicsSpace space, float timeStep) {
        super.physicsTick(space, timeStep);
        if (!isInitialized) {
            super.setMass(this.findLink("Torso:"), 20f);

            BoneLink neck = this.findBoneLink("Neck");
            super.setMass(neck, 5f);
            this.setDynamicSubtree(neck, new Vector3f(0f, 100f, 0f), false);

            this.getTorsoLink().getRigidBody().setAngularDamping(0.5f);
            this.getTorsoLink().setDynamic(new Vector3f(0, 0f, 0));
            for (PhysicsRigidBody rb : this.listRigidBodies()) {
                rb.setCcdMotionThreshold(0.1f);
                rb.setCcdSweptSphereRadius(0.1f);
            }
            isInitialized = true;
        }
    }

    @Override
    public void update(float tpf) {
        super.update(tpf);

        if (currentAction == Action.FLYING && latestActivatedAction != Action.FLYING) {
            AnimComposer animComposer = this.getSpatial().getControl(AnimComposer.class);
            animComposer.setCurrentAction(Action.FLYING.actionName);
            animComposer.setGlobalSpeed(5f);
            latestActivatedAction = Action.FLYING;
        }
    }


}

  • I have looked into Pose Matching in TestDac, but the implementation as is provided in that class does not lead to a better situation to my understanding (It will be a more complex situation, but I’m trying to make the simple case work first :stuck_out_tongue: ). A video of the TestDac implementation of the action “go anim pose”: https://youtu.be/MIOo2rIFcd0 .

  • Using CCD of 0.1f makes the physics of the game extremely slow after a couple of seconds (the fps is still 1400fps). It hangs and after twenty seconds it moves a bit again, with no other objects in the physics space. Video: https://youtu.be/90Y83-N84GQ . Any idea what is going on? I need the birds to be able to collide with terrain while moving fast.

  • Probably not relevant to the issues, but the dynamic torsolink physics debug mesh is lying on the ground at (0,0,0) and is not moving with the bird. I am using Minie 4.0.2.

Nonetheless I’m really content with the provided tools and framework. Here’s a small preview of flying a bird :slight_smile: https://youtu.be/EYA_45gtEfw

1 Like

The physics joint connecting the neck to the body is broken or disabled. Might be a general issue with a kinematic bodies and physics joints.

CCD has some performance impact, but usually not so huge. Generally I recommend enabling Vsync to reduce the CPU/GPU load. How many physics objects are in the video? Are you using a BulletAppState? If so, debug visualization might help us understand what’s happening here. Also, what’s physicsSpace.maxSubSteps()?

I don’t know what’s causing that, but it sounds like a serious issue.

1 Like

Thanks for your answer.

I have inspected the physics joints, the skeleton itself seems pretty normal if I visualize it with the ArmatureDebugger. I’ll try to look further into how DAC handles the physics joints and see if I can find if it is disabled.
bird

The bird is the only object in the physics space. maxSubSteps is 4, I have turned on Vsync, but still the same problem. Both my cpu and graphics card are relatively overpowered so something else must be the problem :stuck_out_tongue:. A video with debug shapes is shown here: https://youtu.be/k3DJvjNmeGI (do note the debug shape is still lying on (0,0,0)).

1 Like

It seems that if I do super.rebuild() in my initialization code, the debug shape problem is gone, and the head detachment problem seems to be somewhat gone. I did not apply a different scale to the bird, I just loaded it from a j3o file. The bird is now falling down very slowly however, and also moving the bird forward almost not happening. You can see that the pink debug shape is fighting with its location somehow: https://youtu.be/IAIjuC1xZ48
Not sure what is going on.

In the video I don’t see the collision shape of the terrain. Why is that?

That is because I wanted to show you that the physics stagnate even when I’ve turned off terrain physics.

Slight update, adding:

for (PhysicsRigidBody rb : listRigidBodies()) {
     Arrays.stream(listRigidBodies())
          .forEach(r -> { 
                if (rb != r)  rb.addToIgnoreList(r); 
          });
     rb.setCcdMotionThreshold(0.1f);
     rb.setCcdSweptSphereRadius(0.1f);
}

made a lot of difference.

1 Like

I’m still not clear what caused the issue in the first place.

I do have a tip, though: you can get the effect of all bodies in the ragdoll ignoring collisions with one another by configuring

    dac.setIgnoredHops(99);
1 Like

The investigation continues :stuck_out_tongue:. Using dac.setIgnoredHops(>=2) solves it too indeed. Further investigation shows that there is somehow a circular link becoming clear (possibly in my model?) in RagUtils:

static void ignoreCollisions(PhysicsBody start, PhysicsBody current,
        int hopsRemaining) {
    System.out.println(String.format("start: %s, current: %s, hopsRemaining: %s", start, current, hopsRemaining));
    if (hopsRemaining <= 0) {
        return;
    }
    /*
     * Take another hop.
     */
    PhysicsJoint[] joints = current.listJoints();

    System.out.println(String.format("joints: %s", String.join(", ", Arrays.asList(joints).stream().map(j -> j.getBodyA().toString() + " -> " + j.getBodyB().toString()).collect(Collectors.toList()))));
    for (PhysicsJoint joint : joints) {
        PhysicsBody neighbor = joint.findOtherBody(current);
        if (neighbor != null && neighbor != start) {
            start.addToIgnoreList(neighbor);
            System.out.println(String.format("start: %s, neighbor: %s", start, neighbor));
            ignoreCollisions(start, neighbor, hopsRemaining - 1);
        }
    }
}

Debug output creates for example:

start: RigidBody#7f81811d4890, neighbor: RigidBody#7f818122f520
start: RigidBody#7f81811d4890, current: RigidBody#7f818122f520, hopsRemaining: 7
joints: RigidBody#7f8181213f90 -> RigidBody#7f818122f520, RigidBody#7f818122f520 -> RigidBody#7f818122e790
start: RigidBody#7f81811d4890, neighbor: RigidBody#7f8181213f90
start: RigidBody#7f81811d4890, current: RigidBody#7f8181213f90, hopsRemaining: 6
joints: RigidBody#7f81811d4890 -> RigidBody#7f8181213f90, RigidBody#7f8181213f90 -> RigidBody#7f818122f520
start: RigidBody#7f81811d4890, neighbor: RigidBody#7f818122f520
start: RigidBody#7f81811d4890, current: RigidBody#7f818122f520, hopsRemaining: 5
joints: RigidBody#7f8181213f90 -> RigidBody#7f818122f520, RigidBody#7f818122f520 -> RigidBody#7f818122e790
start: RigidBody#7f81811d4890, neighbor: RigidBody#7f8181213f90
start: RigidBody#7f81811d4890, current: RigidBody#7f8181213f90, hopsRemaining: 4
joints: RigidBody#7f81811d4890 -> RigidBody#7f8181213f90, RigidBody#7f8181213f90 -> RigidBody#7f818122f520
start: RigidBody#7f81811d4890, neighbor: RigidBody#7f818122f520
start: RigidBody#7f81811d4890, current: RigidBody#7f818122f520, hopsRemaining: 3
joints: RigidBody#7f8181213f90 -> RigidBody#7f818122f520, RigidBody#7f818122f520 -> RigidBody#7f818122e790
2 Likes

Am I going to far by suggesting a possible change for RagUtils and DacLinks?
Here is my implementation:

/**
 * Ignore collisions between rigid bodies connected by at most maxHops
 * physics joints, but don't ignore any other pairs.
 *
 * @param maxHops the maximum number of hops (&ge;0)
 */
private void ignoreCollisions(int maxHops) {
    /*
     * Clear the ignore lists of all bodies.
     */
    PhysicsRigidBody bodies[] = listRigidBodies();
    for (PhysicsRigidBody body : bodies) {
        body.clearIgnoreList();
    }
    /*
     * Rebuild the ignore lists using recursion.
     */

    RagUtils.CollisionIgnoreActor collisionIgnoreActor = new RagUtils.CollisionIgnoreActor(maxHops);

    for (PhysicsRigidBody body : bodies) {
        collisionIgnoreActor.act(body);
    }
}
public static class CollisionIgnoreActor {

    private Set<AbstractMap.SimpleImmutableEntry<PhysicsBody, PhysicsBody>> ignoredPairs = new HashSet<>();
    private int maxHops;

    public CollisionIgnoreActor (int maxHops) {
        this.maxHops = maxHops;
    }

    public void act (PhysicsBody start) {
        act (start, start, maxHops);
    }

    public void act (PhysicsBody start, PhysicsBody current, int hopsRemaining) {
        if (hopsRemaining <= 0) {
            return;
        }
        /*
         * Take another hop.
         */
        PhysicsJoint[] joints = current.listJoints();

        for (PhysicsJoint joint : joints) {
            PhysicsBody neighbor = joint.findOtherBody(current);
            AbstractMap.SimpleImmutableEntry<PhysicsBody, PhysicsBody> collisionPair = new AbstractMap.SimpleImmutableEntry<>(start, neighbor);

            if (neighbor != null && neighbor != start && !ignoredPairs.contains(collisionPair)) {
                start.addToIgnoreList(neighbor);
                ignoredPairs.add(collisionPair);
                act(start, neighbor, hopsRemaining - 1);
            }
        }
    }

}

This way circular paths should no longer occur. :slight_smile:

2 Likes

Thank you for pointing out a bug in the current implementation (Minie v4.0.2) and providing a possible fix. It’s inspired me to implement a simpler fix, which I hope to include in v4.1.0 .

5 Likes