Skip to content

WebGL - Drawing a Triangle

Drawing a simple shape like a triangle is often the “Hello, World!” of graphics programming. It involves all the fundamental steps of a WebGL application: setting up the canvas, defining geometry, writing shaders, sending data to the GPU, and triggering the draw call.

This chapter demonstrates drawing a triangle using an index buffer (drawElements), which is a common and efficient approach, especially for more complex models where vertices are shared between multiple triangles.

Here’s a breakdown of the necessary steps:

Step 1: Prepare the Canvas and Get WebGL Context

Section titled “Step 1: Prepare the Canvas and Get WebGL Context”

First, you need an HTML <canvas> element. Then, use JavaScript to get a reference to it and request a WebGL rendering context.

<canvas id="glCanvas" width="640" height="480"></canvas>
const canvas = document.getElementById('glCanvas');
// Try getting the standard WebGL context first
const gl = canvas.getContext('webgl');
if (!gl) {
console.error("Unable to initialize WebGL. Your browser or machine may not support it.");
// Optionally try 'experimental-webgl' for older browsers
// gl = canvas.getContext('experimental-webgl');
// if (!gl) { ... handle error ... }
}
// If context is obtained, proceed...

Step 2: Define Geometry and Store in Buffers

Section titled “Step 2: Define Geometry and Store in Buffers”

Define the vertex positions and the indices that form the triangle. Then, create WebGL buffer objects (ARRAY_BUFFER for vertices, ELEMENT_ARRAY_BUFFER for indices) and load the data into them.

// Define vertex positions (X, Y, Z)
const positions = [
-0.5, -0.5, 0.0, // Vertex 0: Bottom-left
0.5, -0.5, 0.0, // Vertex 1: Bottom-right
0.0, 0.5, 0.0 // Vertex 2: Top-center
];
// Define the indices that form the triangle (using vertex indices 0, 1, 2)
const indices = [0, 1, 2];
// --- Create and load position buffer ---
const positionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
// Note: It's good practice to unbind after configuration, but not strictly necessary
// gl.bindBuffer(gl.ARRAY_BUFFER, null);
// --- Create and load index buffer ---
const indexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW);
// gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null);

gl.STATIC_DRAW is a hint to WebGL that the data will not change often.

Section titled “Step 3: Create, Compile, and Link Shader Programs”

Write the vertex and fragment shader code in GLSL, then use WebGL functions to compile them and link them into a usable shader program. Remember to include error checking!

Vertex Shader (GLSL):

attribute vec3 a_position; // Receive vertex position attribute
void main() {
// Pass the position directly to the output (no transformation)
// We use vec4 because gl_Position requires (x, y, z, w)
gl_Position = vec4(a_position, 1.0);
}

Fragment Shader (GLSL):

precision mediump float; // Necessary precision declaration
void main() {
// Set the fragment color to opaque orange
gl_FragColor = vec4(1.0, 0.5, 0.0, 1.0);
}

JavaScript (Compilation/Linking - Conceptual, use helper functions):

// Assume vsSource and fsSource contain the GLSL code strings
// Assume loadShader and initShaderProgram helper functions exist (as shown previously)
const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vsSource);
const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fsSource);
const shaderProgram = initShaderProgram(gl, vertexShader, fragmentShader);
// If shaderProgram is null, an error occurred during setup
if (!shaderProgram) {
// Handle error - likely reported by helper functions
return;
}

Step 4: Associate Shader Attributes with Buffer Data

Section titled “Step 4: Associate Shader Attributes with Buffer Data”

Get the location of the attribute variable (a_position) in the shader program. Then, tell WebGL how to pull data from the currently bound ARRAY_BUFFER (our positionBuffer) for that attribute.

// Get the location of the attribute
const positionAttributeLocation = gl.getAttribLocation(shaderProgram, 'a_position');
// Bind the position buffer again (if not already bound)
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
// Tell WebGL how to pull data out of the position buffer
const numComponents = 3; // (X, Y, Z)
const type = gl.FLOAT; // Data type
const normalize = false; // Don't normalize
const stride = 0; // Bytes to skip between elements (0 = tightly packed)
const offset = 0; // Bytes offset from the beginning
gl.vertexAttribPointer(
positionAttributeLocation,
numComponents,
type,
normalize,
stride,
offset
);
// Enable the vertex attribute array
gl.enableVertexAttribArray(positionAttributeLocation);

Step 5: Set Up Render State and Draw the Triangle

Section titled “Step 5: Set Up Render State and Draw the Triangle”

Clear the canvas, set the viewport, tell WebGL to use the shader program, ensure the index buffer is bound, and finally, call drawElements.

// Set clear color to light gray and clear the canvas
gl.clearColor(0.8, 0.8, 0.8, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
// Set the viewport to match the canvas size
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
// Tell WebGL to use our shader program
gl.useProgram(shaderProgram);
// Bind the index buffer (important before drawElements)
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
// Draw the triangle!
const primitiveType = gl.TRIANGLES; // Draw triangles
const count = indices.length; // Number of indices to draw
const indexType = gl.UNSIGNED_SHORT; // Type of data in index buffer
const drawOffset = 0; // Start at the beginning of the index buffer
gl.drawElements(primitiveType, count, indexType, drawOffset);

Below is the conceptual structure of the HTML and JavaScript file. You would fill this in with the code snippets from the steps above, including robust error checking.

<!doctype html>
<html>
<head>
<title>WebGL Triangle</title>
<meta charset="utf-8">
</head>
<body>
<canvas id="glCanvas" width="640" height="480"></canvas>
<script>
// --- Helper function: loadShader ---
function loadShader(gl, type, source) { /* ... see previous chapter ... */ }
// --- Helper function: initShaderProgram ---
function initShaderProgram(gl, vsSource, fsSource) { /* ... see previous chapter ... */ }
// --- Main function ---
function main() {
// Step 1: Get Canvas & Context
const canvas = document.getElementById('glCanvas');
const gl = canvas.getContext('webgl');
if (!gl) { /* handle error */ return; }
// Step 3: Define Shader Sources
const vsSource = `...`; // Vertex shader GLSL code
const fsSource = `...`; // Fragment shader GLSL code
// Step 3: Compile and Link Shaders
const shaderProgram = initShaderProgram(gl, vsSource, fsSource);
if (!shaderProgram) { /* handle error */ return; }
// Step 2: Define Geometry
const positions = [/* ... vertex data ... */];
const indices = [/* ... index data ... */];
// Step 2: Create and Load Buffers
const positionBuffer = gl.createBuffer();
/* ... bind and bufferData for positions ... */
const indexBuffer = gl.createBuffer();
/* ... bind and bufferData for indices ... */
// Step 4: Associate Shaders and Buffers
const positionAttributeLocation = gl.getAttribLocation(shaderProgram, 'a_position');
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.vertexAttribPointer(/* ... parameters ... */);
gl.enableVertexAttribArray(positionAttributeLocation);
// Step 5: Set Up Render State and Draw
gl.clearColor(0.8, 0.8, 0.8, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
gl.useProgram(shaderProgram);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
gl.drawElements(gl.TRIANGLES, indices.length, gl.UNSIGNED_SHORT, 0);
}
// Run the main function when the page loads
window.onload = main;
</script>
</body>
</html>

Running this code in a WebGL-enabled browser should display a single, solid orange triangle centered horizontally, pointing upwards, against a light gray background.