Game Development - Introduction To 3D For Web Games: The Illusion of 3D
Game Development - Introduction To 3D For Web Games: The Illusion of 3D
Volume 30 Number 8
Adding a third dimension to a game truly brings it to life. You can look around from any
viewpoint and see every angle of an object or a scene. But how can you actually pull this off
behind the scenes? In this series of articles, I'll walk through the steps for making 3D games,
and show you how libraries such as three.js can help you achieve the rich, 3D environment
becoming so popular on the Web. In this first installment, I'll keep it simple and focus on
building a 3D version of the Ping game first described in "A Web Game in an Hour".
The Illusion of 3D
Any 3D graphics rendering has a surprising trick up its sleeve. Humans can’t really see in 3D
dimensions—especially on a computer monitor. The entire goal of 3D drawing is to generate,
or render, a 3D description of a scene onto a 2D image. When you add a third dimension to get
more immersive and realistic scenes, you have to throw away some data to get an image from a
specific viewpoint. This concept is called projection. It’s an essential element of what makes
3D graphics work, as shown in the basic 3D scene in Figure 1.
As you can see, this isn’t exactly Halo. For photorealism, a 3D scene requires three things—a
proper camera projection, geometry and shading. I’ll cover each of these concepts as I rebuild
the Ping game as a 3D dueling game.
Getting Started
First, I’ll set up the three.js library. This is a fairly quick configuration, as almost everything
you do with three.js happens in JavaScript. Here’s the HTML code you’ll need:
JavaScript
<html>
<head>
<title>Ping!</title>
<script src=
"//cdnjs.cloudflare.com/ajax/libs/three.js/r69/three.min.js"></script>
<script src="ping3d.js"></script>
</head>
<body>
</body>
</html>
In the JavaScript file ping3d.js, I’m going to set up three.js to render a simple scene. First, I
need to initialize three.js and add its drawing canvas to the page:
JavaScript
var scene = new THREE.Scene();
var renderer = new THREE.WebGLRenderer();
renderer.setSize( window.innerWidth, window.innerHeight );
document.body.appendChild( renderer.domElement );
The scene is exactly what it sounds like—an object that describes our scene and all the objects
within. The renderer is also obviously named. When given a scene, the renderer will draw it to
the screen. This should look similar to some of the 2D drawing systems I described in previous
articles, “A Web Game in an Hour,” “2D Drawing Techniques and Libraries for Web Games”
(msdn.microsoft.com/magazine/dn948109) and “2D Game Engines for the Web”
(msdn.microsoft.com/magazine/dn973016). Now I need to add some elements to the screen.
Geometry
Almost all 3D graphics are built out of polygons. Even curved surfaces like a ball are
approximated into triangular facets to approximate its surface. When assembled, these triangles
are called a mesh. Here’s how I add the ball to the scene:
JavaScript
var geometry = new THREE.SphereGeometry(10);
var material = new THREE.BasicMaterial({color: 0xFF0000});
var mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
This code will create a large number of triangles representing a sphere (the “geometry”
variable), a simple bright red material (the “material”) and a mesh object (the “mesh”). Then it
will add the mesh to the scene.
The triangle is the fundamental building block of 3D graphics. Why is this? I’ll investigate this
further in the next article in this series, but the two main reasons are the straight lines that make
up a triangle are easy to work with and you can’t break a triangle into a more basic planar
surface. The graphics processing unit (GPU) on your computer or phone has dedicated
hardware that can quickly convert shapes with straight lines into pixels. This is a good part of
what makes high-quality 3D graphics possible.
Modeling
I can pass any geometry into the three.Mesh constructor. This includes generated geometry to
make custom shapes or even data from files. For the Ping game, I’d like to have 3D models of
each of the players. Therefore, I’ve taken the liberty of creating geometry in a 3D modeling
program for this exercise. It’s surprisingly easy to use the model instead of a sphere, as three.js
provides a loading tool for this very purpose:
JavaScript
var jsonLoader = new THREE.JSONLoader();
jsonLoader.load('tank1.json', function (geometry) {
var material = new THREE.BasicMaterial({color: 0xFF0000});
var mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
});
Camera
The camera represents the viewpoint of the scene. It stores both the position and angle of the
viewer within the game. More important, the camera represents how the scene becomes
flattened, as described at the beginning of this article.
In my example, the camera was positioned down and to the right. The final image appeared as
the view from that direction. However, using this projection method, no matter how far away
objects are, they will stay the same size in the final image. This is called an orthographic
projection. This is often useful for games with non-realistic view angles like city simulation
games. What I really want to achieve is to make objects appear smaller as they recede into the
distance.
Enter perspective projection: Perspective projection imagines the field of view of a camera as a
pyramid extending from the lens. When positions are mapped to the screen, they’re computed
based on their relative distances to the sides of the pyramid. Using this model, as objects recede
into the distance, they appear to shrink as in real life.
Thankfully, you don’t need to do this mapping yourself because three.js does it for you and
provides an object that represents the camera in the scene (and adding another is simple):
JavaScript
var camera = new THREE.PerspectiveCamera(
75, window.innerWidth/window.innerHeight, 0.1, 1000 );
The first argument is the field of view, which indicates how much of an angular distance to take
in horizontally. The second argument is the ratio of screen width to height, which you need to
ensure things aren’t squished because the screen isn’t square. The final two parameters define
the closest and farthest distances to show. Anything closer or farther than those values isn't
drawn. Now I’m at the point where I can actually draw the scene. Let’s move the camera back a
little to view the whole scene and begin drawing:
JavaScript
camera.position.z = 50;
renderer.render(scene, camera);
JavaScript
var room = new THREE.BoxGeometry( 50, 30, 100 );
var material = new THREE.MeshPhongMaterial({
side: THREE.BackSide,
map: THREE.ImageUtils.loadTexture('arena.png')
});
var model = new THREE.Mesh(room, material);
model.position.y = 15;
scene.add(model);
I’m doing something different than just making box geometry. I’m also making a material. A
material is a definition of how something should reflect light in a scene. This generates its
overall appearance. In this case, I’m making a Phong material, which is a good default for
shiny objects. I’m also adding a texture to the box, which is simple in three.js using the
loadTexture function.
One other notable aspect of this code is the line that reads: side: THREE.BackSide. This
instructs three.js to draw only the interior sides of the box surfaces, rather than exterior sides.
This gives room for the ball to bounce, instead of having a solid box floating in space.
If I were to draw the scene now, the arena wouldn’t be visible. It would just draw black. This is
because materials define how light reflects off objects, and I do not yet have light in the scene.
Three.js makes adding light to a scene simple, as shown here:
JavaScript
this.lights = [];
this.lights[0] = new THREE.PointLight( 0x888888, 1, 300 );
this.lights[0].position.set( 0, 10, 40 );
scene.add( this.lights[0] );
this.lights[1] = new THREE.PointLight( 0x888888, 1, 300 );
this.lights[1].position.set( 0, 20, -40 );
scene.add( this.lights[1] );
Now if I draw the scene, the arena will render properly. To make a better view, I’ll set the
camera position to look in the side of the arena before running the code:
JavaScript
camera.up.copy(new THREE.Vector3(0,1,0));
camera.position.copy(new THREE.Vector3(0,17, -80));
camera.lookAt(new THREE.Vector3(0,0,40));
The first line sets the up variable, which simply tells the camera which way is up. The lookAt
function does exactly as it sounds—it points the camera at the specified position.
Making a 3D Game
Now that the game has moved into three dimensions, making the rest should be pretty easy.
However, this game is going to end up a bit more verbose than previous implementations
because it’s composed of 3D instead of 2D objects. So I’ll break up the code into separate files
to make the additional code easier to handle.
I’ll also shift JavaScript styles for object definition to the more traditional constructor model.
To demonstrate this, I’ve wrapped the arena box and lights into an object and placed that in a
single file, as shown in Figure 3.
JavaScript
function Arena(scene) {
var room = new THREE.BoxGeometry( 50, 30, 100 );
var material = new THREE.MeshPhongMaterial({
side: THREE.BackSide,
map: THREE.ImageUtils.loadTexture('arena.png')
});
var model = new THREE.Mesh(room, material);
model.position.y = 15;
scene.add(model);
this.lights = [];
this.lights[0]= new THREE.PointLight( 0x888888, 1, 300 );
this.lights[0].position.set( 0, 10, 40 );
scene.add( this.lights[0] );
this.lights[1]= new THREE.PointLight( 0x888888, 1, 300 );
this.lights[1].position.set( 0, 20, -40 );
scene.add( this.lights[1] );
}
If I want to create an Arena, I can create a new object using this constructor function:
JavaScript
var arena = new Arena(scene);
Next, I’ll make a ball object that can bounce around the arena. I know how to make a red ball in
three.js, so I’ll wrap that code into an object, as well:
JavaScript
function Ball(scene) {
var mesh = new THREE.SphereGeometry(1.5, 10, 10);
var material = new THREE.MeshPhongMaterial({
color: 0xff0000,
specular: 0x333333
});
var _model = new THREE.Mesh(mesh, material);
_model.position.y = 10;
scene.add(_model);
}
Now I’ll define the basic physics of bouncing the ball by adding a function to the ball object, as
shown in Figure 4.
JavaScript
// Create a private class variable and set it to some initial value.
var _velocity = new THREE.Vector3(40,0,40);
this.update = function(t) {
// Apply a little gravity to the ball.
_velocity.y -= 25 * t;
// Move the ball according to its velocity
var offset = _velocity.clone()
.multiplyScalar(t);
_model.position.add(offset);
// Now bounce it off the walls and the floor.
// Ignore the ends of the arena.
if (_model.position.y - 1.5 <= 0) {
_model.position.y = 1.5;
_velocity.y *= -1;
}
if (_model.position.x - 1.5 <= -25) {
_model.position.x = -23.5;
_velocity.x *= -1;
}
if (_model.position.x + 1.5 >= 25) {
_model.position.x = 23.5;
_velocity.x *= -1;
}
}
Three.js requires that you render the scene every time using requestAnimationFrame. This
should be a familiar pattern:
JavaScript
var ball = new Ball(scene);
var Arena = new Arena(scene);
var render = function (time) {
var t = (time - lastTime) / 1000.0;
lastTime = time;
ball.update(t);
renderer.render(scene, camera);
requestAnimationFrame( render );
}
requestAnimationFrame(render);
Stay Tuned
Now I have an arena with lights, a well-positioned camera and a ball bouncing around the
scene. That’s all I’m going to cover in this article. In the next installment, I’ll explain how 3D
projection works by letting you aim with the mouse. I’ll also explain more about textures and
make smooth animations using a powerful library called tween.js. In the last of these three
articles, I’ll look under the hood of three.js and see how it’s actually drawing such high-fidelity
graphics.
Michael Oneppo is a creative technologist and former program manager at Microsoft on the
Direct3D team. His recent endeavors include working as CTO at the technology nonprofit
Library for All and exploring a master’s degree at the NYU Interactive Telecommunications
Program.
Thanks to the following technical expert for reviewing this article: Mohamed Ameen Ibrahim