Synchronizing an AnimClip

While wrapping up the v1.0 release of Minie, I upgraded the DacWizard application to work with the new animation system. DacWizard uses the Wes animation editing/retargeting library, so that project had to be upgraded also. The Wes project includes a demo app called FlashMobDemo.

During testing, I discovered an issue in FlashMobDemo for which I saw no easy fix. I raise the issue here in hope that someone with a better grasp of the new animation system can suggest a solution.

Here’s the source code: https://github.com/stephengold/Wes/blob/master/WesExamples/src/main/java/jme3utilities/test/wes/FlashMobDemo.java

There are 5 animated models in the scene: Jaime, MhGame, Oto, Puppet, and Sinbad. Jaime and Puppet use the old (AnimControl) animation system; MhGame, Oto, and Sinbad use the new (AnimComposer) system. Each AnimControl contains a 2.333-second Animation named “Dance”, and each AnimComposer contains a 2.333-second AnimClip named “Dance”. The animations and clips are generated in such a way that t=0 in each animation corresponds to t=0 in each clip.

At the end of the initialization routine, all animations and clips get “set” in their respective controls. The intent was for the animations and clips to play in a synchronized fashion, but this is not what happens. The clips in the MhGame/Oto/Sinbad group are synchronized with each other. The animations in the Jaime/Puppet group are synchronized with each other. However, the two groups are out of phase by roughly half a second.

The old system provides AnimChannel.getTime() and .setTime() to access the time of the animation that’s playing. In the new system, time is stored in the Layer; there’s no getTime() or setTime().

Short of using reflection, I don’t see any way to synchronize an AnimClip.

1 Like

I don’t see a way to do it either.

Not being able to set time manually is kind of a pretty big missing feature, to me… as I also used it quite frequently. (I rarely let JME time do anything in my scenes as its uncontrollable.)

2 Likes

I’ll open an enhancement issue at GitHub, then.

PS: https://github.com/jMonkeyEngine/jmonkeyengine/issues/1200

1 Like

I added these methods in AnimCimposer (will make a PR if everybody is Ok with this addition)

/**
 * Returns current time of the specified layer.
 */
public double getTime(String layerName) {
    Layer l = layers.get(layerName);
    if (l == null) {
        throw new IllegalArgumentException("Unknown layer " + layerName);
    }
    return l.time;
}

/**
 * Set current time on the specified layer. 
 */
public void setTime(String layerName, double time) {
    if (time < 0) {
        throw new IllegalArgumentException("Time can not be negative");
    }
    
    Layer l = layers.get(layerName);
    if (l == null) {
        throw new IllegalArgumentException("Unknown layer " + layerName);
    }
    l.time = time;
}

I have no idea why animations starting are not synchronized between the old animation system and the new one.

But you can add below code to resynchronize them.

@Override
public void simpleUpdate(float tpf) {
    double time = sinbadComposer.getTime(AnimComposer.DEFAULT_LAYER);
    if(time == 0) {
        for (AnimChannel allChannel : allChannels) {
            allChannel.setTime(0);
        }
    } 
}
2 Likes

Added some debug prints:

@Override
public void simpleUpdate(float tpf) {
    double time = sinbadComposer.getTime(AnimComposer.DEFAULT_LAYER);
    if(time == 0) {
        System.out.println("AnimChannel=" + allChannels.get(0).getTime());
        for (AnimChannel allChannel : allChannels) {
            allChannel.setTime(0);
        }  
    } 
}

Outputs:

first run:

AnimChannel=0.0
AnimChannel=1.7562661
AnimChannel=0.015033245
AnimChannel=0.00848937
AnimChannel=0.008145571
AnimChannel=0.008092165
AnimChannel=0.008682251  

second run:

AnimChannel=0.0
AnimChannel=1.4175723
AnimChannel=0.02286172
AnimChannel=0.008599043
AnimChannel=0.00848794
AnimChannel=0.008909225
AnimChannel=0.0090687275

third run :

AnimChannel=0.0
AnimChannel=1.5841796
AnimChannel=8.9645386E-4
AnimChannel=0.0081214905
AnimChannel=0.008785963
AnimChannel=0.008635759
AnimChannel=0.0087144375

I don’t think you can/should just set layer.time directly as there is some logic in advance that makes sure it’s always in 0-length range. You probably want similar when setting layer time.

Yes of course, going to add this check.

Edit:
If the provided time is not in boundary 0-length should it throw some illegal argument exception or just ignore it?

I would welcome a PR.

1 Like

I’d prefer an exception to silent failure.

1 Like

Why print THAT?

1 Like

You know, I often write funny and sometimes silly stuff when doing prints. :wink:

Edit:
Fixed now.

I will submit a PR tomorrow.

I think if you set a time outside the range that it should get clipped as if you advanced to it. That’s not silent failure… that’s “the same as regular tpf advancement”.

So just modulo against the length or whatever.

1 Like

@pspeed Is this Ok?

public void setTime(String layerName, double time) {
    Layer l = layers.get(layerName);
    if (l == null) {
        throw new IllegalArgumentException("Unknown layer " + layerName);
    }
    if (l.currentAction == null) {
        throw new RuntimeException("There is no action running in layer " + layerName);
    }
    l.time = time % l.currentAction.getLength();
}

Sorry for being dummy, can someone tell me the difference of
time % lenght
with
(time % length + length) % length;

which is used in

it specifically has to do with the if (time < 0) check:
for example -18 % 10 is -8 which is not always what you want, instead by adding ‘length’ you shift it back to the range you want it to be in but since “time % length” could result in 0 and adding “length” to zero results in “length” which is actually excluded from the range when using modulo, you need the last “% length” to handle that specific case
so the comment above it might actually be misleading in that is only describes the part in the brackets, not the range “time” is actually put into

1 Like

Yeah, that’s weird that it’s only doing it for negative time.

A proper setTime() should do something like that, though. As written, I think it works with positive and negative times, though the math is probably clearer just to do it differently for negative values.
time > 0 then time = time % length
else time = length + time % length

1 Like

For positive times it is resetting time when tween indicates that it is finished running.

this will make sure that if there is any instant tween at end of the chain (for example a call method tween) it has an opportunity to get run.

Edit:
Note, I guess yet the problem exists for negative speeds so.

As you mentioned in the other thread reversing should be done using the idea of channels for reciprocal effects:

This is why in Lemur the idea of channels, canceling, reversing, etc… is at the Effect level where you potentially make entire new animations to do something backwards.

PR is ready

4 Likes

Thank you!

1 Like