I cannot stop AnimationClip on last frame [RESOLVED]

Hello all,

I’m having a tough time with this method, AnimationClip.CreateFromMorphTargetSequence…

I loaded a GLB with some animations. I want two of them to loop only once and stop at last frame.
However, as talked here, the loader copy the first frame of the animations on the last, to make loops look smoother. Someone came up with a solution here, but I cannot use it the right way apparently…

I’m supposed to make a new AnimationClip from the old one (the one with the last frame I don’t want), with AnimationClip.CreateFromMorphTargetSequence, with “noLoop” parameter set to true. I tried but it doesn’t work.

I made a live exemple, with some anotations in the code

<!DOCTYPE html>
<html>
<head>
<title>Live exemple of my problem</title>
<meta charset="utf-8">
<style>
	#world {
		position: fixed;
		margin:0;
		top:0;
		left:0;
		width: 100vw;
		height: 100vh;
		overflow: hidden;
	}

	#menu {
		position: fixed;
		margin: 0;
		padding: 15px;
		top: 25vh;
		width: 30vw;
		height: 50vh;
		background-color: #ffcce6;
		text-align: center;
		z-index: 1;
	}

	#menu>button {
		margin: 10px;
	}
</style>
</head>

<body>
<div id="world"></div>

<div id="menu">
	<p>Click on this button to play the three animationAction.</p>
	<button onclick="onButtonClick()">open the cap</button>
	<p>Two of them (keyRise and capRise,) are supposed to loop only once
	and stop at the last frame. For each of them, I set the clampWhenFinished
	property to true, but to no apparent effect.</p>
	<p>I found an Issue <a href="https://github.com/mrdoob/three.js/issues/8446">here</a>
	about my problem</p>
	<p>Apparently the loader is at fault, because it copy the first fram as the last
	to make loops look smoother. So in my case I'm supposed to use
	<a href="https://github.com/mrdoob/three.js/pull/8637/commits/9d223623114a5473af120a7f6a90d74e01d63205">CreateFromMorphTargetSequence</a>
	, but I'm not sure how.</p>
	<p>You will find more information about this in my code in the GLTFloader</p>
</div>

<script src="Three.js"></script>
<script src="GLTFLoader.js"></script>
<script src="stats.min.js"></script>
<script src="OrbitControls.js"></script>

<script>
	

window.addEventListener("load", function() {
	main();
}, false);


var scene, camera, renderer, stats, clock, loader;
var loader, keyMixer, capMixer, keyRotation, keyRise, capRise;



function onButtonClick() {

keyRotation.play();
keyRise.play();
capRise.play();

keyRise.clampWhenFinished = true;
capRise.clampWhenFinished = true;

keyRise.loop = THREE.LoopOnce;
capRise.loop = THREE.LoopOnce;
};



function main() {

	scene = new THREE.Scene();
	scene.background = new THREE.Color(0x39ac73);

	camera = new THREE.PerspectiveCamera(35, window.innerWidth/window.innerHeight, 0.5, 100);
	camera.position.set(0, 0.3, 1.5);
	camera.lookAt(0, 0.3, 0);

	renderer = new THREE.WebGLRenderer({antialias:true});
	renderer.setSize(window.innerWidth, window.innerHeight);
	document.getElementById("world").appendChild(renderer.domElement);

	stats = new Stats();
	document.body.appendChild(stats.dom);

	addShadowedLight( -10, 6, -10, 0xffffff, 0.5);
	addShadowedLight( 1, 1, 1, 0xffffff, 0.5 );
	addShadowedLight( 0.5, 1, -1, 0xffaa00, 0.1 );

	clock = new THREE.Clock();
	clock.start();






	// GLB importation //

	loader = new THREE.GLTFLoader();


	loader.load("PineappleAndKey.glb", function(glb) {

		var animations = glb.animations;

		var pot = glb.scene.getObjectByName("pot");
		var cap = glb.scene.getObjectByName("couvercle");
		var key = glb.scene.getObjectByName("clef");

		var group = new THREE.Group();
		group.add(pot, cap, key);

		scene.add(group);
		group.position.z += 0.8;

		capMixer = new THREE.AnimationMixer(cap);
		keyMixer = new THREE.AnimationMixer(key);

		keyRotation = keyMixer.clipAction(
			THREE.AnimationClip.findByName( animations, 'rotationClef' ));
		keyRise = keyMixer.clipAction(
			THREE.AnimationClip.findByName( animations, 'elevationClef' ));



		capRise = capMixer.clipAction(
			THREE.AnimationClip.findByName( animations, 'ouverturePot' ));


		// Here is what I should write if I follow the process described in the 
		// pull request whose link I give in the text (here is the link again :

		// https://github.com/mrdoob/three.js/pull/8637/commits/9d223623114a5473af120a7f6a90d74e01d63205).

		// To try it, use the following code and remove
		// the last capRise declaration just above.



			//	var clipCapRise = THREE.AnimationClip.findByName( animations, 'ouverturePot');

			//	var clipCapRiseNoLoop = THREE.AnimationClip.CreateFromMorphTargetSequence(
			//	'ouverturePot', clipCapRise.tracks[0], 60, true);

			//	capRise = capMixer.clipAction(clipCapRiseNoLoop);



		// When I try this, the animation does not work at all.
		// I'm sure there is something wrong with my use of CreateFromMorphTargetSequence

	},


	function ( xhr ) {
			console.log( ( xhr.loaded / xhr.total * 100 ) + '% loaded' );
	},


	function ( error ) {
		console.log( 'An error happened' );
	}


	);

	/////////////////////






	loop();


	function loop() {
		requestAnimationFrame(loop);

		stats.update();

		if (keyMixer && clock) {
			var t = clock.getDelta();
			keyMixer.update(t);
			capMixer.update(t);
		};

		renderer.render(scene, camera);
	};





	function addShadowedLight( x, y, z, color, intensity ) {
		var directionalLight = new THREE.DirectionalLight( color, intensity );

		directionalLight.position.set( x, y, z );
		directionalLight.castShadow = true;

		var d = 1;
		directionalLight.shadow.camera.left = -d;
		directionalLight.shadow.camera.right = d;
		directionalLight.shadow.camera.top = d;
		directionalLight.shadow.camera.bottom = -d;
		directionalLight.shadow.camera.near = 0.1;
		directionalLight.shadow.camera.far = 30;
		directionalLight.shadow.mapSize.width = 1048;
		directionalLight.shadow.mapSize.height = 1048;
		directionalLight.shadow.bias = -0;

		scene.add( directionalLight );
	};

};
</script>

