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 loadTexture() routine starts by creating a WebGL texture object texture by calling the WebGL createTexture() function. It then uploads a single blue pixel using texImage2D(). This makes the texture immediately usable as a solid blue color even though it may take a few moments for our image to download.

To load the texture from the image file, it then creates an Image object and assigned the src to the url for our image we wish to use as our texture. The function we assign to image.onload will be called once the image has finished downloading. At that point we again call texImage2D() this time using the image as the source for the texture. After that we setup filtering and wrapping for the texture based on whether or not the image we download was a power of 2 in both dimensions or not.

WebGL1 can only use non power of 2 textures with filtering set to NEAREST or LINEAR and it can not generate a mipmap for them. Their wrapping mode must also be set to CLAMP_TO_EDGE. On the other hand if the texture is a power of 2 in both dimensions then WebGL can do higher quality filtering, it can use mipmap, and it can set the wrapping mode to REPEAT or MIRRORED_REPEAT.

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(). This will allow non-power-of-two (NPOT) textures at the expense of mipmapping, UV wrapping, UV tiling, and your control over how the device will handle your texture.

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 transparent black: rgba(0,0,0,0).

To load the image, add a call to our loadTexture() function within our main() function. This can be added after the initBuffers(gl) call.

// Load texture
const texture = loadTexture(gl, 'cubetexture.png');

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

First, this code creates a WebGL 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 WebGL has that data ready for its use.

Updating the shaders

The shader program also needs to be updated to use the textures instead of solid colors.

The vertex shader

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

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

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) based on the value of vTextureCoord which like the colors is interpolated bewteen vertices.

Attribute and Uniform Locations

Because we changed an attribute and added a uniform we need to look up their locations

WebGL provides a minimum of 8 texture units; the first of these is gl.TEXTURE0. We tell WebGL we want to affect unit 0. We then call bindTexture() which binds the texture to the TEXTURE_2D bind point of texture unit 0. We then tell the shader that for the uSampler use texture unit 0.

Lastly, add texture as a parameter to the drawScene() function, both where it is defined and where it is called.

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.

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.