Duplicating an animation in Three.js

Magda Odrowąż-Żelezik
Fink IT
Published in
5 min readApr 18, 2020

This fairly simple process seem to cause a lot of trouble for many people. Here I come with a very quick tutorial on duplicating an animation in Three.js based on what I’ve learned so far on forums and docs.

Note: code I present is from my React app (therefore a lot of this keyword etc). I present the code to explain, not for copying :)

Not all at once

Firstly, it is important to remember that simply copying the model won’t let you copy it’s animation — even if it is defined in the FBX or any other file you’re uploading. These are separate steps of the process.

Copy the model

Copying the model is supersimple — use the defined function the geometry object to copy.

Notice, that I create and add material and set other properties to the model one first, and then copy the model. After cloning, I offset the position a little bit to be able to present the outcome.

this.model = obj;var mat1 = new THREE.MeshPhongMaterial({   color: 0xAA4444,   skinning: true ,   morphTargets :true,   specular: 0x1d1c3a,   reflectivity: 0.8,   shininess: 20,} );this.model.traverse(o => {  if (o.isMesh) {    o.castShadow = true;    o.receiveShadow = true;    o.material = mat1;}});// Set the models initial scalethis.model.scale.set(.2, .2,  .2);this.model.position.y = -1;this.model.position.x = 0;this.model2 = this.model.clone();this.model2.position.x = -4;

Note: you need to add both models to the scene.

this.scene.add(this.model);this.scene.add(this.model2);

There you go, two identical models in the scene.

Copied model on the left, original on the right

Copying the animation

Now you need to create a brand new mixer for your new model. Tip: to use easily multiple mixers, create an array to keep them in handy.

this.mixers = [];

While creating mixers on you models, push them to this array.

this.mixer = new THREE.AnimationMixer(this.model);this.mixers.push(this.mixer);this.mixer2 = new THREE.AnimationMixer(this.model2);this.mixers.push(this.mixer2);

Now, when you clip action, you will need to do it on two mixers separately:

let fileAnimations = obj.animations;let anim = fileAnimations[0]; //I have one clip so I don't bother, but there are obviously neater ways to do this ;)anim.optimize();let act = this.mixer.clipAction(anim);let act2 = this.mixer2.clipAction(anim);

Note, that we are creating two separate actions from two separate mixers on the same clip. There is no need to copy the clip, it’s there to be used.

Now comes the bonus part, if you don’t need it, just skip to the next one.

Modifying multiple properties of the object

Let’s say you want to speed up the clip. Also, playing it without loop would be nice. Oh, and you want animation to stop at the end of the clip, not go back to the first frame.

You can obviously do it like this:

act.loop = THREE.LoopOnce;
act2.loop = THREE.LoopOnce;

And this for every property. I already feel asleep writing this two lines.

Of course doing something like this:

act = {
loop: THREE.LoopOnce,
timeScale: 4
}

will overwrite the object, so nope.

Doing sophisticated tricks like this:

let x = Object.assign({}, Object.create(act, Object.getOwnPropertyDescriptors({your properties})));

will also not work. Why? act is an object indeed, so it should be copy-able with Object.assign, but it also an instance of the AnimationAction prototype. What this function above does, is creating a copy of a prototype instance (which is good), but it overwrites the properties of the object, not the prototype — which is both good (we don’t want to mess with the prototype) and bad, because Three will still prioritise prototype properties. We need to target the properties in the instance directly.

I’m no close to a black belt in JS so I could not figure out the neat way of achieving this with Object methods. Instead, I wrote my own single line function which does exactly what I need.

let overwriteProps = (proto, object) => {    Object.entries(object).map(entry => {        proto[entry[0]] = entry[1];    })   
return proto;
}

I use it like this:

let modified = {   loop : THREE.LoopOnce,   clampWhenFinished : true,   timeScale : 4}this.action = overwriteProps(act, modified);this.action2 = overwriteProps(act2, modified);

And there you have two actions with updated properties, without destructuring the base objects. Neat.

All you need to do now is to press play:

this.action.play();this.action2.play();

Updating multiple mixers

Last but not least we need to update mixers in the animating function. We should use the mixers array to have this process go smooth. Now all we need is a simple loop to iterate over the array and update each mixer separately. Be sure to access the delta before the loop!

update=()=>{   let delta = this.clock.getDelta();   if(this.mixers.length !== 0){       for ( let i = 0, l = this.mixers.length; i < l; i ++ ) {          this.mixers[ i ].update( delta );       }   }   this.renderer.render(this.scene, this.camera);   requestAnimationFrame(this.update);}

Last remarks

And that’s it. Here’s the example animation, slightly enhanced in DaVinci Resolve to look cute.

End product

Note that in this example we had just two copies to manage. If you need more, probably some more arrays could come in handy. I already see that actions and models could be packed just like mixers were, in arrays. Then you could easily iterate over them and not loose space and time to create all those temporary variables of act3, ..4, ..182 etc.

Here’s an example:

this.model.position.x = -10;this.models.push(this.model);for(let i =0; i<4; i++){ //let's make four copies   let newModel = this.model.clone();   newModel.position.x = this.model.position.x-4*(i+1);   this.models.push(newModel);}this.models.forEach(model=>{    this.scene.add(model);    this.mixers.push(new THREE.AnimationMixer(model));})let fileAnimations = obj.animations;let anim = fileAnimations[0];anim.optimize();let modified = {  loop : THREE.LoopOnce,  clampWhenFinished : true,  timeScale : 4}this.mixers.forEach(mixer =>{  this.actions.push(    overwriteProps( //overwrite props while clipping       mixer.clipAction(anim),       modified    )  )})this.actions.forEach(action=>{    action.play();})

Get creative. We can always optimise :).

This article is based on a part of my engineer thesis. All the screenshot belong to me and depict my original work.

--

--

Magda Odrowąż-Żelezik
Fink IT
Editor for

Creative front end developer, currently excited about learning 3D graphics. Visit magdazelezik.com