Workbook 6, Page 2: Elements of 3D Graphics Programming

Workbook 6, Page 2: Elements of 3D Graphics Programming

On this page, we'll try to understand what those programs on the prior page actually did. This page is somewhat redundant with all of the THREE.js tutorials out there, but I am going to try to organize it to emphasize how the things that we need to do in the API will connect with the graphics concepts we'll learn about in class (and exist no matter what the API is).

Warning: In order to make THREE work nicely with Visual Studio Code's TypeScript type checker, I have to use a different name for the THREE library (this is discussed in Box 2 of page 1). For this workbook, I am using T since it is short to type. So when you read the code or in the snippets on this page, you'll see T.method rather than THREE.method.

Box 1: Elements of 3D Drawing

THREE.js is a scene-graph API, like SVG. We create pictures by creating "scenes" with graphics objects in them. We animate those scenes by altering the objects.

To draw in 3D with a scene graph API we need to have the following pieces in place:

  1. We need to create a space on the screen that we can draw into. In THREE, this is called a renderer. The renderer not only contains a Canvas element (to give us a rectangle on the web page), it also keeps track of all the information about how drawing will happen.
  2. We need to define the transformation between the 3D space of the world and the 2D space of the Canvas we're drawing into. In THREE this is called a camera. The main thing the camera does is provide a transformation between the 3D coordinates of the world and the 2D coordinates of the Canvas. In graphics, this is sometimes called the Viewing Transformation.
  3. We need to have a scene in which we store graphics objects. THREE calls this the scene.
  4. We need to create graphics objects. THREE calls these Objects. Almost always, the objects we draw are collections of triangles. Collections of triangles are called Meshes. In THREE, there is a notion of the definition of the shape of the triangles, called a Geometry which we then use in a Mesh object.
  5. We need to know what the appearance of the object is. In graphics, we often describe appearance as the material that the object is made out of. In THREE, they call these Materials as well.
  6. Since most kinds of materials can only be seen if there is a light source shining on them, we need to place lights in our scene - otherwise we'll just have a dark image!

So, basically, in order to do anything, we need to be able to do those six steps. THREE has nice abstractions for all of them. Different APIs may do things differently, but all six of those pieces need to be in place.

Box 2: The most basic drawing

OK, here's the simplest picture I can make:

Here's the code (it's in function box2 in 2-3D.js):

  // create the window that we want to draw into - this will
  // create a Canvas element - we'll set it to be
  let renderer = new T.WebGLRenderer();
  // put the canvas into the DOM

  // make a "scene" - a world to put the box into
  let scene = new T.Scene();

  // This transforms the world to the view
  // in this case a simple scaling
  let camera = new T.OrthographicCamera(-2,2, -2,2, -2,2);

  // we are going to make our box out of green "stuff"
  // this green stuff shows up as green even if there is no lighting
  var material = new T.MeshBasicMaterial( { color: 0x00ff00 } );

  // make a box - note that we make the geometry (a collection of triangles)
  // and then make a mesh object out of that geometry - which attaches the
  // triangles to a material
  let geometry = new T.BoxGeometry(1,1,1);
  let mesh = new T.Mesh(geometry, material);

  // now we need to put that box into the world

  // now we just need to draw the scene with the camera
  renderer.render( scene, camera );

In this picture, we use a BasicMaterial which shows up as its given color, ignoring any lighting.

The viewing transform is set by OrthographicCamera which basically scales the X and Y axis to fit the Canvas. We set the range of x from -2 to 2 and y from -2 to 2. The center is at the center of the window (since we go from -2 to 2), and y is up (-2 is top). We also map the range of z from -2 to 2. The negative Z axis goes into the screen (so we have a right handed coordinate system).

The box geometry has sides of length 1. By default, the box is placed with its center at the origin.

The mesh attaches the geometry to a material, making an object, that we place into the scene.

The renderer.render call draws the scene using the camera.

Box 3: Is this really 3D?

I could have drawn the same square using the 2D canvas. How can I convince you this is really 3D?

In this picture, I am going to make another box - this time out of yellow stuff, and put the box behind (and a little to the right) of the green box.

The code I added was:

    let yellowStuff = new T.MeshBasicMaterial( { color: 0xffff00 } );
    let mesh2 = new T.Mesh(geometry,yellowStuff);
    mesh2.position.x = 0.2;
    mesh2.position.z = -1;

