WebGL and Three.js: Texture Mapping

Written by Greg Stier
on November 19, 2013

Three.js

Three.js is a lightweight 3D library that hides a lot of the WebGL complexities and makes it very simple to get started with 3D programming on the web.

Three.js can be downloaded from github or the three.js website, where you will also find links to documentation and examples.

In my previous tutorial, I showed you how to setup a basic Three.js application. In that tutorial I talked about setting the stage and building a scene for our 3D application. My scene was very simple, a blue spinning cube. In this tutorial, I’m going to explain texture mapping and show you how to texture objects using Three.js.

To view this demonstration, you will need a WebGL compatible browser. If you have a recent version of Chrome, Firefox, Safari or Internet Explorer, you should be good to go. If you are running an older version of Internet Explorer, you may be out of luck.

You can view the result of this tutorial here, and the source code can be found here.

What is Texture Mapping?

Texture mapping is a method for adding detail to a 3D object by applying an image to one or more of the faces of that object. This allows us to add fine detail without the need to model those details into our 3D object, thus keeping the polygon count down. This can be a huge performance booster when rendering our models.

Getting Started

In this tutorial I’m going to begin with the same simple HTML file that I used in my previous tutorial:

 

<!DOCTYPE html>
<html>
<head>
<title>WebGL/Three.js Step Tutorial</title>
<style>
body {
margin: 0px;
background-color: #fff;
overflow: hidden;
}
</style>
</head>
<body>
<script src="js/three.min.js"></script>
<script src="js/three-tut.js"></script>
</body>
</html>

The HTML assumes that you have downloaded the minified three.js library and saved it in a folder named ‘js’. I’ve also created my application, three-texture-tut.js, and saved it in the same location. Again, I’m going to start with similar javascript from my previous tutorial:

var camera;
var scene;
var renderer;
var mesh;

init();
animate();

function init() {

scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera( 70, window.innerWidth / window.innerHeight, 1, 1000);

var light = new THREE.DirectionalLight( 0xffffff );
light.position.set( 0, 1, 1 ).normalize();
scene.add(light);

var geometry = new THREE.CubeGeometry( 10, 10, 10);
var material = new THREE.MeshPhongMaterial( { ambient: 0x050505, color: 0x0033ff, specular: 0x555555, shininess: 30 } );

mesh = new THREE.Mesh(geometry, material );
mesh.position.z = -50;
scene.add( mesh );

renderer = new THREE.WebGLRenderer();
renderer.setSize( window.innerWidth, window.innerHeight );
document.body.appendChild( renderer.domElement );

window.addEventListener( 'resize', onWindowResize, false );

render();
}

function animate() {
mesh.rotation.x += .04;
mesh.rotation.y += .02;

render();
requestAnimationFrame( animate );
}

function render() {
renderer.render( scene, camera );
}

function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize( window.innerWidth, window.innerHeight );
render();
}

If you run your application now, you should see a spinning blue cube like the one pictured below.

What we are going to do is make our boring blue cube look like a cool, game ready, low poly crate similar to the one pictured below.

In order to do that we need to do a couple of things. First we need to create a new folder called images. This folder should be at the same level as your js folder. Next, click on the following image, save it to your new images folder and name it crate.jpg.

Once you have the image saved in the correct location, we need to change our application to load the image and use this image as the texture for our cube. Open the three-texture-tut.js file and replace the following line:

var material = new THREE.MeshPhongMaterial( { ambient: 0x050505, color: 0x0033ff, specular: 0x555555, shininess: 30 } );

with:

var material = new THREE.MeshPhongMaterial( { map: THREE.ImageUtils.loadTexture('images/crate.jpg') } );

And that’s it. Refresh your browser and you will see a spinning crate instead of a plain blue cube. So what happened here? You will notice that while constructing our material, we specified the map property and set its value to the crate image. Three.js then took that texture and applied it to each face of our cube for us.

This is really cool, but what if we want a different texture for each face of the cube? Fortunately Three.js provides a couple of ways for us to do this. The first method to do this is to simply give Three.js a list of textures, one for each face. To do this we’re going to create 6 new materials, each with a different texture.

