Create Skeletal Animation using Bones

Hello, I have a model with skeleton but no animation. Instead of creating animation in blender I want to create it directly on three.js ( by rotating bones ).
For example, I want some bones to rotate like following
function rotate_1(){ skeleton.bones[index1].rotate.x += 0.1; skeleton.bones[index2].rotate.y += 0.3; } //similarly, rotate_2(), rotate_3() .... // rotate_1() for 3 seconds, rotate_2() for 5 .... // I want relative timing so that I will able to adjust the speed of animation
And then I want to pass it to someting like gltf.animation[0, 1, 2… ]. After that either it will be

  1. Exported as GLTF
    or,
  2. Directly appied to the page
    mixer.clipAction(gltf.animation[index]); //later action.play();
    Is it possible in three.js? (By any means)
    Alternatively, if I want to manipulate gltf.animations[index] then is there any way to do this ?

Yes, but that means you have to author the keyframe tracks and animation clips by yourself. The problem is that the respective API is not intended for this use case so you really have to know what you are doing.

It’s still best to author animation data in a DCC tool like Blender and then export to glTF.

I’ve found the following in official documentation.
Keyframe Track in three.js
But I haven’t found any example of this. Is there any documentation regarding this in anywhere like threejsfundamentals.org ?

No, there is only a small example that shows the setup of some simple tracks:

https://threejs.org/examples/misc_animation_keys

I modified the example like following. I want to load a custom model and move the mesh. But it is not working. It is showing something like root is undefined

<!DOCTYPE html>
<html lang="en">
	<head>
		<title>three.js webgl - animation - basic</title>
		<meta charset="utf-8">
		<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
		<link type="text/css" rel="stylesheet" href="main.css">
	</head>
	<body>

		<div id="info">
			<a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> webgl - animation - basic use
		</div><script src="js/three.js"></script><script src="js/stats.min.js"></script>
		<script src="js/GLTFLoader.js"></script>

		<script>

			var stats, clock;
			var scene, camera, renderer, mixer;

			init();
			animate();

			function init() {

				scene = new THREE.Scene();

				//

				camera = new THREE.PerspectiveCamera( 40, window.innerWidth / window.innerHeight, 1, 1000 );
				camera.position.set( 50, 50, 100 );
				camera.lookAt( scene.position );

				//

				var axesHelper = new THREE.AxesHelper( 10 );
				scene.add( axesHelper );

				scene.add(new THREE.AmbientLight(0xffffff,1));

				//

				//var geometry = new THREE.BoxBufferGeometry( 5, 5, 5 );
				//var material = new THREE.MeshBasicMaterial( { color: 0xffffff, transparent: true } );
				//var mesh = new THREE.Mesh( geometry, material );
				//scene.add( mesh );
				var mesh;
				var loader = new THREE.GLTFLoader();
				loader.load("models/Soldier.glb",function(gltf){
					mesh = gltf.scene;
					mesh.position.set(0,0,0);
					scene.add(mesh);
				});

				// create a keyframe track (i.e. a timed sequence of keyframes) for each animated property
				// Note: the keyframe track type should correspond to the type of the property being animated

				// POSITION
				var positionKF = new THREE.VectorKeyframeTrack( '.position', [ 0, 1, 2 ], [ 0, 0, 0, 30, 0, 0, 0, 0, 0 ] );

				// SCALE
				var scaleKF = new THREE.VectorKeyframeTrack( '.scale', [ 0, 1, 2 ], [ 1, 1, 1, 2, 2, 2, 1, 1, 1 ] );

				// ROTATION
				// Rotation should be performed using quaternions, using a THREE.QuaternionKeyframeTrack
				// Interpolating Euler angles (.rotation property) can be problematic and is currently not supported

				// set up rotation about x axis
				var xAxis = new THREE.Vector3( 1, 0, 0 );

				var qInitial = new THREE.Quaternion().setFromAxisAngle( xAxis, 0 );
				var qFinal = new THREE.Quaternion().setFromAxisAngle( xAxis, Math.PI );
				var quaternionKF = new THREE.QuaternionKeyframeTrack( '.quaternion', [ 0, 1, 2 ], [ qInitial.x, qInitial.y, qInitial.z, qInitial.w, qFinal.x, qFinal.y, qFinal.z, qFinal.w, qInitial.x, qInitial.y, qInitial.z, qInitial.w ] );

				// COLOR
				var colorKF = new THREE.ColorKeyframeTrack( '.material.color', [ 0, 1, 2 ], [ 1, 0, 0, 0, 1, 0, 0, 0, 1 ], THREE.InterpolateDiscrete );

				// OPACITY
				var opacityKF = new THREE.NumberKeyframeTrack( '.material.opacity', [ 0, 1, 2 ], [ 1, 0, 1 ] );

				// create an animation sequence with the tracks
				// If a negative time value is passed, the duration will be calculated from the times of the passed tracks array
				var clip = new THREE.AnimationClip( 'Action', 3, [ scaleKF, positionKF, quaternionKF, colorKF, opacityKF ] );

				// setup the THREE.AnimationMixer
				mixer = new THREE.AnimationMixer( mesh );

				// create a ClipAction and set it to play
				var clipAction = mixer.clipAction( clip );
				clipAction.play();

				//

				renderer = new THREE.WebGLRenderer( { antialias: true } );
				renderer.setPixelRatio( window.devicePixelRatio );
				renderer.setSize( window.innerWidth, window.innerHeight );
				document.body.appendChild( renderer.domElement );

				//

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

				//

				clock = new THREE.Clock();

				//

				window.addEventListener( 'resize', onWindowResize, false );

			}

			function onWindowResize() {

				camera.aspect = window.innerWidth / window.innerHeight;
				camera.updateProjectionMatrix();

				renderer.setSize( window.innerWidth, window.innerHeight );

			}

			function animate() {

				requestAnimationFrame( animate );

				render();

			}

			function render() {

				var delta = clock.getDelta();

				if ( mixer ) {

					mixer.update( delta );

				}

				renderer.render( scene, camera );

				stats.update();

			}

		</script>

	</body>
