Three.js: Very Basic Animation

My previous post described how I created a simplistic 3D model with Blender and how I imported that into a three.js 3D scene. In this post I look at how a Blender model can be loaded and animated with the aid of the three.js library. Oh, it seems a rather long post too. Give yourself a treat if you manage to read all of the way through!

Work in Blender

To begin I returned to Blender and created a new 3D model. A model that was even more simplistic than my previous robot-type model. I wanted to produce something very simple that I could animate easily. I didn’t want to worry about having to add bones to my 3D model or anything like that. In the end I essentially created a box with some C-shaped ‘feet’ and I animated the movement of the feet and the box ‘body’ in Blender to give the illusion of, erm, a walking box I guess! An example of the final result can be seen here. Only works in WebGL-enabled browsers by the way.

As I write this post I’m trying to recall where I located the Blender Python script file that allowed me to export a javascript (.js) file from Blender. I’m fairly sure that I found it amongst the files that I’d downloaded from the three.js github repository, located down in the utils/exporters/blender path somewhere.

In order to export my model from Blender I used the Export > Three.js (.js) option from the Blender File menu; thus calling upon the Python export script that I’d previously installed.

Export option in Blender

Export option in Blender

From the Blender export window I then selected the options shown in the next image.

Blender Export Options

Blender Export Options

This gave me a .js file that contained the definition of my simple model.

I  want to briefly make a reference to the site of Kadrmas Concepts which is where I found a nice tutorial on how to use bones in Blender. I didn’t actually use bones in my simple model in the end but this post about modelling and exporting was to the point.

I also found the superb THREE Fab tool at the same time. I think that I mentioned this in my previous post; it’s a great way of importing models and just playing with primitives and lighting. I managed to solve some minor issues that I had with my basic animation by using this tool. It’s nice and easy to just drag and drop the exported .js file onto the window in the browser and view (and animate) your model.

Onto the interesting bit…

The Code

Again I’ve used the same code set-up that I’ve used and discussed previously in this series of blog posts. Therefore I’m not going to explain all of the details again such as how the require.js stuff works or setting-up of the basic 3D scene.

Here’s the first chunk of javascript.

require(['96methods/BotCharacter', 'libraries/RequestAnimationFrame', 'libraries/Three', 'jquery'], function(Character) {

	var camera, stage, renderer;
	var character = new Character('./models/robot02_01_feet.js');

	// Initialise and then animate the 3D scene!
	init();
	animate();

	function init() {

		// Begin loading the character model:
		character.load();

		// Instantiate the 3D scene:
		stage = new THREE.Scene();

		// Instantiate an Orthographic camera this time.
		// The Left/Right/Top/Bottom values seem to be relative to the scene's 0, 0, 0 origin.
		// The best result seems to come if the overall viewable area is divided in 2 and
		// the Left & Bottom values set to negative
		camera = new THREE.OrthographicCamera(
			window.innerWidth / -2, 	// Left
			window.innerWidth / 2,		// Right
			window.innerHeight / 2,		// Top
			window.innerHeight / -2,	// Bottom
			-2000,						// Near clipping plane
			1000 );						// Far clipping plane

		// Set the camera position:
		camera.position.y = 100;
		camera.position.x = 200;
		camera.position.z = 200;

		camera.lookAt(new THREE.Vector3(0, 0, 0));

		// Add the camera to the scene/stage:
		stage.add(camera);

		// Add some lights to the scene
		var directionalLight = new THREE.DirectionalLight(0xffffff, 1.0);
		directionalLight.position.x = 1;
		directionalLight.position.y = 0;
		directionalLight.position.z = 0;
		stage.add( directionalLight );

		var directionalLight2 = new THREE.DirectionalLight(0xeeeeee, 2.0);
		// A different way to specify the position:
		directionalLight2.position.set(-1, 0, 1);
		stage.add( directionalLight2 );

		// Instantiate the renderer
		renderer = new THREE.WebGLRenderer();
		// .. and set it's size:
		renderer.setSize(window.innerWidth, window.innerHeight);

		// Place the renderer into the HTML (inside the #container div):
		$('#container').append(renderer.domElement);

	}

At line 4 I’m simply calling the constructor of my Character class again as I have in previous examples. This time I’m passing in an argument that specifies the path of my 3D model. The Character class will load this model, when the load() method is invoked, and will control the animation for me; more of that shortly.

Again, at lines 7 & 8, I am initialising the scene and beginning the animation loop. Following this is the definition of the init() method.

Main point of interest here is at line 13 where I call the load() method of my Character class. Think that’s the only main point of interest in the init() function actually!

This leads us to the remainder of the file and the animate() function.

	function animate() {
		// Defined in the RequestAnimationFrame.js file, this function means that the
		// animate function is called upon timeout:
		requestAnimationFrame( animate );

		// Find out if the robot has loaded:
		if(character.hasLoaded()) {
			// Add the character to the stage?
			if(!character.onStage()) {
				character.addToStage(stage);
			}
			// Animate:
			else {
				character.animateCharacter();
			}
		}

		render();

		// Update the character position
		TWEEN.update();
	}

	function render() {

		// *** Update the scene ***
		renderer.render(stage, camera);
	}
});

