CS315 Lab 6: Texture Mapping

This lab is an introduction to Texture Mapping.

Assignment:

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

  • Use your own image for texture mapping
  • Use repeating and clamped textures

A. Pictorial Overview and Definitions

The following diagram represents the idea of texture mapping:

Some definitions:

  • Texture Mapping--"a method of adding realism to a computer-generated graphic. An image (the texture) is added (mapped) to a simpler shape that is generated in the scene, like a decal pasted to a flat surface. This reduces the amount of computing needed to create the shapes and textures in the scene. For instance, a sphere may be generated and a face texture mapped, to remove the need for processing the shape of the nose and eyes." (Source: http://www.wordiq.com/definition/Texture_mapping)
  • Textures--rectangular arrays of data. The data can be color data, luminance data, or color and alpha data
  • Texels--the individual values in a texture array
  • Mipmaps--Although we will not be covering this in detail in this lab. Mipmaps are often discussed with texture mapping. It's something good to know.

    The idea is that you have multiple images to cover multiple levels of detail. For instance, for an object far away, you have a texture or a mipmap that is small and with little detail, and for closer objects, you have a texture that is large and detailed.

    Taking an example from msdn (Search for EasyTex). If you are looking at a stop sign from far away, you see only a red circle. As you get closer, you can see it is a red shape with some letters. Finally, you see the entire stop sign. You might specify the mipmaps to look something like this:

    If you did not specify different levels of detail, WebGL would try to squish the large stop sign into a smaller image. It would combine the red sign with the white letters, and you would get a pink blob instead of a round, red circle.


B. Coding Overview

Download

For the following discussion you may want to download Lab6.zip.

Working with local files

In this lab, like last lab, Javascript will have to work with local files - your texture files. Your web browser blocks this access to keep you safe. If you are using Visual Studio Code with Live Server, you will have no problem.

On a Mac, you can start Chrome like this to be able to load local files:

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

On Windows, you can create a Chrome shortcut as to do this as shown in this StackOverflow posting to be able to load local files.

For security reasons, I strongly recommend that you use Live Server rather than the command line flag.

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. OpenGL and OpenGL ES use the glGenTextures() command to assign a name (index number - like a pointer) to one or more texture objects. WebGL uses the similar createTexture() command. It creates one texture name as if glGenTextures() was called, and uses it to initialize and return 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, we neet to specify some information about the type of texture we are working with. We do this with bindTexture(). There are four types of textures in WebGL: 2D, cube map, 3D and 2D array. 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);

  

Calling gl.bindTexture() for the first time with texName, initializes the texture object as a 2D texture with some related default values or properties. Once you bind a texture name as either 2D or cube map, you must always bind it as such or you will get warnings and errors. The other properties of the texture can be modified while it is bound.

We will call gl.bindTexture() again later with this same 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.

B2. Texture Properties and the Image

We've got a texture name and we've created a texture with the 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 and their defaults, you should consult the MDN page for texParameter[if] .

  
    //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 MDN 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 texture 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 2.0 supports a minimum of 16 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);

Look Up Texture Values in the Shader

You use the special GLSL function called texture 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:
In your fragment shader:
  
#version 300 es
precision mediump float;
        
in vec2 TexCoord;
uniform sampler2D tex;
out vec4 fragColor;

void main() 
{ 
	fragColor = texture(tex, TexCoord);
}

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 2.0 specifies a minimum of 16 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.



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:

  • Edit your own picture and make it a texture map (3 different sizes)
  • Tile your texture across the image
  • Clamp and center your image on the object

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 Lab6.zip. If you are working locally, use Live-Server or restart Chrome properly to open local texture files. There's instructions to do that at the top of this lab.

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).
       (2 marks)

  8. Modify the code so that your image is in the middle of the cube with the image "clamped" to edge (similar to the image below). Provide a key that will switch to this mode, and a key to switch back to tiled mode.
       (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 specular 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 independently of the diffuse colour. This can some interesting effects. Combining this with another texture to control the specular exponent (shininess) is one way to accomplish the technique called gloss mapping.

To do this bonus:
  • Start with a new copy of this week's lab project.
  • Add a second texture sampler to the fragment shader.
  • Add an appropriate call to setUpTextures to connect a second texture unit to the new sampler.
  • Multiply the specular colour by this new texture sampler - it will be the specular map.
  • Multiply the diffuse and ambient colour by the original texture sampler - it will be the diffuse map.
  • Add the two products together to create the final fragment colour.
  • Load two separate textures to two separate texture names in setUpTextures. One will be your specular texture, and the other will be your diffuse. You will find textures in the project that can be used for this purpose. You should be able to figure out which is which from the file names.
  • Just before drawing the box, bind the two textures to the two texture units you intend to use.
  • If you are confused, ask your lab instructor to demonstrate the desired effect.

Deliverables

  • Written answer to 4.
  • Version of your program with repeated textures.
  • Version of your program with clamped textures. For code components turn in uniquely named .cpp files, and one copy of your Data folder. If you do the bonus, submit your modified fragment shader as well.