WebGL / Tutorials / #2 Square
Covering the Canvas
This is a follow-up to WebGL Tutorial #1 Triangle. We’ll start from what we built in the previous tutorial. This time we’ll draw a square that covers the entire canvas and make some improvements to the code along the way.
View the demo or jump ahead and edit the code.
The next tutorial, #3 Gradient, builds on this one.
Cleanup
First let’s address the repetitiveness of the shader setup code:
const vertexShader = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vertexShader, vertexCode.trim());
gl.compileShader(vertexShader);
if (!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)) {
throw gl.getShaderInfoLog(vertexShader);
}
const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fragmentShader, fragmentCode.trim());
gl.compileShader(fragmentShader);
if (!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS)) {
throw gl.getShaderInfoLog(fragmentShader);
}
const program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
...
We can make a reusable function to do this:
function createShader(shaderType, sourceCode) {
const shader = gl.createShader(shaderType);
gl.shaderSource(shader, sourceCode.trim());
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
throw gl.getShaderInfoLog(shader);
}
return shader;
}
And use it to setup the program:
const program = gl.createProgram();
gl.attachShader(program, createShader(gl.VERTEX_SHADER, vertexCode));
gl.attachShader(program, createShader(gl.FRAGMENT_SHADER, fragmentCode));
Drawing a Square
With that out of the way, let’s draw a square. We can simply add three more vertices to our list of vertices:
const vertices = [
[-1, -1, 0],
[1, -1, 0],
[1, 1, 0],
[1, 1, 0],
[-1, 1, 0],
[-1, -1, 0],
];
This can be improved though. When we call gl.drawArrays(gl.TRIANGLES, 0, vertices.length);
,
the gl.TRIANGLES
parameter tells WebGL to draw one triangle for every three vertices.
There are other options, including
gl.TRIANGLE_STRIP.
With triangle strips, every vertex in the list defines a triangle with the following two vertices (as long as there
are at least two vertices following it). For example, the list [A,B,C,D,E]
defines three triangles: [A,B,C]
,
[B,C,D]
, and [C,D,E]
.
Let’s try it:
const vertices = [
[-1, -1, 0],
[1, -1, 0],
[1, 1, 0],
[-1, 1, 0],
];
...
gl.drawArrays(gl.TRIANGLE_STRIP, 0, vertices.length);
This doesn’t draw a square though. The order of our vertices are wrong:
When we connect vertices 1,2,3 and 2,3,4 into triangles, they overlap instead of covering the canvas. We need to draw the vertices in a “Z” shape to get the intended result:
const vertices = [
[-1, -1, 0],
[1, -1, 0],
[-1, 1, 0],
[1, 1, 0],
];
And now we have a square again:
2D Vertices
We can simplify this more. We’re always setting the z coordinate to 0 because we’re drawing 2D. In the vertex shader, z defaults to 0 if we don’t provide it, so we don’t need to provide it:
const vertices = [
[-1, -1],
[1, -1],
[-1, 1],
[1, 1],
];
For this to work, we have to tell
gl.vertexAttribPointer()
that we are giving it 2D vertices by changing
the second parameter to 2
:
gl.vertexAttribPointer(vertexPosition, 2, gl.FLOAT, false, 0, 0);
Result
It’s not much to look at, but now we have the entire canvas to play around with:
Here’s the full HTML page with code. Changes to the previous tutorial are highlighted.
<!DOCTYPE html>
<html>
<body>
<canvas id="canvas" width="500" height="500"></canvas>
<script id="vertex" type="x-shader/x-vertex">
#version 300 es
in vec4 vertexPosition;
void main() {
gl_Position = vertexPosition;
}
</script>
<script id="fragment" type="x-shader/x-fragment">
#version 300 es
precision highp float;
out vec4 fragColor;
void main() {
fragColor = vec4(1, 0, 0, 1);
}
</script>
<script>
const canvas = document.getElementById("canvas");
const vertexCode = document.getElementById("vertex").textContent;
const fragmentCode = document.getElementById("fragment").textContent;
const gl = canvas.getContext("webgl2");
if (!gl) throw "WebGL2 not supported";
function createShader(shaderType, sourceCode) {
const shader = gl.createShader(shaderType);
gl.shaderSource(shader, sourceCode.trim());
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
throw gl.getShaderInfoLog(shader);
}
return shader;
}
const program = gl.createProgram();
gl.attachShader(program, createShader(gl.VERTEX_SHADER, vertexCode));
gl.attachShader(program, createShader(gl.FRAGMENT_SHADER, fragmentCode));
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
throw gl.getProgramInfoLog(program);
}
gl.useProgram(program);
const vertices = [
[-1, -1],
[1, -1],
[-1, 1],
[1, 1],
];
const vertexData = new Float32Array(vertices.flat());
gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());
gl.bufferData(gl.ARRAY_BUFFER, vertexData, gl.STATIC_DRAW);
const vertexPosition = gl.getAttribLocation(program, "vertexPosition");
gl.enableVertexAttribArray(vertexPosition);
gl.vertexAttribPointer(vertexPosition, 2, gl.FLOAT, false, 0, 0);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, vertices.length);
</script>
</body>
</html>
Go to the next tutorial, #3 Gradient.