Lines 68 to 77 contain the key points of interest. Essentially here I’m trying to determine if my 3D model has loaded, if it has loaded have I added it to the stage yet and if it’s already on the stage I then call my animateCharacter() method to update the character animation.

Right, now onto the Character class definition.

define(['libraries/Three', 'libraries/mootools-core-1.4.2'], function() {

	return new Class(function(modelPath) {

		// Private members
		var mesh = null;
		var modelLoader = null;
		var loadedModel = false;

		var onStage = false;

		var animCycleDuration = 1000,
			numKeyframes = 39,
			scaleFactor = 20.5;

		var currentKeyframe,
			lastKeyframe;

		var lastFrameRenderedFlag = false;

		Object.append(this, {
			// Getters/setters
			getMesh: function() { return mesh; },
			setMesh: function(value) { mesh = value; },

			// Load the model
			load: function() {
				// Instantiate the JSON loader:
				modelLoader = new THREE.JSONLoader();

				// Initiate loading of the model and define callback:
				modelLoader.load( modelPath, function ( geometry ) {

					// Create a mesh based upon the loaded geometry:
					mesh = new THREE.Mesh( geometry, new THREE.MeshLambertMaterial( { color: 0xFF6060, morphTargets: true } ) );

					// Scale-up the model so that we can see it:
					mesh.scale.set( scaleFactor, scaleFactor, scaleFactor );

					// Perhaps set flag and the main scene can ask this character if it's loaded?
					loadedModel = true;
				});
			},

			// Checks if the character model has loaded:
			hasLoaded: function() {	return loadedModel; },
			// Checks if the character model has been added to the stage:
			onStage: function() { return onStage; },
			// Adds the character model to the stage:
			addToStage: function(stage) {
				stage.add(mesh);
				onStage = true;
			},

			animateCharacter: function() {

				// Calculate interpolation - how long a single frame is shown for:
				var interpolation = animCycleDuration / numKeyframes;

				// Determine the current frame (keyframe) by calculating how much
				// more time of our animation cycle remains to be played thru.
				var time = Date.now() % animCycleDuration;

				var keyframe = Math.floor( time / interpolation ) + 1;

				// Update the frame details if the keyframe just calculated is different to the
				// current keyframe:
				if ( keyframe != currentKeyframe ) {

					// Update the morphTargetInfluences array to progress the animation cycle:
					mesh.morphTargetInfluences[ lastKeyframe ] = 0;
					mesh.morphTargetInfluences[ currentKeyframe ] = 1;
					mesh.morphTargetInfluences[ keyframe ] = 0;

					// Track previous/last keyframe and the current keyframe:
					lastKeyframe = currentKeyframe;
					currentKeyframe = keyframe;

					// Not 100% sure about this little bit. I think it helps for a smoother animation,
					// especially when animation duration is long, according to the catchvar.com site!
					// However I have found that it screws up the my little bot animation so I've taken it out.
					// Essentially the movement is very jerky when it comes to the forward movement of the whole mesh.
					//mesh.morphTargetInfluences[ keyframe ] = ( time % interpolation ) / interpolation;
					//mesh.morphTargetInfluences[ lastKeyframe ] = 1 - mesh.morphTargetInfluences[ keyframe ];

					// Determine if the character mesh should be moved forward.
					// The character mesh should be moved once the animation cycle has completed,
					// to provide the suggestion of continuous walking.

					// Has the final frame of the cycle been rendered and, thus, we are now going
					// to render the first frame in the cycle?
					if(lastFrameRenderedFlag) {
						// Clear the flag:
						lastFrameRenderedFlag = false;
						// The character in the Blender animation moves forward by 2 units during a complete
						// animation cycle, so want to move character by that amount
						// and factor in the scaling-up, hence multiplying scale factor by 2:
						mesh.position.x = mesh.position.x + (2* scaleFactor);
					}
					// The keyframe will be < the lastKeyframe when the last frame
					// of the animation cycle is being rendered, i.e. when the final
					// frame is reached:
					if(keyframe < lastKeyframe)
					{
						// Flag that the last frame has been reached:
						lastFrameRenderedFlag=true;
					}
				}
			}
		}); // End of Object.append
	});
});

I apologise as I have rather dumped a lot of code there. I’ve also written a lot of comments in that block of code.

The first interesting bit is on lines 12 to 14 where I’ve declared some local variables. Here they are again:

var animCycleDuration = 1000,
			numKeyframes = 39,
			scaleFactor = 20.5;

My animation in Blender runs for a second, so that’s where the 1000 came from, i.e. 1000 milliseconds. I have a total of 40 frames in my animation and I probably should have named my numKeyframes variable something like lastKeyframe as the frame numbering goes from 0 to 39. I’m scaling-up my model as it can barely be seen otherwise.

Next comes the load() method that we saw being called earlier in the init() function:

