11 12 13 14 15 16

Textures

15

It's time to add some textures. The simplest way to create a texture from an image file is to use Image JavaScript class instance.

First, we need to create and bind texture:

const texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);

Next thing I suggest you to do right away is specify UNPACK_FLIP_Y_WEBGL option since for some reason WebGL textures are flipped vertically in comparasion to OpenGL:

gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);

And now we'll create an image and continue texture initialization after the image is loaded:

const image = new Image();
image.src = "texture.jpg";
image.onload = function()
{
    gl.bindTexture(gl.TEXTURE_2D, texture);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);
    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, this);
    gl.generateMipmap(gl.TEXTURE_2D);
};

Let's see what's going on in the load handler. These two options specify texture filtering behavior. TEXTURE_MAG_FILTER is how texture should filter when it scales up, and TEXTURE_MIN_FILTER is how should it filter upon scaling down:

gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);

When scaling up, linear filtering is OK. However when scaling down, linear filtering gives noticeable artifacts. LINEAR_MIPMAP_LINEAR option gives better result. This option enables filtering between texels as well as between mipmap levels. Mipmap levels are the series of the original image where each level is the previous level scaled down by two times by a special averaging algorithm.

After setting up filtering parameters, we initialize texture image data and ask WebGL to build mipmap levels for it:

gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, this);
gl.generateMipmap(gl.TEXTURE_2D);

Now we can use the texture in our shaders. But before that we need to do something else. We've already learned how to pass multiple vertex attributes for each vertex. Last time we passed vertex color with its position. This time we need to pass texture coordinates. First, let's update our vertex buffer values:

const vertices =
[
//   x   y  s  t
    -1, -1, 0, 0,
    -1,  1, 0, 1,
     1, -1, 1, 0,
     1,  1, 1, 1
];

This time we'll draw a quad. x and y are vertex positions while s and t are texture coordinates. Now let's setup vertex buffer properly:

const buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);

const position = gl.getAttribLocation(program, "position");
const texCoord = gl.getAttribLocation(program, "texCoord");
gl.enableVertexAttribArray(position);
gl.enableVertexAttribArray(texCoord);

const stride = 4 * Float32Array.BYTES_PER_ELEMENT;
gl.vertexAttribPointer(position, 2, gl.FLOAT, false, stride, 0);
gl.vertexAttribPointer(texCoord, 2, gl.FLOAT, false, stride, 2 * Float32Array.BYTES_PER_ELEMENT);

We've already seen this when we've dealt with vertex colors. After the buffer is properly set up, we should update our shaders. Vertex program:

#version 300 es
precision highp float;

// ...

in vec2 position;
in vec2 texCoord; // This is texture coordinates which we passed with vertex attributes

out vec2 v_texCoord; // We should pass it into the fragment program

void main(void)
{
    v_texCoord = texCoord; // Here we go

    // ...
}

Fragment program:

#version 300 es
precision highp float;

uniform sampler2D diffuse; // We will bind our texture through this uniform

in vec2 v_texCoord; // Texture coordinates passed from the vertex program

out vec4 frag_color;

void main(void)
{
    // texture() is built-in GLSL function which gives 4-components color for a given
    // texture sampler and texture coordinates
    frag_color = texture(diffuse, v_texCoord);
}

And finally, we should bind our texture upon rendering:

// Get the uniform location before the draw loop
const diffuse = gl.getUniformLocation(program, "diffuse");

function draw(timestamp)
{
    // ...

    gl.useProgram(program);
    gl.uniform1f(aspect, canvas.height / canvas.width);
    gl.uniform1f(time, timestamp / 10000);

    gl.activeTexture(gl.TEXTURE0); // Set the active texture
    gl.bindTexture(gl.TEXTURE_2D, texture);
    gl.uniform1i(diffuse, 0); // Bind our texture to the texture slot 0

    // Now everything is ready and we can draw our textured quad:
    gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
    gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);

    // ...
}

You may have several questions like how do I use multiple textures, what types of textures there are, etc. We'll cover all those in future lessons. But for now one texture is enough. If everything is correct, you should see something like this:

Source code for this lesson: source.zip

Note that due to security reasons image loading won't work with local files. You should put this lesson's source code and texture on some server (remote either local) to make it work.

Lesson 15
Share this page:

Learning plan

11. Uniforms
How to use draw call level parameters to control the shading process
Let's add more attributes to vertices and use them to enhance our triangle
How to evaluate a vertex-specified data and interpolate it between vertices
14. Textures
An introduction on how to load texture, initialize it properly with WebGL and pass it into GLSL program
15. glMatrix
How to install and use glMatrix library which provides vector and matrix arithmetics and helper functions
We've learned many thing by this point. Let's start organizing them neat and clean in the object-oriented way