user606521
user606521

Reputation: 15424

How to manually control animation frame by frame?

Regarding this Three.js example https://threejs.org/examples/#webgl_animation_keyframes I was able to load my own model and play animation in a loop. However I want to make a "slider" that will allow user to control animation frame by frame. How I can achieve this with AnimationMixer? Let's say that animation clip has duration 4s. I would like to control in realtime current animation time from 0s to 4s.

Upvotes: 6

Views: 6445

Answers (4)

Glavin001
Glavin001

Reputation: 1346

Alexander's answer is correct; to achieve what is described with a slider input control (i.e. like scrubbing a video) then the AnimationMixer.update method is not appropriate.

Fortunately, there now exists a AnimationMixer.setTime method to accomplish this. It is implemented essentially as Alexander described, which you can see here: https://github.com/mrdoob/three.js/blob/r130/src/animation/AnimationMixer.js#L649-L661

Scrub/slide to an exact time in seconds:

mixer.setTime( exactTimeInSeconds )

Move forward in time by delta amount of seconds:

Using the Clock.getDelta method to get seconds passed since last call:

const delta = clock.getDelta(); // Optional: In this case we get delta from Three.js' own clock instead of a slider
mixer.update( delta );

All Together

To tie this all together, consider an example scenario where we want to get to the 3 second mark:

mixer.setTime(2); // time = 2 second mark
mixer.update(1); // time = 2 + 1 = 3 second mark

Upvotes: 1

M -
M -

Reputation: 28462

The answer is right there in the code of the example you linked:

mixer = new THREE.AnimationMixer( model );
mixer.clipAction( gltf.animations[ 0 ] ).play();

// ...

function animate() {
    var delta = clock.getDelta();
    mixer.update( delta );

    renderer.render( scene, camera );

    requestAnimationFrame( animate );
}

mixer.update( delta ); is what updates the animation by 0.0166 seconds on every frame (at 60FPS). If you want to jump to a specific moment, simply assign the .time property to whatever second you need.

See here for more documentation on AnimationMixer.

Upvotes: 3

Llama D'Attore
Llama D'Attore

Reputation: 437

The other answer is incomplete. It would never work with a slider input as described in the original post. The problem is that the Animation Mixer update function changes the time relative to the current time and is subsequently unsuitable for a slider input as described.

Instead you want to make a function like this:

function seekAnimationTime(animMixer, timeInSeconds){
  animMixer.time=0;
  for(var i=0;i<animMixer._actions.length;i++){
    animMixer._actions[i].time=0;
  }
  animMixer.update(timeInSeconds)
}

With this you could then run this in your slider OnChange event listener:

seekAnimationTime(yourAnimationMixer, event.value);

Or to set the animation time to 2s:

seekAnimationTime(yourAnimationMixer,2);

This solution will work regardless of what the previous animationMixer time was.

Upvotes: 4

aggregate1166877
aggregate1166877

Reputation: 3150

I've created some code that allows stepping the animation by percentage from 0% to 100%. You can find the JavaScript version below, or a TypeScript version here.

To my knowledge, three.js doesn't have per-frame stepping, but rather general interpolation info (though I could be wrong about that). IMO percentage-based seeking is more useful anyway as it allows consistent stepping regardless of mesh frame count.

Example Blender animation: Controlling frame position by percentage with an external device:

The code:

/**
 * Class that offers a means of controlling an animation by percentage.
 */
class AnimationSlider {
  constructor(mesh) {
    this._fakeDelta = 0;
    this._mesh = mesh;
    this._mixer = new THREE.AnimationMixer(mesh.scene);
    this._clips = mesh.animations;
    this._percentage = 0;

    this.anims = [];
    for (let i = 0, len = this._clips.length; i < len; i++) {
      const clip = this._clips[i];
      const action = this._mixer.clipAction(clip);
      action.play();
      action.paused = true;
      //
      this.anims.push({ clip, action });
    }
  }

  /**
   * Sets the animation to an exact point.
   * @param percentage - A number from 0 to 1, where 0 is 0% and 1 is 100%.
   */
  seek(percentage) {
    if (this._percentage === percentage) {
      return;
    }

    for (let i = 0, len = this.anims.length; i < len; i++) {
      const { clip, action } = this.anims[i];
      action.time = percentage * clip.duration;
      this._mixer.update(++this._fakeDelta);
    }

    this._percentage = percentage;
  }
}

Example usage:

// Create a slider instance:
const slider = new AnimationSlider(gltfMesh);

// Set the animation position to 40%:
slider.seek(0.4);

I believe that the code is self-explanatory, but if not, let me know and I'll riddle it with comments.

Upvotes: 1

Related Questions