So, the first thing we need to do is have six different textures. Click on the following images and save them to your images folder with these names: bricks.jpg, clouds.jpg, stone-wall.jpg, water.jpg, and wood-floor.jpg.

After you have saved the images replace the following line:

var material = new THREE.MeshPhongMaterial( { map: THREE.ImageUtils.loadTexture('images/crate.jpg') } );

with:

var material1 = new THREE.MeshPhongMaterial( { map: THREE.ImageUtils.loadTexture('images/crate.jpg') } ); var material2 = new THREE.MeshPhongMaterial( { map: THREE.ImageUtils.loadTexture('images/bricks.jpg') } ); var material3 = new THREE.MeshPhongMaterial( { map: THREE.ImageUtils.loadTexture('images/clouds.jpg') } ); var material4 = new THREE.MeshPhongMaterial( { map: THREE.ImageUtils.loadTexture('images/stone-wall.jpg') } ); var material5 = new THREE.MeshPhongMaterial( { map: THREE.ImageUtils.loadTexture('images/water.jpg') } ); var material6 = new THREE.MeshPhongMaterial( { map: THREE.ImageUtils.loadTexture('images/wood-floor.jpg') } ); var materials = [material1, material2, material3, material4, material5, material6]; var meshFaceMaterial = new THREE.MeshFaceMaterial( materials );

What we just did with the lines above was to create an array of materials, each material having a different texture. We then use this array of materials to create a MeshFaceMaterial.

Finally, we need to tell our geometry to use this array of materials. Change the following line:

mesh = new THREE.Mesh(geometry, material );

to:

mesh = new THREE.Mesh(geometry,  meshFaceMaterial);

Refresh your browser. As you watch the cube spin you will see a different texture for each face of the cube. Again, this is very cool, but creating and loading an image for every face of our model becomes very impractical as the face count increases in our 3D models. This leads to the final method used for texture mapping called UV mapping.

UV Mapping

UV mapping is the process of taking an image and assigning parts of that image to individual faces of our 3D object. To see UV mapping in action, save the following image to the images folder and name it texture-atlas.jpg.

Once you have the image saved, we are going to change the following lines: 

var material1 = new THREE.MeshPhongMaterial( { map: THREE.ImageUtils.loadTexture('images/crate.jpg') } );
var material2 = new THREE.MeshPhongMaterial( { map: THREE.ImageUtils.loadTexture('images/bricks.jpg') } );
var material3 = new THREE.MeshPhongMaterial( { map: THREE.ImageUtils.loadTexture('images/clouds.jpg') } );
var material4 = new THREE.MeshPhongMaterial( { map: THREE.ImageUtils.loadTexture('images/stone-wall.jpg') } );
var material5 = new THREE.MeshPhongMaterial( { map: THREE.ImageUtils.loadTexture('images/water.jpg') } );
var material6 = new THREE.MeshPhongMaterial( { map: THREE.ImageUtils.loadTexture('images/wood-floor.jpg') } );

 to:

var material = new THREE.MeshPhongMaterial( { map: THREE.ImageUtils.loadTexture('images/texture-atlas.jpg') } );

As you can see, we’ve gone back to creating one material and loading one texture. Next we need to assign different parts of our image to individual faces of our cube.

The first thing I’m going to do is define the sub images in our texture. Right after creating the material we are going to add the following lines: 

var bricks = [new THREE.Vector2(0, .666), new THREE.Vector2(.5, .666), new THREE.Vector2(.5, 1), new THREE.Vector2(0, 1)];
var clouds = [new THREE.Vector2(.5, .666), new THREE.Vector2(1, .666), new THREE.Vector2(1, 1), new THREE.Vector2(.5, 1)];
var crate = [new THREE.Vector2(0, .333), new THREE.Vector2(.5, .333), new THREE.Vector2(.5, .666), new THREE.Vector2(0, .666)];
var stone = [new THREE.Vector2(.5, .333), new THREE.Vector2(1, .333), new THREE.Vector2(1, .666), new THREE.Vector2(.5, .666)];
var water = [new THREE.Vector2(0, 0), new THREE.Vector2(.5, 0), new THREE.Vector2(.5, .333), new THREE.Vector2(0, .333)];
var wood = [new THREE.Vector2(.5, 0), new THREE.Vector2(1, 0), new THREE.Vector2(1, .333), new THREE.Vector2(.5, .333)];

