Monkanim: new animation system in the works

about the order. We already have constraints today (AnimControl should be before the Skeleton control), and this is ensured in the skeletonControl i believe. Not sure we have a better option, one of the control should ensure the proper order

Well then you are definately safe against a wrong Order however i would use a “master control” because the appstate would have to track affected spatials in a List else

Ok, then I will start with doing assertion then at least you get an exception and not some strange behaviour when doing it wrong!

Yes, you are right. Thats also whats stopped me from going into the AppState direction. I mean it would not be a really bad thing to have a list of affected spatials because it would not mean anything really for the performance, but its just something that “smells” wrong;)

Hi again;)

I have a requirements that I need your thoughts on: When transitioning between two animation states I want to “synchronise” the time of the two. Reason for this is, when e.g. switching between a crouch and a normal walk the foot positions should be as close together as possible, and this foot position can be different in the crouch- and the walk animation.

Edit: So if the right foot is currently in front in the crouch animation I want to set the time of the “normal walk” animation to a position where the right foot is in front as well

Do you understand what I mean?

I thought about doing it in the BlendSpace (here i could provide different times for the animations) but the problem with this approach is, that I need the relative weight (inside the layer) of both states to calculate the right time and I only know the weights after blend was called for both states, and then I don’t have any hook before the animations are applied to the skeleton…

Edit: @nehon: sorry forgot to tag you

hey, maybe you need the possibility to set an offset start for the transition’s target anim. Would it work?

Are you by chance working on what I have dubbed a ‘Rosen Engine’ … an animation engine based on the work by David Rosen presented in a GDC talk about how he animated the characters in Overgrowth ? (if not, I have a link that will blow you mind :wink: ) … because I have put a bunch of work into and we should probably have a little chat

Yep it’s planned. Seen this video and it’s awesome. We can definitely have chat :slight_smile:

not talking to you @nehon, I know you are over it =P …was directed at @tsr

Hahaha douche.

No, I know this GDC talk and its impressive. Anyway, what I already have is quite a mile away on what Rosen presented :wink:

Basically I’ve made an JME implementation of Rune Skovbo Johansen’s master thesis about “Automated Semi-Procedural Animation for Character Locomotion”. If you are interested you can see some videos here:

And now I am trying to redesign/refactor it based on monkanim, which is not that easy because the initial implementation is quite tightly coupled and I need to break it apart. But I think it can be done :wink:

If I make it, I am currently planning on contributing it - so maybe you can use it afterwards and work a little bit towards the “Rosen Engine”… But it will take some time…

Hmm - could be. But its not a static offset, so some kind of callback would be required i think. Maybe some general way to “listening” to transition changes.

Another issue: Do you think it could be changed, that an AppState stores the latest relative weight? So my IK Control that runs “afterwards” knows how much of which state was applied?

I was blown away by how good that system looked, and also that you managed to create an implementation of it.

Sorry for the late reply, crazy business trip week for me.

mhhh maybe it could leverage the “Additional motion events” mentioned in the first post?
Didn’t implement it (nor really designed it) but it will probably be a way to attach a custom event or “action” in a state, with an offset. Would it work? that would basically be some kind of event enqueueing / scheduling.

well there is no such thing, I actually keep a list of the anims with their weight, not States properly speaking. This list can be already accessed, but only as a read-only list, so accessing it on each frame may be a bit slow…

The first simple prototype is finished:

Its analyzing the animations, to get the step size and direction and then it uses this information together with the velocity of the spatial to calculate the weights for the different “walk” animations and also the speed of these animations. Its also already working for walking backwards, but currently without any step prediction or IK stuff.

And the result is quite ok. This is how its currently set up:

_legMotionControl = new LegMotionControl();
        rig.addControl(_legMotionControl);
        manager = _legMotionControl.getAnimationManager();
                
        _legMotionControl.addLeg(
                "left_leg",
                "thigh.L",
                "foot.L",
                "toe.L",
                0.095f,
                0.185f,
                new Vector2f(0.008f, 0.012f)
        );
        
        _legMotionControl.addLeg(
                "right_leg",
                "thigh.R",
                "foot.R",
                "toe.R",
                0.095f,
                0.185f,
                new Vector2f(0.008f, 0.012f)
        );