</body>
</html>

The GLB file is here :
PineappleAndKey.glb (67.6 KB)

I would be so glad if you helped me wrap my head around this method…

I don’t think you’ll need CreateFromMorphTargetSequence with GLB models, was there something in the docs that suggested that particular method? Try action.clampWhenFinished = true instead:


var mixer = new THREE.AnimationMixer( gltf.scene );
var clip = gltf.animations[ 0 ];
var action = mixer.clipAction( clip );
action.clampWhenFinished = true;
action.play();

https://threejs.org/docs/#api/en/animation/AnimationAction.clampWhenFinished

1 Like

I did use action.clampWhenFinished = true on my function onButtonClick(), but to no apparent effect.

In the thread that I found there is someone having the same problem, and they found out that the GLTF loader copy the first frame of animations to the last to make loops look smooth (which is a good idea I guess). But in my case, when the action stops on the last frame it looks like it came back to the first.

The solution they offered was to use AnimationClip.CreateFromMorphTargetSequence, to “remove the last frame” of the clip. But I’m not sure what is the MorphTargetSequence that I must pass as argument and where to find it in the loaded glb.

Which thread do you mean? THREE.GLTFLoader does not do this… testing the first animation (‘ouverturePot’) in your model, the following works for me:

action.clampWhenFinished = true;
action.loop = THREE.LoopOnce;
action.play();

… the pineapple stops at the top of its animation path:

1 Like

I meant this thread, but if it works for you I must have made a mistake somewhere, I guess it’s because I wrote in the wrong order :

action.play();
action.clampWhenFinished = true;
action.loop = THREE.LoopOnce;

I’m going to make a try with another order

That is referring to either JSONLoader or ObjectLoader, I think… but in any case, your code looks very close to mine so I’m surprised it isn’t working. Perhaps the order is the difference. :thinking:

I definitely cannot get it to work… The cap and the key still go back to their initial positions.
I tried to log the local action times, and I found that it doesn’t come back to 0, though it appears so.

Could you copy here the working code you made please, to compare ?

I updated the live example
Here is my new code :

<!DOCTYPE html>
<html>
<head>
<title>Live exemple of my problem</title>
<meta charset="utf-8">
<style>
	#world {
		position: fixed;
		margin:0;
		top:0;
		left:0;
		width: 100vw;
		height: 100vh;
		overflow: hidden;
	}

	#menu {
		position: fixed;
		margin: 0;
		padding: 15px;
		top: 25vh;
		width: 30vw;
		height: 50vh;
		background-color: #ffcce6;
		text-align: center;
		z-index: 1;
	}

	#menu>button {
		margin: 10px;
	}
</style>
</head>

<body>
<div id="world"></div>

<div id="menu">
	<p>Click on this button to play the three animationAction.</p>
	<button onclick="onButtonClick()">open the cap</button>
	<p>Two of them (keyRise and capRise,) are supposed to loop only once
	and stop at the last frame. For each of them, I set the clampWhenFinished
	property to true, but to no apparent effect.</p>
	<p>I found an Issue <a href="https://github.com/mrdoob/three.js/issues/8446">here</a>
	about my problem</p>
</div>

<script src="Three.js"></script>
<script src="GLTFLoader.js"></script>
<script src="stats.min.js"></script>
<script src="OrbitControls.js"></script>