</html>

How can I resolve this issue ? ( I tried with mesh.children[0] but it didn’t work )
It should be something like mesh.children[0].material or mesh.material but nothing seems to work.
Another question, is it something like
var quaternionKF = new THREE.QuaternionKeyframeTrack( '.quaternion', [ 0, 1, 2 ], [ qInitial.x, qInitial.y, qInitial.z, qInitial.w, qFinal.x, qFinal.y, qFinal.z, qFinal.w, qInitial.x, qInitial.y, qInitial.z, qInitial.w ] );
And then mixer = new THREE.AnimationMixer(mesh); means I am modifying mesh.quaternion object ?
I tried with var quaternionKF = new THREE.QuaternionKeyframeTrack( '.quaternion', [ 0, 1, 2 ], [ qInitial.x, qInitial.y, qInitial.z, qInitial.w, qFinal.x, qFinal.y, qFinal.z, qFinal.w, qInitial.x, qInitial.y, qInitial.z, qInitial.w ] ); then mixer = new THREE.AnimationMixer(skeleton.bones[8]);
I didn’t get any output anyway.
Reference: I created a CSV file with bone data of the soldier.soldier_bone_data.csv (4.0 KB)
Then I printed gltf.animations[1] in console . It showed something like following


(I also printed positionKF and scaleKF in console as well as removed colorKF and opacityKF)
Where am I doing wrong ?

[ISSUE RESOLVED]
The follwoing code is working. (I managed to control bones using keyframe track) I don’t know why people like you don’t encourage others to create animation directly in three.js

