Skip to content

WebGL - Drawing a Quad

While WebGL’s fundamental drawing primitives are points, lines, and triangles, we often need to draw more complex shapes like quadrilaterals (quads) or squares. This chapter demonstrates how to draw a quad by composing it from two triangles, using an index buffer.

A quad can be represented as two adjacent triangles sharing an edge. To draw this efficiently using WebGL, we define the four unique vertices of the quad and then use an index buffer (ELEMENT_ARRAY_BUFFER) to specify which vertices form each of the two triangles.

Consider a quad with vertices 0, 1, 2, 3:

0-------3
| \ |
| \ |
| \ |
| \ |
| \ |
1-------2

We can define the vertices and then use indices to form triangle 1 (vertices 0, 1, 2) and triangle 2 (vertices 0, 2, 3).

The process is very similar to drawing a single triangle with indices, but with different geometry data:

Step 1: Setup Canvas and WebGL Context

Standard initialization.

Step 2: Define Geometry (Vertices & Indices) and Buffers

Define the 4 unique vertices of the quad in a Float32Array. Define the 6 indices (3 per triangle) in a Uint16Array (or similar). Create two WebGL buffers: one VBO for vertices (gl.ARRAY_BUFFER) and one IBO for indices (gl.ELEMENT_ARRAY_BUFFER). Load the respective data into each buffer using gl.bufferData.

// Define 4 vertices for the quad (X, Y, Z)
const quadVertices = new Float32Array([
-0.5, 0.5, 0.0, // Vertex 0: Top-left
-0.5, -0.5, 0.0, // Vertex 1: Bottom-left
0.5, -0.5, 0.0, // Vertex 2: Bottom-right
0.5, 0.5, 0.0 // Vertex 3: Top-right
]);
// Define indices for two triangles forming the quad
const quadIndices = new Uint16Array([
0, 1, 2, // Triangle 1 (0 -> 1 -> 2)
0, 2, 3 // Triangle 2 (0 -> 2 -> 3)
]);
// ... (Create VBO & IBO, bufferData for both) ...

Step 3: Create and Compile Shaders

Basic vertex and fragment shaders are sufficient. The vertex shader needs to take the position attribute and output gl_Position. The fragment shader sets gl_FragColor.

// Vertex Shader (Simple Pass-through)
attribute vec3 a_position;
void main() {
gl_Position = vec4(a_position, 1.0);
}
// Fragment Shader (Solid Color)
precision mediump float;
void main() {
gl_FragColor = vec4(0.3, 0.8, 0.4, 1.0); // Greenish color
}

Step 4: Link Shaders and Buffers

Get attribute locations, enable the position attribute array, and configure gl.vertexAttribPointer to read from the VBO.

Step 5: Drawing the Quad

Clear the canvas, set viewport, etc. Crucially, bind the Index Buffer Object (IBO) to the gl.ELEMENT_ARRAY_BUFFER target before drawing. Then, use gl.drawElements(gl.TRIANGLES, count, type, offset).

  • mode: gl.TRIANGLES
  • count: The number of indices to draw (6 for a quad made of two triangles).
  • type: The data type of the indices in the IBO (e.g., gl.UNSIGNED_SHORT).
  • offset: Starting offset in the IBO (usually 0).
// Bind the index buffer before drawing
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBufferObject);
// Draw the quad using indices
gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0);

This example draws a solid greenish quad using the steps outlined above.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>WebGL Drawing a Quad</title>
<style>
body { margin: 0; overflow: hidden; background-color: #444; }
canvas { display: block; border: 1px solid lightblue; }
</style>
</head>
<body>
<canvas id="webgl-canvas" width="500" height="500"></canvas>
<script>
// --- Shader Sources ---
const vsSource = `
attribute vec3 a_position;
void main() {
gl_Position = vec4(a_position, 1.0);
}
`;
const fsSource = `
precision mediump float;
void main() {
gl_FragColor = vec4(0.3, 0.8, 0.4, 1.0); // Greenish
}
`;
// --- Utilities: createShader, createProgram (Assume defined) ---
function createShader(gl, type, source) { /* ... */
const shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
console.error(`Shader compilation error (${type === gl.VERTEX_SHADER ? 'Vertex' : 'Fragment'}):`, gl.getShaderInfoLog(shader));
gl.deleteShader(shader);
return null;
}
return shader;
}
function createProgram(gl, vsSource, fsSource) { /* ... */
const vertexShader = createShader(gl, gl.VERTEX_SHADER, vsSource);
const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fsSource);
if (!vertexShader || !fragmentShader) return null;
const program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
console.error('Program linking error:', gl.getProgramInfoLog(program));
gl.deleteProgram(program);
return null;
}
return program;
}
// --- Main WebGL ---
const canvas = document.getElementById('webgl-canvas');
const gl = canvas.getContext('webgl');
if (!gl) { alert('WebGL not supported.'); throw new Error('WebGL not supported'); }
const shaderProgram = createProgram(gl, vsSource, fsSource);
if (!shaderProgram) throw new Error('Failed program creation.');
// --- Geometry ---
const quadVertices = new Float32Array([
-0.5, 0.5, 0.0, // 0: Top-left
-0.5, -0.5, 0.0, // 1: Bottom-left
0.5, -0.5, 0.0, // 2: Bottom-right
0.5, 0.5, 0.0 // 3: Top-right
]);
const quadIndices = new Uint16Array([
0, 1, 2, // First triangle
0, 2, 3 // Second triangle
]);
// --- Buffers ---
const vertexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
gl.bufferData(gl.ARRAY_BUFFER, quadVertices, gl.STATIC_DRAW);
const indexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, quadIndices, gl.STATIC_DRAW);
// --- Attribute Setup ---
const positionAttribLocation = gl.getAttribLocation(shaderProgram, 'a_position');
if (positionAttribLocation < 0) console.error("Attr 'a_position' not found");
gl.enableVertexAttribArray(positionAttribLocation);
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer); // Bind VBO before pointer
gl.vertexAttribPointer(positionAttribLocation, 3, gl.FLOAT, false, 0, 0);
// --- Drawing ---
gl.useProgram(shaderProgram); // Activate program
gl.clearColor(0.25, 0.25, 0.25, 1.0); // Dark gray background
gl.enable(gl.DEPTH_TEST);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
// ** Bind Index Buffer **
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
// ** Draw using Indices **
gl.drawElements(
gl.TRIANGLES, // Primitive type
6, // Number of indices to draw
gl.UNSIGNED_SHORT, // Type of indices (matches Uint16Array)
0 // Offset in buffer
);
// Good practice: Unbind buffers after drawing if needed elsewhere
gl.bindBuffer(gl.ARRAY_BUFFER, null);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null);
</script>
</body>
</html>

Expected Output:

The code will render a solid greenish square centered on the canvas, filling the area from (-0.5, -0.5) to (0.5, 0.5) in clip space.

  • Modify the vertex coordinates to create a rectangle instead of a square.
  • Change the index order (e.g., 1, 2, 3, 1, 3, 0) to see how it affects which triangles are drawn (it should still form the same quad).
  • Apply colors or transformations to the quad using techniques from other chapters.
  • Try drawing the quad using gl.TRIANGLE_STRIP mode with drawArrays and only 4 vertices ordered correctly (e.g., 1, 0, 2, 3). Compare the setup required.