Page 4: Textures in THREE

CS559 Spring 2021 Sample Solution - Workbook 9

Written by CS559 course staff

You can try out the example solutions here. The interesting code is on page 5, page 6 and page 7.

Using textures is notoriously tricky because there are a lot of steps involved. Fortunately, THREE makes it remarkably easy - it takes care of all of the little details, allowing you to focus on the big parts (setting coordinates, defining the texture image).

If you want a sense of how much had to happen to get a cube with texture on it, have a look at the 2015 example! And this doesn’t do any lighting!

Fortunately, THREE takes care of all of this for us. And it gives us access to lots of fancier kinds of texture things as well.

The “ThreeJS Fundamentals” website has a nice tutorial on texture mapping that gets to some of the more advanced issues that we will get to later.

The Complete Example

We’re going to do this backwards. Here is a complete example program. It’s in 09-04-01.js. It’s the two triangles from Page  1  (Meshes: What you need to know), with an extra plane and a texture added. The plane is so you can see what the texture looks like.

We’ll explain how it works in the following, but you might want to have a look first.

Box 2: Texture Coordinates in THREE

Most THREE primitives have reasonable texture coordinates defined. For example, the demo in Box 1 has its u,v going from 0 to 1 over the course of a plane.

For user-defined geometries (of class Geometry), we have to provide texture coordinates ourselves. Just like with colors and normals, a vertex may be “split” so that it has a different texture coordinate for each face it is part of. However, unlike colors and normals, the texture coordinates are not part of the Face3 data structures.

Texture coordinates must be provided as an array of arrays of arrays of vectors. The faceVertexUvs property of a Geometry has type Vector2[][][]. It is triple nested because:

  • Each vertex needs a 2D coordinate (a Vector2)
  • Each face has 3 vertices
  • Each layer has an array of faces
  • Each geometry can have multiple layers of texture coordinates.

We won’t use layers now. So we’ll always make the top level be an array of length 1.

Here’s an example from the code:

    let f1 = new T.Face3(0, 1, 2);
    geometry.faceVertexUvs = [[]];
      new T.Vector2(0, 0),
      new T.Vector2(1, 0),
      new T.Vector2(0, 1)

The first line of this snippet creates the faceVertexUvs array with a single layer. For now, that layer is empty (it has no faces in it). The last line adds the texture coordinate for the particular triangle. Notice that it adds to the layer (faceVertexUvs[0]). What it adds is the information for the face: the coordinates for the three vertices of the triangle. Each coordinate is a Vector2, and the face is an array of three of them.

To see the nesting, let me write the code in multiple lines:

let vertex1 = new T.Vector2(0,0); // vertex 2 and 3 defined as well
let face1 = [vertex1, vertex2, vertex3]; // a face is 3 vertices
let layer = [face1]; // and more faces...
let geometry.faceVertexUvs = [layer]; // we only have one layer

Texture Image Loading in THREE

In order to have a texture image, we need to get the image from somewhere. Usually, this is from some image file.

Loading an image into a texture is tricky. We need to load the image. We need to wait until the image is loaded. We need to process the image and put it into a special format. We need to attach the image to its surface.

Fortunately, THREE takes care of all this for you with a texture loader. You say “load me a texture”, and it loads the texture in and sets things up once its loaded. A cool feature: before the texture is loaded, things still work (it just shows up as blank). Remember, this is JavaScript asynchronicity, so there really isn’t any multi-threading. You need to make sure you return back to the main event loop for loading to happen (remember how requestAnimationFrame works?).

The line of code is:

let t1 = new T.TextureLoader().load("../images/UV_Grid_Sm.jpg");

Texture Usage in THREE

Once a texture is created, we simply need to tell the material to use it in order to get the colors on the triangles. This is done by specifying a map parameter to a texture. So, in the example:

let t1 = new T.TextureLoader().load("../images/UV_Grid_Sm.jpg");
let m1 = new T.MeshBasicMaterial({ map: t1, side: T.DoubleSide });

We can specify other properties for the material. In the example, I specified roughness. You can even specify color, and this color will be combined with the colors from the texture. And of course, with a MeshStandardMaterial, the colors are affected by lighting as well. If you want to use textures but do not want the color affected by lighting, use a MeshBasicMaterial.

THREE allows us to control many properties of how textures are used. These will make more sense once we learn about how texturing works in class. For now, the default parameters will probably be fine.

Texture Re-Use

It’s so easy to load and use textures with THREE that you might forget how many you’ve loaded. In general, using lots of textures can be a problem: they take up a lot of memory.

For example, in the example code (for_students/09-04-01.js), the texture file /images/UV_Grid_Sm.jpg is loaded three times. A separate copy will be loaded for each instance of TwoTrianglesTextured and TwoTrianglesTextured2 that we create. Right now, there is just one of each (so that’s two copies of the texture - a third is used for the plane). If we made a lot of these objects, the memory usage would add up fast.

Therefore, when we use textures, we need to be careful to re-use things that we’ve loaded already and not load multiple copies. In fact, we might want to try to re-use the same texture over and over to save memory. Think about this when you make your own objects for future projects.

Summary: Textures in THREE

That was a quick summary of how textures are applied in THREE. On Page  5  (Dice and Dominos (Exercise 2 and 3)), you can try doing it yourself!

There are no points associated with this page.