<!DOCTYPE html>
<html lang="en">
	<head>
		<title>three.js webgl - animation - basic</title>
		<meta charset="utf-8">
		<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
		<link type="text/css" rel="stylesheet" href="main.css">
	</head>
	<body>

		<div id="info">
			<a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> webgl - animation - basic use
		</div><script src="js/three.js"></script><script src="js/stats.min.js"></script>
		<script src="js/GLTFLoader.js"></script>

		<script>

			var stats, clock;
			var scene, camera, renderer, mixer, mesh, skeleton;

			init();
			animate();

			function init() {

				scene = new THREE.Scene();

				//

				camera = new THREE.PerspectiveCamera( 40, window.innerWidth / window.innerHeight, 1, 1000 );
				camera.position.set( 3, 3, 6 );
				camera.lookAt( scene.position );

				//

				var axesHelper = new THREE.AxesHelper( 10 );
				scene.add( axesHelper );

				scene.add(new THREE.AmbientLight(0xffffff,1));

				//

				//var geometry = new THREE.BoxBufferGeometry( 5, 5, 5 );
				//var material = new THREE.MeshBasicMaterial( { color: 0xffffff, transparent: true } );
				//var mesh = new THREE.Mesh( geometry, material );
				//scene.add( mesh );

				var loader = new THREE.GLTFLoader();
				loader.load("models/Soldier.glb",function(gltf){
					mesh = gltf.scene;
					mesh.children[0].material = new THREE.MeshLambertMaterial();
					mesh.position.set(0,0,0);
					scene.add(mesh);

					skeleton = new THREE.SkeletonHelper( mesh );
					skeleton.visible = true;
					scene.add( skeleton );

					// create a keyframe track (i.e. a timed sequence of keyframes) for each animated property
					// Note: the keyframe track type should correspond to the type of the property being animated

					// POSITION
					/*var positionKF = new THREE.VectorKeyframeTrack( '.position', [ 0, 1, 2 ], [ 0, 0, 0, 30, 0, 0, 0, 0, 0 ] );

					// SCALE
					var scaleKF = new THREE.VectorKeyframeTrack( '.scale', [ 0, 1, 2 ], [ 1, 1, 1, 2, 2, 2, 1, 1, 1 ] );

					// ROTATION
					// Rotation should be performed using quaternions, using a THREE.QuaternionKeyframeTrack
					// Interpolating Euler angles (.rotation property) can be problematic and is currently not supported

					// set up rotation about x axis
					var xAxis = new THREE.Vector3( 1, 0, 0 );

					var qInitial = new THREE.Quaternion().setFromAxisAngle( xAxis, 0 );
					var qFinal = new THREE.Quaternion().setFromAxisAngle( xAxis, Math.PI );
					var quaternionKF = new THREE.QuaternionKeyframeTrack( '.quaternion', [ 0, 1, 2 ], [ qInitial.x, qInitial.y, qInitial.z, qInitial.w, qFinal.x, qFinal.y, qFinal.z, qFinal.w, qInitial.x, qInitial.y, qInitial.z, qInitial.w ] );

					// COLOR
					var colorKF = new THREE.ColorKeyframeTrack( '.material.color', [ 0, 1, 2 ], [ 1, 0, 0, 0, 1, 0, 0, 0, 1 ], THREE.InterpolateDiscrete );

					// OPACITY
					var opacityKF = new THREE.NumberKeyframeTrack( '.material.opacity', [ 0, 1, 2 ], [ 1, 0, 1 ] );

					// create an animation sequence with the tracks
					// If a negative time value is passed, the duration will be calculated from the times of the passed tracks array
					var clip = new THREE.AnimationClip( 'Action', 3, [ scaleKF, positionKF, quaternionKF, colorKF, opacityKF ] );*/

					var xAxis = new THREE.Vector3( 1, 0, 0 );

					var qInitial = new THREE.Quaternion().setFromAxisAngle( xAxis, 0 );
					var qFinal = new THREE.Quaternion().setFromAxisAngle( xAxis, Math.PI );
					var quaternionKF = new THREE.QuaternionKeyframeTrack( skeleton.bones[42].name+'.quaternion', [ 0, 1, 2 ], [ qInitial.x, qInitial.y, qInitial.z, qInitial.w, qFinal.x, qFinal.y, qFinal.z, qFinal.w, qInitial.x, qInitial.y, qInitial.z, qInitial.w ] );
					var clip = new THREE.AnimationClip( 'Action', 3, [ quaternionKF ] );

					// setup the THREE.AnimationMixer
					mixer = new THREE.AnimationMixer( mesh );

					// create a ClipAction and set it to play
					var clipAction = mixer.clipAction( clip );
					clipAction.play();
	
					//
				});

				renderer = new THREE.WebGLRenderer( { antialias: true } );
				renderer.outputEncoding = THREE.sRGBEncoding;
				renderer.setClearColor(new THREE.Color(0xcccccc));
				renderer.setPixelRatio( window.devicePixelRatio );
				renderer.setSize( window.innerWidth, window.innerHeight );
				document.body.appendChild( renderer.domElement );

				//

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

				//

				clock = new THREE.Clock();

				//

				window.addEventListener( 'resize', onWindowResize, false );

			}

			function onWindowResize() {

				camera.aspect = window.innerWidth / window.innerHeight;
				camera.updateProjectionMatrix();

				renderer.setSize( window.innerWidth, window.innerHeight );

			}

			function animate() {

				requestAnimationFrame( animate );

				render();

			}

			function render() {

				var delta = clock.getDelta();

				if ( mixer ) {

					mixer.update( delta );

				}

				renderer.render( scene, camera );

				stats.update();
				
			}

		</script>

	</body>
</html>
1 Like

Thanks a lot for your contribution, I was looking exactly for this, since I need programtic animations on the same model. Found your code snippet extremly useful.

I found also that using Euler for the rotation matrix serves better my porpouse since that way is easier to apply multiple rotations to the same joint.

let a = new THREE.Euler( Math.PI, 0, -.15*Math.PI/2, ‘XYZ’ );
var qInitial = new THREE.Quaternion().setFromEuler( a );