Textures
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.