Texture Mapping


Highlights of this lab:

This lab is an introduction to Texture Mapping. It is organized by the following sections:

Assignment:

After the lab lecture, you have until next week to:


A. Pictorial Overview and Definitions

The following diagram represents the idea of texture mapping:

Some definitions:


B. Coding Overview

Download
For the following discussion you may want to download Lab6Exercise.zip. In the lab, make sure you restart Chrome like this to be able to load local files:
open /Applications/Google\ Chrome.app --args --allow-file-access-from-files

The Steps in Texture Mapping are the following

  1. Create a texture name and bind it to the type of texture we will be creating
  2. Specify texture properties and the texture image
  3. Enable texture mapping
  4. Provide the mapping between texture coordinates and the object's coordinates.

B1. Texture names

To begin with, we need a texture name. Texture names are numbers that WebGL uses to identify internal texture objects. OpenGL and OpenGL ES use the glGenTextures() command to assign a name to a texture object. WebGL uses the similar createTexture command. It creates a texture name as if glGenTextures was called, and uses is to initialize a WebGLTexture object. The code to generate a new WebGLTexture object is as follows:

    var texName;
    //allocate a texture name
    texName = gl.createTexture();

Now that we have a texture name and a corresponding texture object, we want to specify what texture we are working with. There are two types of textures in WebGL: 2D and cube map. In this lab, we will be working with 2D textures, which is indicated by the gl.TEXTURE_2D in the following code:

    //select our current texture
    gl.bindTexture(gl.TEXTURE_2D, texName);

By issuing the gl.bindTexture() for the first time with texName, we are creating a texture object which is 2D and contains some initial default values or properties.

You will notice that we will call gl.bindTexture() again later with texName. When gl.bindTexture() is called with a previously created texture object, that texture object becomes active. Therefore, the next call to gl.bindTexture() will make texName the current texture. Once you bind a texture name as either 2D or cube map, you must always bind it as such.

B2. Texture Properties and the Image

We've got a texture name and we've created a texture with the gl.bindTexture() command, now we can specify some properties or parameters for the texture. If we don't specify any parameters, the default values will be used.

The following may be some parameters that we want to set. The comments provide some idea as to what each of these calls is doing. For more details about these parameters, you might want to consult the manual for texParameteri .

    //The texture wraps over the edges (repeats) in the x direction
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT);

    //The texture wraps over the edges (repeats) in the y direction
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT);

    //when the texture area is large, repeat texel nearest center
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);

    //when the texture area is small, repeat texel nearest center
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);

Next, we can specify the image we are going to use. Assuming that we have an image (checkImage) which is a 2 dimensional checkerboard pattern stored in an array containing RGBA components. We can set the texture to the checkImage with the following code:

gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, checkImageWidth, checkImageHeight, 
                             0, gl.RGBA, gl.UNSIGNED_BYTE, checkImage); 

See the online man page for texImage2D for function details.

B3. Switching Textures

While you are drawing, you need to specify which texture you are going to be working with. You can have multiple textures loaded into your Rendering Context. You switch between them by calling gl.bindTexture. Remember, the first time you call gl.bindTexture it creates a texture object with default values for the texture image and texture properties. You can then load a texture and configure it. The second time you call it with the texture name, that texture data becomes the current texture state.

    gl.bindTexture(GL_TEXTURE_2D, texName);

B4. Provide the Mapping between Texture Coordinates and the Object's Coordinates.

You have to specify which part of our texture image are going to fit or map onto the object. Texture coordinates are often referred to as s and t. For a texture image, s and t are in the range of 0 to 1. The mapping itself is similar to setting a color for each vertex. To map the checkerboard texture onto a square, we would provide vertex and texture coordinate arrays that would look like this:

var points = 
[
        //square
	-2.0, -1.0,  0.0,
	 0.0,  1.0,  0.0,
	-2.0,  1.0,  0.0,

	-2.0, -1.0,  0.0,
	 0.0, -1.0,  0.0,
	 0.0,  1.0,  0.0,
];

var texCoords = 
[
        //square
	0.0,  0.0,
	1.0,  1.0,
	0.0,  1.0,

	0.0,  0.0,
	1.0,  0.0,
	1.0,  1.0,
];

Graphically, we could see the mapping as the following:

B5. A Simple Texture Mapping Shader