_legMotionControl.analyseAnimation( "idle" );
        _legMotionControl.analyseAnimation( "walk", LegMotionStatistics.MotionType.WalkCycle, true );
        _legMotionControl.analyseAnimation( "jog", LegMotionStatistics.MotionType.WalkCycle, true );
        _legMotionControl.analyseAnimation( "run", LegMotionStatistics.MotionType.WalkCycle, true );
        
        
        //create motion state
        _legMotionControl.createMotionState("motion-full", "default", "idle", "walk", "jog", "run" );        
        _legMotionControl.createMotionState("motion-walk_only", "default", "idle", "walk" );        
        _legMotionControl.createMotionState("motion-jog_only", "default", "idle", "jog" ); 
        _legMotionControl.createMotionState("motion-run_only", "default", "idle", "run" ); 

        manager.findState(ANY_STATE).interruptTo("motion-full").when(() -> currentState.equals("motion-full"));
        manager.findState(ANY_STATE).interruptTo("motion-walk_only").when(() -> currentState.equals("motion-walk_only"));
        manager.findState(ANY_STATE).interruptTo("motion-jog_only").when(() -> currentState.equals("motion-jog_only"));
        manager.findState(ANY_STATE).interruptTo("motion-run_only").when(() -> currentState.equals("motion-run_only"));

And I am ok with this.

What I currently don’t like that much is:

  • I am currently manually calculating the tpf in the BlendSpace
  • I am completely ignoring the “time” parameter of the blend method, because I am calculating the time per animation
  • This also means, that when the time gets reset (reset method) in the AppState I ignore this as well

And without changing anything in Monkanim I currently have no idea how to optimize this.

@Nehon: Maybe you have some thoughts on this?

Here the complete BlendSpace class to get a better understanding what i mean:

