Skip to content

WebGL - Cube Rotation

This chapter demonstrates drawing a rotating 3D cube using WebGL, incorporating modern JavaScript practices and a standard math library.

Manually coding matrix operations (projection, view, model transformations, rotations) is error-prone and verbose. In practice, developers use well-tested math libraries. A popular choice is gl-matrix. We’ll use it in this example. You’d typically include it via a script tag or import it if using a module bundler.

The following HTML and JavaScript code set up a WebGL canvas and draw a continuously rotating colored cube.

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>WebGL Rotating Cube</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gl-matrix/3.3.0/gl-matrix-min.js"></script>
<style>canvas { border: 1px solid black; }</style>
</head>
<body>
<canvas width="570" height="570" id="my_Canvas"></canvas>
<script>
// Ensure glMatrix library is available
const { mat4, vec3 } = glMatrix;
/*============= Creating a canvas =================*/
const canvas = document.getElementById('my_Canvas');
const gl = canvas.getContext('webgl'); // Use 'webgl' or 'webgl2'
if (!gl) {
alert('WebGL not supported!');
throw new Error('WebGL not supported');
}
/*============ Defining and storing the geometry =========*/
// Define vertices for the cube faces
const vertices = [
// Front face
-1.0, -1.0, 1.0, 1.0, -1.0, 1.0, 1.0, 1.0, 1.0, -1.0, 1.0, 1.0,
// Back face
-1.0, -1.0, -1.0, -1.0, 1.0, -1.0, 1.0, 1.0, -1.0, 1.0, -1.0, -1.0,
// Top face
-1.0, 1.0, -1.0, -1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, -1.0,
// Bottom face
-1.0, -1.0, -1.0, 1.0, -1.0, -1.0, 1.0, -1.0, 1.0, -1.0, -1.0, 1.0,
// Right face
1.0, -1.0, -1.0, 1.0, 1.0, -1.0, 1.0, 1.0, 1.0, 1.0, -1.0, 1.0,
// Left face
-1.0, -1.0, -1.0, -1.0, -1.0, 1.0, -1.0, 1.0, 1.0, -1.0, 1.0, -1.0,
];
// Define colors for each vertex (normalized R, G, B)
const faceColors = [
[1.0, 0.0, 0.0], // Front face: red
[0.0, 1.0, 0.0], // Back face: green
[0.0, 0.0, 1.0], // Top face: blue
[1.0, 1.0, 0.0], // Bottom face: yellow
[1.0, 0.0, 1.0], // Right face: magenta
[0.0, 1.0, 1.0], // Left face: cyan
];
// Convert face colors to per-vertex colors
let colors = [];
for (let j = 0; j < faceColors.length; ++j) {
const c = faceColors[j];
// Repeat each color four times for the four vertices of the face
colors = colors.concat(c, c, c, c);
}
// Define indices for the cube triangles
const indices = [
0, 1, 2, 0, 2, 3, // front
4, 5, 6, 4, 6, 7, // back
8, 9, 10, 8, 10, 11, // top
12, 13, 14, 12, 14, 15, // bottom
16, 17, 18, 16, 18, 19, // right
20, 21, 22, 20, 22, 23, // left
];
// --- Create Buffers ---
const vertexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
const colorBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STATIC_DRAW);
const indexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW);
/*=================== Shaders =========================*/
// Using GLSL ES 1.00 (standard for WebGL 1)
const vsSource = `
attribute vec4 aVertexPosition;
attribute vec3 aVertexColor;
uniform mat4 uModelViewMatrix;
uniform mat4 uProjectionMatrix;
varying lowp vec3 vColor;
void main(void) {
gl_Position = uProjectionMatrix * uModelViewMatrix * aVertexPosition;
vColor = aVertexColor;
}
`;
const fsSource = `
varying lowp vec3 vColor;
void main(void) {
gl_FragColor = vec4(vColor, 1.0);
}
`;
// Helper function to compile shader
function loadShader(gl, type, source) {
const shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
alert('An error occurred compiling the shaders: ' + gl.getShaderInfoLog(shader));
gl.deleteShader(shader);
return null;
}
return shader;
}
const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vsSource);
const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fsSource);
// Create shader program
const shaderProgram = gl.createProgram();
gl.attachShader(shaderProgram, vertexShader);
gl.attachShader(shaderProgram, fragmentShader);
gl.linkProgram(shaderProgram);
if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
alert('Unable to initialize the shader program: ' + gl.getProgramInfoLog(shaderProgram));
throw new Error('Shader program linking failed');
}
// Store attribute and uniform locations
const programInfo = {
program: shaderProgram,
attribLocations: {
vertexPosition: gl.getAttribLocation(shaderProgram, 'aVertexPosition'),
vertexColor: gl.getAttribLocation(shaderProgram, 'aVertexColor'),
},
uniformLocations: {
projectionMatrix: gl.getUniformLocation(shaderProgram, 'uProjectionMatrix'),
modelViewMatrix: gl.getUniformLocation(shaderProgram, 'uModelViewMatrix'),
},
};
/* ====== Associating attributes to vertex shader =====*/
// Tell WebGL how to pull out the positions from the vertex buffer
{
const numComponents = 3; // pull out 3 values per iteration (x, y, z)
const type = gl.FLOAT; // the data in the buffer is 32bit floats
const normalize = false; // don't normalize
const stride = 0; // how many bytes to get from one set of values to the next
// 0 = use type and numComponents above
const offset = 0; // how many bytes inside the buffer to start from
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
gl.vertexAttribPointer(
programInfo.attribLocations.vertexPosition,
numComponents,
type,
normalize,
stride,
offset);
gl.enableVertexAttribArray(programInfo.attribLocations.vertexPosition);
}
// Tell WebGL how to pull out the colors from the color buffer
{
const numComponents = 3; // R, G, B
const type = gl.FLOAT;
const normalize = false;
const stride = 0;
const offset = 0;
gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
gl.vertexAttribPointer(
programInfo.attribLocations.vertexColor,
numComponents,
type,
normalize,
stride,
offset);
gl.enableVertexAttribArray(programInfo.attribLocations.vertexColor);
}
// Tell WebGL which indices to use to index the vertices
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
gl.useProgram(programInfo.program);
/*==================== MATRIX Setup =====================*/
const fieldOfView = 45 * Math.PI / 180; // in radians
const aspect = gl.canvas.clientWidth / gl.canvas.clientHeight;
const zNear = 0.1;
const zFar = 100.0;
const projectionMatrix = mat4.create();
mat4.perspective(projectionMatrix, fieldOfView, aspect, zNear, zFar);
const modelViewMatrix = mat4.create();
// Move the drawing position a bit where we can see it
mat4.translate(modelViewMatrix, modelViewMatrix, [-0.0, 0.0, -6.0]);
/*================= Drawing ===========================*/
let lastTime = 0;
let cubeRotation = 0.0;
function render(now) {
now *= 0.001; // convert to seconds
const deltaTime = now - lastTime;
lastTime = now;
// Rotate the cube
mat4.rotate(modelViewMatrix, // destination matrix
modelViewMatrix, // matrix to rotate
cubeRotation, // amount to rotate in radians
[0, 0, 1]); // axis to rotate around (Z)
mat4.rotate(modelViewMatrix, modelViewMatrix, cubeRotation * .7, [0, 1, 0]); // Y
mat4.rotate(modelViewMatrix, modelViewMatrix, cubeRotation * .3, [1, 0, 0]); // X
cubeRotation += deltaTime;
// --- Drawing ---
gl.clearColor(0.5, 0.5, 0.5, 0.9); // Clear to grey, fully opaque
gl.clearDepth(1.0); // Clear everything
gl.enable(gl.DEPTH_TEST); // Enable depth testing
gl.depthFunc(gl.LEQUAL); // Near things obscure far things
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
// Set the shader uniforms
gl.uniformMatrix4fv(
programInfo.uniformLocations.projectionMatrix,
false,
projectionMatrix);
gl.uniformMatrix4fv(
programInfo.uniformLocations.modelViewMatrix,
false,
modelViewMatrix);
{
const vertexCount = 36; // 6 faces * 2 triangles/face * 3 vertices/triangle
const type = gl.UNSIGNED_SHORT;
const offset = 0;
gl.drawElements(gl.TRIANGLES, vertexCount, type, offset);
}
requestAnimationFrame(render);
}
requestAnimationFrame(render);
</script>
</body>
</html>

Expected Result:

This code will render a 3D cube with different colored faces, continuously rotating around multiple axes in the center of the canvas.