// Load the model
load: function() {
	// Instantiate the JSON loader:
	modelLoader = new THREE.JSONLoader();

	// Initiate loading of the model and define callback:
	modelLoader.load( modelPath, function ( geometry ) {

		// Create a mesh based upon the loaded geometry:
		mesh = new THREE.Mesh( geometry, new THREE.MeshLambertMaterial( { color: 0xFF6060, morphTargets: true } ) );

		// Scale-up the model so that we can see it:
		mesh.scale.set( scaleFactor, scaleFactor, scaleFactor );

		// Perhaps set flag and the main scene can ask this character if it's loaded?
		loadedModel = true;
	});
},

I am using the JSONLoader class to load my 3D model. At line 32 I am calling the load() method and specifying a callback function that will be invoked once the model has loaded. Once loaded I define a mesh using the loaded model geometry, scale it up and set a flag to show that the model has loaded (this flag is used by the animate() function in my main block of javascript code to determine if the model is loaded).

Following this are the definitions of some methods used by the main javascript code to determine if the model is loaded, if it’s been added to the scene (or stage as I’ve called it) and a method used to add the loaded model to the stage.

Next comes the animateCharacter() method. Note I’ve reduced some of comments in the following section to make it a little easier to follow.

animateCharacter: function() {

	// Calculate interpolation - how long a single frame is shown for:
	var interpolation = animCycleDuration / numKeyframes;

	// Determine the current frame (keyframe) by calculating how much
	// more time of our animation cycle remains to be played thru.
	var time = Date.now() % animCycleDuration;

	var keyframe = Math.floor( time / interpolation ) + 1;

	// Update the frame details if the keyframe just calculated is different to the
	// current keyframe:
	if ( keyframe != currentKeyframe ) {

		// Update the morphTargetInfluences array to progress the animation cycle:
		mesh.morphTargetInfluences[ lastKeyframe ] = 0;
		mesh.morphTargetInfluences[ currentKeyframe ] = 1;
		mesh.morphTargetInfluences[ keyframe ] = 0;

		// Track previous/last keyframe and the current keyframe:
		lastKeyframe = currentKeyframe;
		currentKeyframe = keyframe;

		// Determine if the character mesh should be moved forward.
		// The character mesh should be moved once the animation cycle has completed,
		// to provide the suggestion of continuous walking.

		// Has the final frame of the cycle been rendered and, thus, we are now going
		// to render the first frame in the cycle?
		if(lastFrameRenderedFlag) {
			// Clear the flag:
			lastFrameRenderedFlag = false;
			// The character in the Blender animation moves forward by 2 units during a complete
			// animation cycle, so want to move character by that amount
			// and factor in the scaling-up, hence multiplying scale factor by 2:
			mesh.position.x = mesh.position.x + (2* scaleFactor);
		}
		// The keyframe will be < the lastKeyframe when the last frame
		// of the animation cycle is being rendered, i.e. when the final
		// frame is reached:
		if(keyframe < lastKeyframe) {
			// Flag that the last frame has been reached:
			lastFrameRenderedFlag=true;
		}
	}
}

At lines 58 to 64 I am essentially calculating which of my 40 keyframes I should be displaying.

The statement at line 68 is determining if there has been a change in the keyframe to be rendered since the last time this method was called. If so, the morphTargetInfluences array is updated so that the array cell, that refers to the frame that needs to be shown, is set to 1. Bearing in mind that currentKeyframe always refers to the frame being rendered on that pass. Next lines 76 and 77 store the currentKeyframe and keyframe ready for the next time around.

The code described thus far achieves the animation of our model; cycling through the frames of our animation and looping back once we have completed one full cycle of animation, i.e. once we’ve rendered all 40 frames of animation. The remainder of the code in the animateCharacter() method moves the whole model forwards a little bit each time we complete one cycle of animation. The result is that the character appears to walk slowly forwards.

The condition at line 85 determines if the lastFrameRenderedFlag has been set and, if it has, resets the flag and moves the model mesh forward by 2 units (the character model in Blender was 2 units in size) multiplied by the scale factor that was used to enlarge our model in the first case. The condition that follows this on line 96 determines if we are about to render the last frame in the animation cycle and, if so, sets the lastFrameRenderedFlag so that the mesh movement, as just described, can take place on the next time through the render sequence.

Conclusions

The working example can be found here. It’s a bit ugly but this is a work in progress. Also remember that it only works in WebGL-enabled browsers.

I’ve realised that the material applied to my model doesn’t seem to be showing up correctly. Pretty sure, upon editing this post, that it’s because of the mesh definition at line 35 of my BotCharacter.js code.

Whilst writing-up this post I discovered that my character model had the wrong pivot point, or centre of rotation. I was going to amend my example code so that the character walked in a circle by rotating the mesh each time an animation cycle was complete, but this was when I found that the character did not rotate as intended. I went back to the THREE Fab site and dropped-in my model. When rotating around the y-axis I saw the weird rotation again. Going back to take a look in Blender I recalled that the pivot point was out. This is something I’ll need to look into in the future. It raises a number of concerns that I have about how I’m achieving the character animation and if there’s a much better way of doing it. I realised that the centre-point of my 3D scene is the point around which the animating character will rotate. I have no idea at the moment if this centre-point can be moved while the character is animating or not. If anyone reading this has any thoughts on the topic and would like to enlighten me then please comment.

Comments are closed.