Page 2: Elements of 3D Graphics Programming

CS559 Spring 2023 Sample Solution

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 we 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 that exist no matter what the API is).

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” data structure 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. In THREE, there is are two different data structures we must consider. A Geometry is just a collection of triangles - it represents a shape. A Mesh is a “graphics object” that can be drawn: it combines a geometry with a material and other properties like transformations. This terminology is bit non-standard: usually in computer graphics, we use the term “Mesh” to mean a connected collection of triangles.
  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 1: The most basic drawing

Here’s the simplest picture we can make:

06-02-01

This might look like it’s just a square, but it’s really a cube that we’re viewing from one side. The cube is solid green (it has no lighting).

Let’s walk through the code in 06-02-01.js ( 06-02-01.html). T is our THREE.js import, and we’re skipping most of the lines with comments:

11
12
13
14
let renderer = new T.WebGLRenderer();
renderer.setSize(200, 200);
// put the canvas into the DOM
document.getElementById("div1").appendChild(renderer.domElement);

In these lines, we create the renderer, which will create a canvas for us. We set the size, and then add the canvas to our DOM. Our HTML had a <div> that we put the canvas inside of.

Next we create our “world” - an empty Scene to put things in:

19
let scene = new T.Scene();

The next line we create a “camera” to take a picture of our world. The camera is a “viewing transformation” that transforms points in the world to locations on the screen:

25
let camera = new T.OrthographicCamera(-2, 2, -2, 2, -2, 2);

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

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

31
let material = new T.MeshBasicMaterial({ color: 0x00ff00 });

Next, we need to make our cube. First we define a geometry (the collection of triangles that make up the cube). 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:

38
39
let geometry = new T.BoxGeometry(1, 1, 1);
let mesh = new T.Mesh(geometry, material);

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

47
renderer.render(scene, camera);

Box 2: Is this really 3D?

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

In this picture ( 06-02-02.html and 06-02-02.js), we are 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.

06-02-02-solution

The code we added was:

37
38
39
40
41
let yellowStuff = new T.MeshBasicMaterial({ color: 0xffff00 });
let mesh2 = new T.Mesh(geometry, yellowStuff);
mesh2.position.x = 0.2;
mesh2.position.z = -1;
scene.add(mesh2);

Some things to notice here:

  1. We made a yellow material to use (in addition to the green material).
  2. We made a second object (mesh), but used the same geometry as the first box.
  3. We set the object’s position in order to translate it. Objects have basic transformations built in.
  4. We 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 3: 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.

In 06-02-03.js ( 06-02-03.html), we switch from using BasicMaterial to using StandardMaterial, so we will get lighting effects. However, if we try it, we’ll first just get a black Canvas…

06-02-03

Box 4: Turning the lights on

…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 we’ll add not one, but two lights in 06-02-04.js ( 06-02-04.html):

06-02-04

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

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

First, we made an AmbientLight with color 0xffffff (white) and intensity 0.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. We use the object’s position to move it into the right place (just as we 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, or cameras - are treated the same way. Lights, cameras and meshes are all “Objects” that are added to the scene.

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

06-02-05

Here we changed the camera to:

18
19
20
let camera = new T.PerspectiveCamera(50, 1);
camera.position.set(3, 5, 5);
camera.lookAt(0, 0, 0);

You can see in 06-02-05.js ( 06-02-05.html) we made a PerspectiveCamera (the normal kind of camera). We positioned the camera at (3, 5, 5) (up and to the right of the cubes, and still in front of them). The lookat function sets the rotation of the object (in this case the camera) so that the Z axis points towards a given point. In this case, we 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 in 06-02-06.js ( 06-02-06.html):

06-02-06

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.8 * timeDelta). 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).

Box 7: Interaction

In the following box ( 06-02-07.js) 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).

06-02-07

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

  1. You must import OrbitControls from the OrbitControls.js file when loading THREE. It is a separate module and not part of 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 continue on to the next pages, which will give more details.

Page  3  (Making a Scene) will talk more about 3D Worlds and THREE’s scenes.

Next: Making a Scene

Page 2 Rubric (1 points total)
Points (1):
Box 06-02-02
1 pt
move the yellow cube