To add texture mapping capability to your shader program, you need to do the following:
  1. New technique: Connect active textures to the shader. You have to tell the shader what texture image unit(s) your texture(s) is (are) bound to. This information is sent to a special type of uniform in the fragment shader called a sampler.
  2. Send the texture coordinates to the vertex shader. Do this like sending vertices, colors and normals.
  3. Pass the texture coordinates to the fragment shader. This is like passing a color or vertex position, except texture coordinates usually have 2D coordinates (vec2).
  4. New technique: Look up texture values in the shader. In the fragment shader you use the GLSL function texture2D to look up a value in the texture sampler at the interpolated texture coordinates.
Let's look at the new techniques in a little more detail:

Connect Active Textures to the Shader

To use a texture you must declare a uniform of type sampler* in your fragment shader. Samplers come in 2D and cube flavours. You will be using sampler2D. The sampler is used to help look up values in a texture correctly. Each sampler in your shader program will be connected to a single texture image unit. WebGL supports a minimum of 8 simultaneous texture samplers. For starters you will only be using one texture image unit, and that is unit 0, but you can find out your system's limit with this javacode: console.log(gl.getParameter(gl.MAX_COMBINED_TEXTURE_IMAGE_UNITS));.

In your fragment shader:
uniform sampler2D tex;
In your OpenGL code:
    //The default texture image unit is unit 0
    gl.uniform1i(gl.getUniformLocation(program, "tex"), 0);
Multi-Texturing

As you write more advanced programs, you may wish to use more than one texture at the same time to achieve certain effects such as adding detail, bump mapping or gloss mapping. To do this you create, bind, set parameters, and load data into multiple texture names, then you bind them to multiple texture image units and send their numbers to the shader like this:

In texture init phase
    //Configure two separate texture names, just like for simple texturing
     .
     .
     .
    //Connect the texture units you plan to use to samplers in the shader
    gl.uniform1i(gl.getUniformLocation(program, "tex0"), 0);
    gl.uniform1i(gl.getUniformLocation(program, "tex1"), 1);
While drawing
    //Bind texture names to texture units
    gl.activeTexture(gl.TEXTURE0); //switch to texture image unit 0
    gl.bindTexture(gl.TEXTURE_2D, textures[0]);   //Bind a texture to this unit
    gl.activeTexture(gl.TEXTURE1); //switch to texture image unit 1
    gl.bindTexture(gl.TEXTURE_2D, textures[1]);   //Bind a different texture to this unit

   //Draw textured item
   ...

Note that gl.activeTexture takes OpenGL constants of the form gl.TEXTUREn whereas you only send the number, n, of the texture unit to the sampler in the shader. WebGL specifies a minimum of 8 texture units, so n can be a value from 0 through 8. The number can be higher if your implementation supports it, which is likely — even my laptop with Intel integrated graphics supports 16.

Look Up Texture Values in the Shader

You use the special GLSL function called texture2D to look up values in a texture. You provide it with a sampler which is connected to a texture image unit that has been configured to look up values in a certain way from a specific texture image. The value to look up is controlled by the texture coordinates. Consider the following example from the Checkers demo:
precision mediump float;
        
varying vec2 TexCoord;
uniform sampler2D tex;

void main() 
{ 
	gl_FragColor = texture2D(tex, TexCoord);
}


C. Repeating a Texture Pattern or Clamping It

In the example that we are using, we have specified that we would like to repeat the pattern, but we are currently not making use of this repeating feature. When we repeat a texture, it provides a "tile" effect. The following code specifies that we will repeat the texture map.

    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT);
 

But in order to see any results, we have to assign texture coordinates outside the range [0,1]. For instance to get a 3x3 tile of the checkerboard on our square, we would specify the texture coordinates like this:

var texCoords = 
[
    //square
	0.0,  0.0,
	3.0,  3.0,
	0.0,  3.0,

	0.0,  0.0,
	3.0,  0.0,
	3.0,  3.0,
];

Try it out!

Graphically, we could see the mapping as the following (the red lines are placed in the texture map to help show that the texture is repeated three times)

Instead of specifying that we want to repeat the texture, we can specify that we want to clamp or mirror repeat the texture. In this case, we change the gl.REPEAT to gl.CLAMP_TO_EDGE or gl.MIRRORED_REPEAT. Here's an example of clamping:

   gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); 
   gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); 

