Texture Mapping and Matrix Stacks
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
open /Applications/Google\ Chrome.app --args --allow-file-access-from-files
The Steps in Texture Mapping are the following
- Create a texture name and bind it to the type of texture we will be creating
- Specify texture properties and the texture image
- Enable texture mapping
- 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:- 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.
- Send the texture coordinates to the vertex shader. Do this like sending vertices, colors and normals.
- 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).
- 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.
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));.
uniform sampler2D tex;
//The default texture image unit is unit 0
gl.uniform1i(gl.getUniformLocation(program, "tex"), 0);
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:
//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);
//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:
<img src="pic.png" id="mypic" hidden />
// 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)
);
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.
Extended Resources
Save