public class LegMotionState implements BlendSpace {

private final float _blendSmoothing = 0.2f;

private final LegMotionControl _legControl;
private final AnimationLayer _layer;
private final AnimState _animationState;
private final float[][] _samples;

private final MotionStatistics[] _motions;
private final Anim[] _animations;
private float _oldTime;
private float[] _relativeMotionWeights;
private float[] _relativeWeightsBlended;
private float _cycleDuration;
private float _cycleDistance;
private float _normalizedTime;

public LegMotionState(LegMotionControl control, String name, String layer, String... motions) {
    List<MotionStatistics> l_stats;
    List<Anim> l_anims;
    Vector3f l_motionVelocity;

    _legControl = control;
    _oldTime = 0f;
    _normalizedTime = 0f;

    if (!_legControl.getAnimationManager().getLayers().containsKey(layer)) {
        throw new IllegalArgumentException("Could not find layer " + layer);
    }
    _layer = _legControl.getAnimationManager().getLayers().get(layer);

    _animationState = _legControl.getAnimationManager().createState(name).forAnims(motions).withBlendSpace(this);
    _animationState.setLayer(_layer);

    l_stats = new ArrayList<>();
    l_anims = new ArrayList<>();
    for (String m : motions) {
        if (!_legControl.hasMotion(m)) {
            throw new IllegalArgumentException("Could not find motion " + m);
        }
        l_stats.add(_legControl.getMotion(m));
        l_anims.add(_legControl.getAnimationManager().getAnimation(m));
        if (_legControl.hasMotion(m + "_backward")) {
            l_stats.add(_legControl.getMotion(m + "_backward"));
            l_anims.add(_legControl.getAnimationManager().getAnimation(m));
        }
    }

    _motions = new MotionStatistics[l_stats.size()];
    l_stats.toArray(_motions);
    _animations = new Anim[l_anims.size()];
    l_anims.toArray(_animations);

    _samples = new float[_motions.length][];
    for (int i = 0; i < _motions.length; i++) {
        l_motionVelocity = _motions[i].getCycleVelocity();
        _samples[i] = new float[]{l_motionVelocity.x, l_motionVelocity.y, l_motionVelocity.z};
        //_motions[i].reset();
    }
}

@Override
public void blend(List<Anim> animations, BlendingDataPool weightedAnims, float globalWeight, float time, AnimationMask mask) {
    float l_tpf = 0f;

    if (_oldTime < time) {
        l_tpf = time - _oldTime;
    }
    _oldTime = time;

    updateRelativeWeights(l_tpf, globalWeight);
    updateNormalizedTime(l_tpf);

    if (_relativeWeightsBlended == null) {
        return;
    }

    for (int i = 0; i < _animations.length; i++) {
        _animations[i].resolve(weightedAnims, _relativeWeightsBlended[i] * globalWeight, _motions[i].updateTime(_normalizedTime) * _animations[i].getLength(), mask);
    }
}

private void updateNormalizedTime(float tpf) {
    float l_scale = 1f;// scale.z;        
    float animatedCycleSpeed = 0f;
    float cycleFrequency = 0;
    float desiredCycleDuration = _legControl.getMaxStepDuration();
    //_logger.info("hSpeedSmoothed: " + hSpeedSmoothed);

    if (_relativeWeightsBlended != null) {
        for (int i = 0; i < _motions.length; i++) {
            cycleFrequency += _motions[i].getCycleFrequency() * _relativeWeightsBlended[i]; //(1/motion.cycleDuration) * weight;
            animatedCycleSpeed += _motions[i].getCycleSpeed() * _relativeWeightsBlended[i]; //motion.cycleSpeed * weight;
        }
        if (cycleFrequency > 0) {
            desiredCycleDuration = 1 / cycleFrequency;
        }
    }

    // Make the step duration / step length relation follow a sqrt curve
    float speedMultiplier = 1;
    if (_legControl.getSpeed() != 0) {
        speedMultiplier = animatedCycleSpeed * l_scale / _legControl.getSpeed();
    }

    //_logger.info("Step Duration: " + getStepDuration() + " speed " + l_speed + "multiplier: " + speedMultiplier);
    if (speedMultiplier > 0) {
        desiredCycleDuration *= speedMultiplier; //Math.sqrt( speedMultiplier );
    }

    // Enforce short enough step duration while rotating
    if (_legControl.getVerticalAngularVelocity() > 0) {
        desiredCycleDuration = Math.min(
                _legControl.getMaxStepDuration() / _legControl.getVerticalAngularVelocity(),
                desiredCycleDuration
        );
    }

    // Enforce short enough step duration while accelerating
    if (_legControl.getGroundAccelerationMagnitude() > 0) {
        desiredCycleDuration = FastMath.clamp(
                _legControl.getMaxStepDuration() / _legControl.getGroundAccelerationMagnitude(),
                desiredCycleDuration / 2,
                desiredCycleDuration
        );
    }

    // Enforce short enough step duration in general
    desiredCycleDuration = Math.min(desiredCycleDuration, _legControl.getMaxStepDuration());

    _cycleDuration = desiredCycleDuration;

    _cycleDistance = _cycleDuration * _legControl.getSpeed();

    // Synchronize animations
    if (!_legControl.isAllParked()) {
        _normalizedTime = Util.mod(_normalizedTime + (1f / _cycleDuration) * tpf);
        /*            _legMotions.values().forEach((m) -> {
            m.setTime(_normalizedTime); //@TBD: This should be only for cycleMotions - but what about the idle? need to be checked
        });*/
    }
}

private void updateRelativeWeights(float tpf, float globalWeight) {
    AlignmentTracker tr = _legControl.getAlignmentTracker();
    Vector3f objectVelocity = tr.getCombinedVelocitySmoothed();

    if (globalWeight > 0) {
        if (tr.hasNewCombinedVelocity() || _relativeMotionWeights == null) {
            // Update weights in motion group if necessary
            // Calculate motion weights - heavy call! :(
            _relativeMotionWeights = interpolate(new float[]{objectVelocity.x, 0f, objectVelocity.z}, true);
        }

        if (_relativeWeightsBlended == null) {
            _relativeWeightsBlended = new float[_relativeMotionWeights.length];
            for (int m = 0; m < _motions.length; m++) {
                _relativeWeightsBlended[m] = _relativeMotionWeights[m];
            }
        }

        for (int m = 0; m < _motions.length; m++) {
            if (_blendSmoothing > 0) {
                _relativeWeightsBlended[m] = FastMath.interpolateLinear(tpf / _blendSmoothing, _relativeWeightsBlended[m], _relativeMotionWeights[m]);
            } else {
                _relativeWeightsBlended[m] = _relativeMotionWeights[m];
            }
        }

    } else {
        _relativeWeightsBlended = null;
    }

    //normalize relative weights
    if (_relativeWeightsBlended != null) {
        float l_sum = 0f;
        for (float w : _relativeWeightsBlended) {
            l_sum += w;
        }

        if (l_sum != 1f) {
            for (int m = 0; m < _motions.length; m++) {
                _relativeWeightsBlended[m] = _relativeWeightsBlended[m] / l_sum;
            }
        }
    }
}

// Returns the weights if simple cases are fulfilled.
// Returns null otherwise.
private float[] basicChecks(float[] output) {
    if (_samples.length == 1) {
        float[] l_ret = new float[1];
        l_ret[0] = 1f;
        return l_ret;
    }
    for (int i = 0; i < _samples.length; i++) {
        if (Equals(output, _samples[i])) {
            float[] weights = new float[_samples.length];
            weights[i] = 1;
            return weights;
        }
    }
    return null;
}

private float[] interpolate(float[] output, boolean normalize) {
    float[] weights = basicChecks(output);
    if (weights != null) {
        return weights;
    }
    weights = new float[_samples.length];

    for (int i = 0; i < _samples.length; i++) {
        weights[i] = 0f;
    }

    Vector3f outp;
    Vector3f[] samp = new Vector3f[_samples.length];
    switch (output.length) {
        case 2:
            outp = new Vector3f(output[0], output[1], 0);
            for (int i = 0; i < _samples.length; i++) {
                samp[i] = new Vector3f(_samples[i][0], _samples[i][1], 0);
            }
            break;
        case 3:
            outp = new Vector3f(output[0], output[1], output[2]);
            for (int i = 0; i < _samples.length; i++) {
                samp[i] = new Vector3f(_samples[i][0], _samples[i][1], _samples[i][2]);
            }
            break;
        default:
            return null;
    }

    for (int i = 0; i < _samples.length; i++) {
        boolean outsideHull = false;
        float value = 1;
        for (int j = 0; j < _samples.length; j++) {
            if (i == j) {
                continue;
            }

            Vector3f sampleI = samp[i];
            Vector3f sampleJ = samp[j];

            float iAngle, oAngle;
            Vector3f outputProj;
            float angleMultiplier = 2;
            if (sampleI.equals(Vector3f.ZERO)) {
                iAngle = outp.angleBetween(sampleJ);
                oAngle = 0;
                outputProj = outp;
                angleMultiplier = 1;
            } else if (sampleJ.equals(Vector3f.ZERO)) {
                iAngle = outp.angleBetween(sampleI);
                oAngle = iAngle;
                outputProj = outp;
                angleMultiplier = 1;
            } else {
                iAngle = sampleI.angleBetween(sampleJ);
                if (iAngle > 0) {
                    if (outp.equals(Vector3f.ZERO)) {
                        oAngle = iAngle;
                        outputProj = outp;
                    } else {
                        Vector3f axis = sampleI.cross(sampleJ);
                        outputProj = Util.projectOntoPlane(outp, axis);
                        oAngle = sampleI.angleBetween(outputProj);
                        if (iAngle < FastMath.PI * 0.99f) {
                            if (sampleI.cross(outputProj).dot(axis) < 0) {
                                oAngle *= -1;
                            }
                        }
                    }
                } else {
                    outputProj = outp;
                    oAngle = 0;
                }
            }

            float magI = sampleI.length();
            float magJ = sampleJ.length();
            float magO = outputProj.length();
            float avgMag = (magI + magJ) / 2;
            magI /= avgMag;
            magJ /= avgMag;
            magO /= avgMag;
            Vector3f vecIJ = new Vector3f(iAngle * angleMultiplier, magJ - magI, 0);
            Vector3f vecIO = new Vector3f(oAngle * angleMultiplier, magO - magI, 0);

            float newValue = 1f - (vecIJ.dot(vecIO) / vecIJ.lengthSquared());

            if (newValue < 0) {
                outsideHull = true;
                break;
            }
            value = Math.min(value, newValue);
        }
        if (!outsideHull) {
            weights[i] = value;
        }
    }

    // Normalize weights
    if (normalize) {
        float summedWeight = 0;
        for (int i = 0; i < _samples.length; i++) {
            summedWeight += weights[i];
        }
        if (summedWeight > 0) {
            for (int i = 0; i < _samples.length; i++) {
                weights[i] /= summedWeight;
            }
        }
    }

    return weights;
}

}