<script>
	

window.addEventListener("load", function() {
	main();
}, false);


var scene, camera, renderer, stats, clock, loader;
var loader, keyMixer, capMixer, keyRotation, keyRise, capRise;



function onButtonClick() {

keyRise.clampWhenFinished = true;
capRise.clampWhenFinished = true;

keyRise.loop = THREE.LoopOnce;
capRise.loop = THREE.LoopOnce;

keyRotation.play();
keyRise.play();
capRise.play();

};



function main() {

	scene = new THREE.Scene();
	scene.background = new THREE.Color(0x39ac73);

	camera = new THREE.PerspectiveCamera(35, window.innerWidth/window.innerHeight, 0.5, 100);
	camera.position.set(0, 0.3, 1.5);
	camera.lookAt(0, 0.3, 0);

	renderer = new THREE.WebGLRenderer({antialias:true});
	renderer.setSize(window.innerWidth, window.innerHeight);
	document.getElementById("world").appendChild(renderer.domElement);

	stats = new Stats();
	document.body.appendChild(stats.dom);

	addShadowedLight( -10, 6, -10, 0xffffff, 0.5);
	addShadowedLight( 1, 1, 1, 0xffffff, 0.5 );
	addShadowedLight( 0.5, 1, -1, 0xffaa00, 0.1 );

	clock = new THREE.Clock();
	clock.start();






	// GLB importation //

	loader = new THREE.GLTFLoader();


	loader.load("PineappleAndKey.glb", function(glb) {

		var pot = glb.scene.getObjectByName("pot");
		var cap = glb.scene.getObjectByName("couvercle");
		var key = glb.scene.getObjectByName("clef");

		var animations = glb.animations;

		var keyRotationClip = THREE.AnimationClip.findByName( animations, 'rotationClef' )
		var keyRiseClip = THREE.AnimationClip.findByName( animations, 'elevationClef' )
		var capRiseClip = THREE.AnimationClip.findByName( animations, 'ouverturePot' )

		capMixer = new THREE.AnimationMixer(cap);
		keyMixer = new THREE.AnimationMixer(key);

		keyRotation = keyMixer.clipAction(keyRotationClip);
		keyRise = keyMixer.clipAction(keyRiseClip);
		capRise = capMixer.clipAction(capRiseClip);

		var group = new THREE.Group();
		group.add(pot, cap, key);

		scene.add(group);
		group.position.z += 0.8;
	},


	function ( xhr ) {
			console.log( ( xhr.loaded / xhr.total * 100 ) + '% loaded' );
	},


	function ( error ) {
		console.log( 'An error happened' );
	}


	);

	/////////////////////






	loop();


	function loop() {
		requestAnimationFrame(loop);

		stats.update();

		if (keyMixer && clock) {
			var t = clock.getDelta();
			keyMixer.update(t);
			capMixer.update(t);

			console.log("keyRise.time = " + keyRise.time + " ; capRise.time = " + capRise.time);
		};

		renderer.render(scene, camera);
	};





	function addShadowedLight( x, y, z, color, intensity ) {
		var directionalLight = new THREE.DirectionalLight( color, intensity );

		directionalLight.position.set( x, y, z );
		directionalLight.castShadow = true;

		var d = 1;
		directionalLight.shadow.camera.left = -d;
		directionalLight.shadow.camera.right = d;
		directionalLight.shadow.camera.top = d;
		directionalLight.shadow.camera.bottom = -d;
		directionalLight.shadow.camera.near = 0.1;
		directionalLight.shadow.camera.far = 30;
		directionalLight.shadow.mapSize.width = 1048;
		directionalLight.shadow.mapSize.height = 1048;
		directionalLight.shadow.bias = -0;

		scene.add( directionalLight );
	};

};
</script>

</body>
</html>

This turns out to be an issue from an older version of three.js. If you update from r91 to r96+, including a corresponding version of GLTFLoader, you should be OK:

F0E64A4F-7F1E-4D33-AE67-CF9CD3305B07-80425-0001109DDCB8620D

2 Likes

OK it works, thanks a lot for you help !

this bug occurs in 135, using action.clampWhenFinished = true;
stops it too early

Thanks this thread helped me out a lot. I am working with an enemy bot gltf model on my prototype first person shooter. The robot has a single track animation with many actions. I had to split the track up into sub clips with the following code then applied clampwhenfinished.

var EnemyHeavyBotFallBackClip = THREE.AnimationUtils.subclip(gltf.animations[0], “Take_001”, 1300, 1355);
actionEnemyHeavyBotFallBackMixer = mixer.clipAction(EnemyHeavyBotFallBackClip);
actionEnemyHeavyBotFallBackMixer.clampWhenFinished = true;
actionEnemyHeavyBotFallBackMixer.setLoop(THREE.LoopOnce);
actionEnemyHeavyBotFallBackMixer.play();