The code above creates six arrays, one for each sub image in the texture. Each array contains 4 points that define the bounds of the sub image. The values of the coordinates range from 0-1 where 0,0 is the bottom left corner and 1,1 is the upper right corner.

The coordinate system is in terms of percentage of the texture. Let’s take a look at the array representing the bricks sub image in more detail to help clarify what is happening here.

var bricks = [
new THREE.Vector2(0, .666),
new THREE.Vector2(.5, .666),
new THREE.Vector2(.5, 1),
new THREE.Vector2(0, 1)
];

The bricks sub image is in the upper left corner of our texture, so the coordinates for it are as follows in a counter clockwise direction starting with the lower left corner of the sub image.

Lower left corner:
0 – The left most edge
.666 – Two thirds of the way up from the bottom

Lower right corner:
.5 – Half way across
.666 – Two thirds of the way up from the bottom

Upper right corner:
.5 – Half way across
1 – The top of the texture

Upper left corner
0 – The left most edge
1 – The top of the texture

Now that we’ve defined the sub images in our texture we can begin applying them to the faces of our cube. The first thing we’re going to do is add the following line:

geometry.faceVertexUvs[0] = [];

This clears out any UV mapping that may have already existed on the cube.

Next we’re going to add the following lines of code:

geometry.faceVertexUvs[0][0] = [ bricks[0], bricks[1], bricks[3] ];
geometry.faceVertexUvs[0][1] = [ bricks[1], bricks[2], bricks[3] ];

geometry.faceVertexUvs[0][2] = [ clouds[0], clouds[1], clouds[3] ];
geometry.faceVertexUvs[0][3] = [ clouds[1], clouds[2], clouds[3] ];

geometry.faceVertexUvs[0][4] = [ crate[0], crate[1], crate[3] ];
geometry.faceVertexUvs[0][5] = [ crate[1], crate[2], crate[3] ];

geometry.faceVertexUvs[0][6] = [ stone[0], stone[1], stone[3] ];
geometry.faceVertexUvs[0][7] = [ stone[1], stone[2], stone[3] ];

geometry.faceVertexUvs[0][8] = [ water[0], water[1], water[3] ];
geometry.faceVertexUvs[0][9] = [ water[1], water[2], water[3] ];

geometry.faceVertexUvs[0][10] = [ wood[0], wood[1], wood[3] ];
geometry.faceVertexUvs[0][11] = [ wood[1], wood[2], wood[3] ];

The faceVertexUvs property of geometry is an array of arrays that contains the coordinate mapping for each face of the geometry. Since we are mapping to a cube you may be wondering why there are 12 faces in the array. The reason is that each face of the cube is actually created from 2 triangles. So we must map each triangle individually. In the scenario above where we simply handed Three.js a single material, it automatically broke our texture down into triangles and mapped it to each face for us.

The order in which you specify the coordinates for each face matters, they must be defined in a counter clockwise direction. Our sub image arrays were created in a counter clockwise direction, so looking at the image below we can see that in order to map the bottom triangle we need to use the vertices at index 0, 1, and 3. And to map the top triangle we need to use the vertices at index 1, 2, and 3.

Finally, we replace the following lines:

var meshFaceMaterial = new THREE.MeshFaceMaterial( materials );
mesh = new THREE.Mesh(geometry, meshFaceMaterial);

with:

mesh = new THREE.Mesh(geometry, material);

Now if you refresh your browser you should see that each face of the cube is mapped to a different part of our texture.

As you can see, Three.js gives us a nice range of possibilities for texturing our 3D models from simply handing it an image, or a series of images, and having it do the mapping for us, to the more advanced UV mapping which allows us to target specific portions of our texture.

Be sure to check out Part 3 here: WebGL and Three.js: Lighting.

Image courtesy of Unsplash.