4 Likes

Hey, very nice work.
Could you share the code somewhere that I could have a look?
If you want to branch monkanim I can give you access to it.

I don’t know the details, but an event thats triggered when the state transists could work.

Hmm - ok. Lets leave this for now - this is actually “only” important for the next step prediction stuff. I think we might find a better solution later…

Thx, appreciate it! Its actually only about 10% done. Most of the code I still need to “port” is about step prediction… Next I will do a simple IK solution (define a layer, which ensures that the legs do not overlap with the environment using raytracing and IK) and then do the full step prediction+IK afterwards…

Currently I cannot put it public because I am still using some assets, where I “only” have a private license. But if you PM me some eMail address I can send it to you and I trust you that you don’t put it public yet :wink:

Currently not - I think its better if we keep this separate right now, because otherwise its to tempting to just change Monkanim to my “special” needs without discussing potential solutions here :wink: But if you like we can make this part of Monkanim (or a separate module of Monkanim) as soon as a fully functional prototype is ready.

1 Like

Update: Now it also included leg prediction using IK:

So i am now finished with the port to monkanim. I would like to continue with doing an example jump state. @Nehon: Would it be possible for you to provide some jump animation in the puppet model? I would do it myself, but i am really bad at doing animations in blender :wink:

16 Likes

That’s really nice!!
I can make a jump animation yes, I have to make a crouch and stand one.
Also I also made some progress on my side, I’ll post my result later :wink:

5 Likes