Using textures in WebGL

Now that our sample program has a rotating 3D cube, let's map a texture onto it instead of having its faces be solid colors.

Loading textures

The first thing to do is add code to load the textures. In our case, we'll be using a single texture, mapped onto all six sides of our rotating cube, but the same technique can be used for any number of textures.

Note: It's important to note that the loading of textures follows cross-domain rules; that is, you can only load textures from sites for which your content has CORS approval. See Cross-domain textures below for details.

The code that loads the texture looks like this:

JavaScript
function initTextures() {
  cubeTexture = gl.createTexture();
  cubeImage = new Image();
  cubeImage.onload = function() { handleTextureLoaded(cubeImage, cubeTexture); }
  cubeImage.src = "cubetexture.png";
}

function handleTextureLoaded(image, texture) {
  gl.bindTexture(gl.TEXTURE_2D, texture);
  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_NEAREST);
  gl.generateMipmap(gl.TEXTURE_2D);
  gl.bindTexture(gl.TEXTURE_2D, null);
}

The initTextures() routine starts by creating the GL texture object cubeTexture by calling the GL createTexture() function. To load the texture from the image file, it then creates an Image object and loads into it the graphic file we wish to use as our texture. The handleTextureLoaded() callback routine is run when the image is done loading.

To actually create the texture, we specify that the new texture is the current texture on which we want to operate by binding it to gl.TEXTURE_2D. After that, the loaded image is passed into texImage2D() to write the image data into the texture.

Note: Textures' widths and heights must, in most circumstances, be a power of two number of pixels (that is, 1, 2, 4, 8, 16, etc) in each dimension. For the exception, see section: "Non power-of-two textures", below.

The next two lines set up filtering for the texture; this controls how the image is filtered while scaling. In this case we're using linear filtering when scaling the image up, and a mipmap when scaling down. Then the mipmap is generated by calling generateMipMap(), and we finish up by telling WebGL we're done manipulating the texture by binding null to gl.TEXTURE_2D.

Non power-of-two textures

Generally speaking, using textures whose sides are a power of two is ideal. They are efficiently stored in video memory and are not restricted in how they could be used. Artist-created textures should be scaled up or down to a nearby power of two and, really, should have been authored in power-of-two to begin with. Each side should be: 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, or 2048 pixels. Many, but not all, devices can support 4096 pixels; some can support 8192 and above.

Occasionally, it is difficult to use power-of-two textures due to your specific circumstance. If the source is some 3rd party, often the best results come from modifying the images using HTML5 canvas into power-of-two sizes before they are passed to WebGL; UV coordinates may also require adjustment if stretching is apparent.

But, if you must have a non power-of-two (NPOT) texture, WebGL does include limited native support. NPOT textures are mostly useful if your texture dimensions must be the same resolution as something else, such as your monitor resolution, or if the above suggestions are just not worth the hassle. The catch: these textures cannot be used with mipmapping and they must not "repeat" (tile or wrap).

An example of a repeated texture is tiling an image of a few bricks to cover a brick wall.

Mipmapping and UV repeating can be disabled with texParameteri() when you create your texture using bindTexture(). This will allow NPOT textures at the expense of mipmapping, UV wrapping, UV tiling, and your control over how the device will handle your texture.

JavaScript
// gl.NEAREST is also allowed, instead of gl.LINEAR, as neither mipmap.
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
// Prevents s-coordinate wrapping (repeating).
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
// Prevents t-coordinate wrapping (repeating).
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);

Again, with these parameters, compatible WebGL devices will automatically accept any resolution for that texture (up to their maximum dimensions). Without performing the above configuration, WebGL requires all samples of NPOT textures to fail by returning solid black: rgba(0,0,0,1).

Mapping the texture onto the faces

At this point, the texture is loaded and ready to use. But before we can use it, we need to establish the mapping of the texture coordinates to the vertices of the faces of our cube. This replaces all the previously existing code for configuring colors for each of the cube's faces in initBuffers().

JavaScript
cubeVerticesTextureCoordBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, cubeVerticesTextureCoordBuffer);
  
var textureCoordinates = [
  // Front
  0.0,  0.0,
  1.0,  0.0,
  1.0,  1.0,
  0.0,  1.0,
  // Back
  0.0,  0.0,
  1.0,  0.0,
  1.0,  1.0,
  0.0,  1.0,
  // Top
  0.0,  0.0,
  1.0,  0.0,
  1.0,  1.0,
  0.0,  1.0,
  // Bottom
  0.0,  0.0,
  1.0,  0.0,
  1.0,  1.0,
  0.0,  1.0,
  // Right
  0.0,  0.0,
  1.0,  0.0,
  1.0,  1.0,
  0.0,  1.0,
  // Left
  0.0,  0.0,
  1.0,  0.0,
  1.0,  1.0,
  0.0,  1.0
];

gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(textureCoordinates),
              gl.STATIC_DRAW);

First, this code creates a GL buffer into which we'll store the texture coordinates for each face, then we bind that buffer as the array we'll be writing into.

The textureCoordinates array defines the texture coordinates corresponding to each vertex of each face. Note that the texture coordinates range from 0.0 to 1.0; the dimensions of textures are normalized to a range of 0.0 to 1.0 regardless of their actual size, for the purpose of texture mapping.

Once we've set up the texture mapping array, we pass the array into the buffer, so that GL has that data ready for its use.

Updating the shaders

The shader program -- and the code that initializes the shaders -- also needs to be updated to use the textures instead of solid colors.

First, let's take a look at the very simple change needed in initShaders():

JavaScript
textureCoordAttribute = gl.getAttribLocation(shaderProgram, "aTextureCoord");
gl.enableVertexAttribArray(textureCoordAttribute);
gl.vertexAttribPointer(texCoordAttribute, 2, gl.FLOAT, false, 0, 0);

This replaces the code that set up the vertex color attribute with one that contains the texture coordinate for each vertex.

The vertex shader

Next, we need to replace the vertex shader so that instead of fetching color data, it instead fetches the texture coordinate data.

HTML
<script id="shader-vs" type="x-shader/x-vertex">
  attribute vec3 aVertexPosition;
  attribute vec2 aTextureCoord;
    
  uniform mat4 uMVMatrix;
  uniform mat4 uPMatrix;
      
  varying highp vec2 vTextureCoord;
  
  void main(void) {
    gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0);
    vTextureCoord = aTextureCoord;
  }
</script>

The key change here is that instead of fetching the vertex color, we're setting the texture coordinates; this will indicate the location within the texture corresponding to the vertex.

The fragment shader

The fragment shader likewise needs to be updated:

HTML
<script id="shader-fs" type="x-shader/x-fragment">
  varying highp vec2 vTextureCoord;
      
  uniform sampler2D uSampler;
      
  void main(void) {
    gl_FragColor = texture2D(uSampler, vec2(vTextureCoord.s, vTextureCoord.t));
  }
</script>

Instead of assigning a color value to the fragment's color, the fragment's color is computed by fetching the texel (that is, the pixel within the texture) that the sampler says best maps to the fragment's position.

Drawing the textured cube

The change to the drawScene() function is simple (except that for the purpose of clarity, I've removed the code that causes the cube to translate through space while animating; instead it just rotates).

The code to map colors to the texture is gone, replaced with this:

JavaScript
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, cubeTexture);
gl.uniform1i(gl.getUniformLocation(shaderProgram, "uSampler"), 0);

GL provides 32 texture registers; the first of these is gl.TEXTURE0. We bind our previously-loaded texture to that register, then set the shader sampler uSampler (specified in the shader program) to use that texture.

At this point, the rotating cube should be good to go.

View the complete code | Open this demo on a new page

Cross-domain textures

Loading of WebGL textures is subject to cross-domain access controls. In order for your content to load a texture from another domain, CORS approval needs to be be obtained. See HTTP access control for details on CORS.

See this hacks.mozilla.org article for an explanation of how to use CORS-approved images as WebGL textures, with a self-contained example.

Note: CORS support for WebGL textures and the crossOrigin attribute for image elements is implemented in Gecko 8.0.

Tainted (write-only) 2D canvases can't be used as WebGL textures. A 2D <canvas> becomes tainted, for example, when a cross-domain image is drawn on it.

Note: CORS support for Canvas 2D drawImage is implemented in Gecko 9.0. This means that using a cross-domain image with CORS approval does no longer taint the 2D canvas, so the 2D canvas remains usable as the source of a WebGL texture.

Note: CORS support for cross-domain videos and the crossorigin attribute for <video> elements is implemented in Gecko 12.0.

License

© 2016 Mozilla Contributors
Licensed under the Creative Commons Attribution-ShareAlike License v2.5 or later.
https://developer.mozilla.org/en-us/docs/web/api/webgl_api/tutorial/using_textures_in_webgl

Tutorial WebGL