August 2015

Volume 30 Number 8

Game Development - Introduction to 3D for Web Games

By Michael Oneppo | August 2015

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.

A Simple 3D Scene
Figure 1 A Simple 3D Scene

In this scene, the Z axis recedes upward and backward. If I wanted to actually view it on the screen, I could just drop the Z information from every object as a simple and valid way to project the 3D scene, as shown in Figure 2.

A Squashed 3D Scene
Figure 2 A Squashed 3D Scene

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:

<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:

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:

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:

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):

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:

camera.position.z = 50;
renderer.render(scene, camera);

Materials and Lights

Next, I’ll place the ball in the arena in which it will bounce:

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:

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:

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.

Figure 3 The Arena Object

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:

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:

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.

Figure 4 The Ball Object’s Update Function

// 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:

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