I wanted to try Three.Js out since a little while, and I finally found some time for it. I’ll detail here the different classes I wrote for this to work.
Notice that the collisions are not handled yet. But this will be the topic of an upcoming post, as soon as I figure it out !
I used John Resig’s Simple JavaScript Inheritance in order to get rid of those ugly prototype declarations, to get a cleaner code, and to simplify any further implementation that would require classes inherithance.
Oh, that, and Paul Irish’s requestAnimationFrame polyfill.
That’s it for the tools. You don’t have to use them, but I will in the following explanations. In case you don’t, just replace the classes declarations by their prototype equivalences.
Now the first thing to say about what I experimented : so much trigonometry fun ! Let’s go.
Set the scene
I’m rendering within a <figure>
tag.
<figure id="basic-scene"></figure>
Now we got to create the scene. We need a camera, to define from which view point we’re looking, some light or we won’t see a thing, and a “renderer”, which is actually the canvas viewport we render with.
This main class here will also set some event listeners for us to handle the user’s interactions, and a “frame” method that we’ll call at every animation frame request.
var basicScene;
var BasicScene = Class.extend({
init: function () {
'use strict';
this.scene = new THREE.Scene();
this.camera = new THREE.PerspectiveCamera(45, 1, 0.1, 10000);
this.scene.add(this.camera);
this.light = new THREE.PointLight();
this.light.position.set(-256, 256, -256);
this.scene.add(this.light);
this.renderer = new THREE.WebGLRenderer();
this.container = jQuery('#basic-scene');
this.user = new Character({
color: 0x7A43B6
});
this.scene.add(this.user.mesh);
this.world = new World({
color: 0xF5F5F5
});
this.scene.add(this.world.mesh);
this.setAspect();
this.container.prepend(this.renderer.domElement);
this.setFocus(this.user.mesh);
this.setControls();
},
setControls: function () {
'use strict';
var user = this.user,
controls = {
left: false,
up: false,
right: false,
down: false
};
jQuery(document).keydown(function (e) {
var prevent = true;
switch (e.keyCode) {
case 37:
controls.left = true;
break;
case 38:
controls.up = true;
break;
case 39:
controls.right = true;
break;
case 40:
controls.down = true;
break;
default:
prevent = false;
}
if (prevent) {
e.preventDefault();
} else {
return;
}
user.setDirection(controls);
});
jQuery(document).keyup(function (e) {
var prevent = true;
switch (e.keyCode) {
case 37:
controls.left = false;
break;
case 38:
controls.up = false;
break;
case 39:
controls.right = false;
break;
case 40:
controls.down = false;
break;
default:
prevent = false;
}
if (prevent) {
e.preventDefault();
} else {
return;
}
user.setDirection(controls);
});
jQuery(window).resize(function () {
basicScene.setAspect();
});
},
setAspect: function () {
'use strict';
var w = this.container.width(),
h = jQuery(window).height() - this.container.offset().top - 20;
this.renderer.setSize(w, h);
this.camera.aspect = w / h;
this.camera.updateProjectionMatrix();
},
setFocus: function (object) {
'use strict';
this.camera.position.set(object.position.x, object.position.y + 128, object.position.z - 256);
this.camera.lookAt(object.position);
},
frame: function () {
'use strict';
this.user.motion();
this.setFocus(this.user.mesh);
this.renderer.render(this.scene, this.camera);
}
});
Hello World
Or so… Because of obvious performance reasons, we won’t recreate an exact 3D representation of planet earth. We’ll create one ground and four walls instead. But I’ll call it world anyway.
Using a Object3D instead of a simple Mesh allows us to update independently the geometries within this very group of meshes.
var World = Class.extend({
init: function (args) {
'use strict';
var ground = new THREE.PlaneGeometry(512, 1024),
height = 128,
walls = [
new THREE.PlaneGeometry(ground.height, height),
new THREE.PlaneGeometry(ground.width, height),
new THREE.PlaneGeometry(ground.height, height),
new THREE.PlaneGeometry(ground.width, height)
],
obstacles = [
new THREE.CubeGeometry(64, 64, 64)
],
material = new THREE.MeshLambertMaterial(args),
i;
this.mesh = new THREE.Object3D();
this.ground = new THREE.Mesh(ground, material);
this.ground.rotation.x = -Math.PI / 2;
this.mesh.add(this.ground);
this.walls = [];
for (i = 0; i < walls.length; i += 1) {
this.walls[i] = new THREE.Mesh(walls[i], material);
this.walls[i].position.y = height / 2;
this.mesh.add(this.walls[i]);
}
this.walls[0].rotation.y = -Math.PI / 2;
this.walls[0].position.x = ground.width / 2;
this.walls[1].rotation.y = Math.PI;
this.walls[1].position.z = ground.height / 2;
this.walls[2].rotation.y = Math.PI / 2;
this.walls[2].position.x = -ground.width / 2;
this.walls[3].position.z = -ground.height / 2;
this.obstacles = [];
for (i = 0; i < obstacles.length; i += 1) {
this.obstacles[i] = new THREE.Mesh(obstacles[i], material);
this.mesh.add(this.obstacles[i]);
}
this.obstacles[0].position.set(0, 32, 128);
}
});
And now the funny part…
Live little 3D character, live !
Here as well, we use an Object3D in order to group our meshes.
Our character has two different types of motion to update : its position and its rotation. Further on, we could easily make it jump, dive, or dance. But let’s start with a simple “moving around” action only.
A direction vector will represent the motion that our user is calling through the controls.
The step property will record the progression of the character’s position motion. We will use it to animate its feet and hands.
The feet of our simple character are half-spheres. The trick is simply to properly set the phiStart, phiLength, thetaStart and thetaLength parameters of the SphereGeometry method.
var Character = Class.extend({
init: function (args) {
'use strict';
var head = new THREE.SphereGeometry(32, 8, 8),
hand = new THREE.SphereGeometry(8, 4, 4),
foot = new THREE.SphereGeometry(16, 4, 4, 0, Math.PI * 2, 0, Math.PI / 2),
nose = new THREE.SphereGeometry(4, 4, 4),
material = new THREE.MeshLambertMaterial(args);
this.mesh = new THREE.Object3D();
this.mesh.position.y = 48;
this.head = new THREE.Mesh(head, material);
this.head.position.y = 0;
this.mesh.add(this.head);
this.hands = {
left: new THREE.Mesh(hand, material),
right: new THREE.Mesh(hand, material)
};
this.hands.left.position.x = -40;
this.hands.left.position.y = -8;
this.hands.right.position.x = 40;
this.hands.right.position.y = -8;
this.mesh.add(this.hands.left);
this.mesh.add(this.hands.right);
this.feet = {
left: new THREE.Mesh(foot, material),
right: new THREE.Mesh(foot, material)
};
this.feet.left.position.x = -20;
this.feet.left.position.y = -48;
this.feet.left.rotation.y = Math.PI / 4;
this.feet.right.position.x = 20;
this.feet.right.position.y = -48;
this.feet.right.rotation.y = Math.PI / 4;
this.mesh.add(this.feet.left);
this.mesh.add(this.feet.right);
this.nose = new THREE.Mesh(nose, material);
this.nose.position.y = 0;
this.nose.position.z = 32;
this.mesh.add(this.nose);
this.direction = new THREE.Vector3(0, 0, 0);
this.step = 0;
},
setDirection: function (controls) {
'use strict';
var x = controls.left ? 1 : controls.right ? -1 : 0,
y = 0,
z = controls.up ? 1 : controls.down ? -1 : 0;
this.direction.set(x, y, z);
},
motion: function () {
'use strict';
if (this.direction.x !== 0 || this.direction.z !== 0) {
this.rotate();
if (this.collide()) {
return false;
}
this.move();
return true;
}
},
rotate: function () {
'use strict';
var angle = Math.atan2(this.direction.x, this.direction.z),
difference = angle - this.mesh.rotation.y;
if (Math.abs(difference) > Math.PI) {
if (difference > 0) {
this.mesh.rotation.y += 2 * Math.PI;
} else {
this.mesh.rotation.y -= 2 * Math.PI;
}
difference = angle - this.mesh.rotation.y;
}
if (difference !== 0) {
this.mesh.rotation.y += difference / 4;
}
},
move: function () {
'use strict';
this.mesh.position.x += this.direction.x * ((this.direction.z === 0) ? 4 : Math.sqrt(8));
this.mesh.position.z += this.direction.z * ((this.direction.x === 0) ? 4 : Math.sqrt(8));
this.step += 1 / 4;
this.feet.left.position.setZ(Math.sin(this.step) * 16);
this.feet.right.position.setZ(Math.cos(this.step + (Math.PI / 2)) * 16);
this.hands.left.position.setZ(Math.cos(this.step + (Math.PI / 2)) * 8);
this.hands.right.position.setZ(Math.sin(this.step) * 8);
},
collide: function () {
'use strict';
return false;
}
});
I started to look at Cannon.JS to deal with the collisions, but it will have to wait for another post.
Now let’s call our main object and start the animated rendering.
basicScene = new BasicScene();
function animate () {
requestAnimationFrame(animate);
basicScene.frame();
}
animate();
And here we are !
Conclusion
Isn’t it surprising how easy it became to set up in-browser 3D ? Thanks Mr.doob for Three.JS !
This is only an experiment on a basic set up, but I would like to upgrade it every now and then. The next step will be the collisions detection.
Have fun fellows, I hope I’ll see you around again !