What does this mean? Any values greater than 1.0 are set to 1.0, and any values less than 0.0 are set to 0.0. For instance, if we use gl.CLAMP_TO_EDGE with the above texture coordinates, then a small copy of the texture will appear in the bottom left-hand corner of the object. To the right will be a "clamp" of the texture at s=1. To the top, will be a clamp of t=1.

Graphically, we could see the mapping as something like this (again, the red lines are in the image to point out the original texture in the bottom left-hand corner).


D. Working with Image Files

The checkerboard pattern in the above example was a simple black and white image that we stored into an array. It is possible to generate other images procedurally in various ways, but you may want to use more complicated pictures — for instance art or photographs.

WebGL allows you to easily load any image supported by your browser, including images created in other canvases, with a modified version of texImage2d. It is documented in the WebGL specification, but has no equivalent in OpenGL ES. Here is how you would typically use it.

First you create an image tag. You can do it programatically with javascript, but it is easier to make a hidden img tag with an ID like this:

In your HTML file:
<img src="pic.png" id="mypic" hidden />
        
Then, you request the picture by its ID and send it to texImage2d as an argument, like this:
In your .js file:
    // WebGL often loads images upside down. This will correct that.
    gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL,1);

    // Get the IMG tag by its ID
    var mypic = document.getElementById("mypic");
    
    // Load data from the IMG
    gl.texImage2D(gl.TEXTURE_2D, 
                  0,                //mip map level
                  gl.RGBA,          //internal texel format (in graphics memory)
                  gl.RGBA,          //external texel format (in system memory)
                  gl.UNSIGNED_BYTE, //external texel data type
                  mypic             //image data source - IMG tag, ImageData object, 
                                    //  HTMLCanvasElement or HTMLVideoElement 
                                    //  (frame number required)
                  );


        
Please Note:

Although modern OpenGL supports images of arbitrary sizes, WebGL is like older OpenGL. It only allows images that have both a width and height that are a power of 2. For instance, valid sizes would be: 64 x 16, 512x512, 256 x 256, 128 x 128, and 16 x 4. Some invalid sizes would be: 100 x 100, 6 x 4, 5 x 5, and 2 x 22.


E. References

The following are a list of references which were used in the making of this lab:


F. Exercise

Goals:

This week's lab exercise was originally based on Lesson 6 from NeHe OpenGL tutorials. It has been mostly rewritten, but still uses some textures provided in that tutorial.

Start with Lab6Exercise.zip. If you are working locally, be sure to restart Chrome properly to open local texture files. For example on the lab Macs:

open /Applications/Google\ Chrome.app --args --allow-file-access-from-files

Modify the code by following these steps:

  1. First, run the code to see how it works. You should see a cube with a NEHE logo. You can use the 'a' key to toggle a spinning animation.
  2. Pick three of your favorite images and use an image editor to crop a portion of an image and save it in the web image format of your choice (jpg, png or gif are probably best). Save the three images with these sizes: 512 x 512, 500 x 500, and 256 x 256. Store these files in the Data directory.

  3.    (3 marks--one mark for each file)

  4. Edit the code in Lab6Exercise.js so that you can switch between the three image files that you created by pressing keys. Do NOT reload the texture using texImage2D every time you switch - only rebind textures you have already loaded.
       (2 marks - one for keys that switch, one for switching properly)
  5. Run the result. Try all three images. Do they all work on your computer? From the lab notes tell me whether they are guaranteed to work on all OpenGL implementations. Which ones wouldn't? Why?

  6.    (1 mark--for answering question)

  7. Modify the code so that your image is tiled 3x3 on the surface of the cube (similar to the image below). Save a copy of this code to hand in.
       (2 marks)

  8. Modify the code so that your image is in the middle of the cube with the "clamped" image along the edges (similar to the image below).
       (2 marks)

      (TOTAL   /10)

BONUS: Specular Mapping and Multitexturing

Notice that the fragment shader receives two colours from the vertex shader - one for diffuse and ambient, and another for specular. Both are added together then multiplied by the texture colour. This hides spcular highlights when the texture is dark. If you only multiply the texture against the diffuse and ambient colour, you can see the specular highlight across the surface of the whole cube. It is possible to use a second texture to control specular colour.

To do this bonus:

Deliverables