You can see the whole thing in function box2 in 2-3D.js. Some things to notice here:

  1. I made a yellow material to use (in addition to the green material).
  2. I made a second object (mesh), but I used the same geometry as the first box.
  3. I set the object's position in order to translate it. Objects have basic transformations built in.
  4. I translated the new object 1 unit along the -z axis (z=-1) - remember that the negative Z axis goes into the screen (or the positive Z axis comes out of the screen). That way the yellow box is behind the green one.
  5. Objects in front block out objects in back. This is called visibility testing. Later, we'll learn about the Z-buffer algorithm that is being used to do this.

OK, to be sure that it really is doing visibility, and not just showing the last object added, change the z position of the yellow cube to have value z=1 so it is in front of the green object.

Hopefully, you noticed that THREE objects have their own transforms that allow us to rotate, translate and scale them. If you look at the documentation for Object3D (which is the base class of objects), you will see that objects have a matrix inside of them (actually multiple matrices, for reasons we'll learn about later). These matrices get built from the transformation data (e.g., position, scale, and rotation), so generally we don't deal with the matrices directly.

Box 4: Who turned the lights off?

In the previous boxes, we used a BasicMaterial which just shows up as a color. It doesn't respond to light. In the real world, different parts of the object get different amounts of light, so they appear different colors/brightnesses. This is one way that we can interpret 3D shape.

If we switch to using StandardMaterial rather than BasicMaterial, we will get lighting effects. However if we try it, we'll first just get a black Canvas:

Because we don't have any lights on, and we can't see. This is a mistake that all graphics programmers make at some point. So here I'll add not one, but two lights:

The lights are just like other objects, they get added to the scene.

    let ambientLight = new T.AmbientLight ( 0xffffff, 0.5);
    let pointLight = new T.PointLight( 0xffffff, 1 );
    pointLight.position.set( 0,-20,-10 );
    scene.add( pointLight );

First, I made an AmbientLight with color 0xffffff (white) and intensity .5 - an ambient light source is a special kind of light that shines in all directions equally. It's useful because it will light everything.

The second light is a PointLight, also with the white color. A point light is like a light bulb. The rays of light shine outwards from a particular location. I use the object's position to move it into the right place (just as I can move the cubes).

We'll learn more about lights later.

One of the things that is interesting about THREE is that all objects - whether they are graphics objects (Meshes), lights and cameras are treated the same way.

Box 5: Getting a better View

Until now, the transform for mapping the world onto the screen just flattened things on the Z axis. Instead, we might want to look at our world from a little bit higher up, so we can look down on the two cubes in order to see their shape better. We also will want a more "regular" camera that has perspective (things that are far away look smaller).

Here we changed the camera to:

    let camera = new T.PerspectiveCamera(50,1);

You can see I made a PerspectiveCamera (the normal kind of camera). I positioned the camera at (3,5,5) (up and to the right of the cubes, and still in front of them). The lookat function set the rotation of the object (in this case the camera) so that the Z axis points towards a given point. In this case, I point the camera at the origin, since that's where the green cube is centered.

We'll learn a lot more about cameras later.

Box 6: Spinning

In order to animate things, we need to create an animation loop (like we did with Canvas), and redraw the image each time (using renderer.render). We don't need to re-create the objects: we just need to move them a little. Here's an example:

The code (in the box6 function) should look like the animation loop code you saw for 2D Canvas programming, except that rather than redrawing all of the objects, we simply change the ones we want to move and then use renderer.render to redraw everything.

Note the line mesh1.rotateY(0.02). This line updates the transformation of the mesh1 object (the green cube), by post-multiplying it with a small rotation about the Y axis (vertical). The rotation gets added to the previous rotation (we never reset the objects transformation back to the start).

There was another addition in this version: try dragging with the left mouse button in the window. You'll see the camera "orbits" around the center, letting you look around. This is called an OrbitControl and is provided by THREE. The controls themselves aren't the easiest to use, but they are simple and do let you look around (getting this right is hard). You can also use the middle and right mouse buttons to control the camera (middle zooms, right translates).

There are a few catches to using THREE's orbit controls (you can see the documentation):

  1. You must include the OrbitControls.js file from your HTML after you load THREE. It adds itself to the THREE "namespace."
  2. Your program must run an animation loop - the OrbitControl object updates the camera based on the mouse movements, but doesn't cause redraws to happen. There are ways around this, but the animation loop is the easier way.
  3. You need to set up the OrbitControl after you make your camera and renderer.

Summary: The Basics of THREE

This should give you the basic ideas of how we make things with THREE. You might want to look at a THREE tutorial which will show off more stuff, or start to look at the THREE documentation. Or, you can wait till the next pages which will give more details.

The next page will talk more about 3D Worlds and THREE's scenes.