Build an HTML5 Rotating Cube With WebGL

3D Rotating Cube in WebGL
3D Rotating Cube in WebGL

Create Your First 3D Rotating Cube Using WebGL and HTML5

Are you curious about how to make interactive 3D graphics right in your browser? With HTML5 and WebGL, you can create stunning 3D objects without installing any extra software! Today, we’ll explore how to build a simple rotating cube using WebGL – perfect for beginners eager to dive into the world of 3D programming with JavaScript.

What is WebGL?

WebGL (Web Graphics Library) is a JavaScript API that lets you render 3D graphics inside the browser, using hardware acceleration. Unlike regular 2D canvas, WebGL taps directly into your GPU for smooth and powerful visuals.

The Rotating Cube Example

<canvas id="glcanvas"></canvas>

<script>
// --- Setup ---
const canvas = document.getElementById('glcanvas');
const gl = canvas.getContext('webgl');
if (!gl) alert('WebGL not supported');

function resize() {
  canvas.width = window.innerWidth;
  canvas.height = window.innerHeight;
  gl.viewport(0, 0, canvas.width, canvas.height);
}
window.addEventListener('resize', resize);
resize();

// --- Vertex shader ---
const vsSource = `
attribute vec3 a_position;
attribute vec3 a_normal;

uniform mat4 u_modelViewMatrix;
uniform mat4 u_projectionMatrix;
uniform mat4 u_normalMatrix;

varying vec3 v_normal;
varying vec3 v_position;

void main() {
  v_position = (u_modelViewMatrix * vec4(a_position, 1.0)).xyz;
  v_normal = mat3(u_normalMatrix) * a_normal;
  gl_Position = u_projectionMatrix * vec4(v_position, 1.0);
}
`;

// --- Fragment shader ---
const fsSource = `
precision mediump float;

varying vec3 v_normal;
varying vec3 v_position;

uniform vec3 u_lightPos;
uniform vec3 u_viewPos;
uniform vec4 u_color;

void main() {
  vec3 normal = normalize(v_normal);
  vec3 lightDir = normalize(u_lightPos - v_position);
  vec3 viewDir = normalize(u_viewPos - v_position);

  // Diffuse lighting
  float diff = max(dot(normal, lightDir), 0.0);

  // Specular lighting (Phong)
  vec3 reflectDir = reflect(-lightDir, normal);
  float spec = pow(max(dot(viewDir, reflectDir), 0.0), 64.0);

  // Ambient + diffuse + specular
  vec3 ambient = 0.1 * u_color.rgb;
  vec3 diffuse = diff * u_color.rgb;
  vec3 specular = spec * vec3(1.0);

  vec3 color = ambient + diffuse + specular;

  // Alpha blending for glass transparency
  float alpha = 0.4;

  gl_FragColor = vec4(color, alpha);
}
`;

// --- Shader compilation helpers ---
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 compile failed:', gl.getShaderInfoLog(shader));
    gl.deleteShader(shader);
    return null;
  }
  return shader;
}

function createProgram(gl, vs, fs) {
  const program = gl.createProgram();
  gl.attachShader(program, vs);
  gl.attachShader(program, fs);
  gl.linkProgram(program);
  if(!gl.getProgramParameter(program, gl.LINK_STATUS)) {
    console.error('Program link failed:', gl.getProgramInfoLog(program));
    return null;
  }
  return program;
}

const vertexShader = createShader(gl, gl.VERTEX_SHADER, vsSource);
const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fsSource);
const program = createProgram(gl, vertexShader, fragmentShader);
gl.useProgram(program);

// --- Cube Data ---
const positions = new Float32Array([
  // Front face
  -1, -1,  1,
   1, -1,  1,
   1,  1,  1,
  -1,  1,  1,
  // Back face
  -1, -1, -1,
  -1,  1, -1,
   1,  1, -1,
   1, -1, -1,
  // Top face
  -1,  1, -1,
  -1,  1,  1,
   1,  1,  1,
   1,  1, -1,
  // Bottom face
  -1, -1, -1,
   1, -1, -1,
   1, -1,  1,
  -1, -1,  1,
  // Right face
   1, -1, -1,
   1,  1, -1,
   1,  1,  1,
   1, -1,  1,
  // Left face
  -1, -1, -1,
  -1, -1,  1,
  -1,  1,  1,
  -1,  1, -1,
]);

const normals = new Float32Array([
  // Front
  0, 0, 1,
  0, 0, 1,
  0, 0, 1,
  0, 0, 1,
  // Back
  0, 0, -1,
  0, 0, -1,
  0, 0, -1,
  0, 0, -1,
  // Top
  0, 1, 0,
  0, 1, 0,
  0, 1, 0,
  0, 1, 0,
  // Bottom
  0, -1, 0,
  0, -1, 0,
  0, -1, 0,
  0, -1, 0,
  // Right
  1, 0, 0,
  1, 0, 0,
  1, 0, 0,
  1, 0, 0,
  // Left
  -1, 0, 0,
  -1, 0, 0,
  -1, 0, 0,
  -1, 0, 0,
]);

const indices = new Uint16Array([
  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
]);

// --- Buffers ---
const posBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, posBuffer);
gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW);

const normBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, normBuffer);
gl.bufferData(gl.ARRAY_BUFFER, normals, gl.STATIC_DRAW);

const indexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW);

// --- Attributes ---
const posLoc = gl.getAttribLocation(program, 'a_position');
gl.enableVertexAttribArray(posLoc);
gl.bindBuffer(gl.ARRAY_BUFFER, posBuffer);
gl.vertexAttribPointer(posLoc, 3, gl.FLOAT, false, 0, 0);

const normLoc = gl.getAttribLocation(program, 'a_normal');
gl.enableVertexAttribArray(normLoc);
gl.bindBuffer(gl.ARRAY_BUFFER, normBuffer);
gl.vertexAttribPointer(normLoc, 3, gl.FLOAT, false, 0, 0);

// --- Uniforms ---
const uModelViewMatrixLoc = gl.getUniformLocation(program, 'u_modelViewMatrix');
const uProjectionMatrixLoc = gl.getUniformLocation(program, 'u_projectionMatrix');
const uNormalMatrixLoc = gl.getUniformLocation(program, 'u_normalMatrix');
const uLightPosLoc = gl.getUniformLocation(program, 'u_lightPos');
const uViewPosLoc = gl.getUniformLocation(program, 'u_viewPos');
const uColorLoc = gl.getUniformLocation(program, 'u_color');

// --- Matrices Helpers ---
function createProjectionMatrix(fov, aspect, near, far) {
  const f = 1.0 / Math.tan(fov / 2);
  const nf = 1 / (near - far);
  return new Float32Array([
    f / aspect, 0, 0, 0,
    0, f, 0, 0,
    0, 0, (far + near) * nf, -1,
    0, 0, (2 * far * near) * nf, 0,
  ]);
}

function multiplyMatrices(a, b) {
  const out = new Float32Array(16);
  for (let i=0; i<4; i++) {
    for (let j=0; j<4; j++) {
      out[i*4+j] =
        a[0*4+j]*b[i*4+0] +
        a[1*4+j]*b[i*4+1] +
        a[2*4+j]*b[i*4+2] +
        a[3*4+j]*b[i*4+3];
    }
  }
  return out;
}

function identityMatrix() {
  return new Float32Array([
    1,0,0,0,
    0,1,0,0,
    0,0,1,0,
    0,0,0,1
  ]);
}

function translationMatrix(tx, ty, tz) {
  const m = identityMatrix();
  m[12] = tx;
  m[13] = ty;
  m[14] = tz;
  return m;
}

function rotationMatrixX(angle) {
  const c = Math.cos(angle);
  const s = Math.sin(angle);
  return new Float32Array([
    1,0,0,0,
    0,c,s,0,
    0,-s,c,0,
    0,0,0,1
  ]);
}

function rotationMatrixY(angle) {
  const c = Math.cos(angle);
  const s = Math.sin(angle);
  return new Float32Array([
    c,0,-s,0,
    0,1,0,0,
    s,0,c,0,
    0,0,0,1
  ]);
}

function transposeInverse(m) {
  // Only for rotation/translation matrix (no scale)
  const r = new Float32Array(16);
  // upper 3x3 transpose
  r[0] = m[0]; r[1] = m[4]; r[2] = m[8];
  r[4] = m[1]; r[5] = m[5]; r[6] = m[9];
  r[8] = m[2]; r[9] = m[6]; r[10] = m[10];
  // last row/col
  r[3] = 0; r[7] = 0; r[11] = 0; r[15] = 1;
  // translation ignored
  return r;
}

// --- Interaction ---
let rotationX = 0;
let rotationY = 0;
let dragging = false;
let lastX, lastY;

canvas.addEventListener('mousedown', e => {
  dragging = true;
  lastX = e.clientX;
  lastY = e.clientY;
});
window.addEventListener('mouseup', e => dragging = false);
window.addEventListener('mousemove', e => {
  if (!dragging) return;
  const dx = e.clientX - lastX;
  const dy = e.clientY - lastY;
  rotationY += dx * 0.01;
  rotationX += dy * 0.01;
  lastX = e.clientX;
  lastY = e.clientY;
});

// --- Render loop ---
function render() {
  gl.clearColor(0.05, 0.05, 0.1, 1);
  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
  gl.enable(gl.DEPTH_TEST);
  gl.enable(gl.BLEND);
  gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);

  // Projection matrix
  const aspect = canvas.width / canvas.height;
  const projMatrix = createProjectionMatrix(Math.PI/4, aspect, 0.1, 100);

  // ModelView matrix (translate back, then rotate)
  let modelView = translationMatrix(0, 0, -6);
  modelView = multiplyMatrices(modelView, rotationMatrixX(rotationX));
  modelView = multiplyMatrices(modelView, rotationMatrixY(rotationY));

  // Normal matrix (transpose inverse of modelView)
  const normalMatrix = transposeInverse(modelView);

  // Set uniforms
  gl.uniformMatrix4fv(uProjectionMatrixLoc, false, projMatrix);
  gl.uniformMatrix4fv(uModelViewMatrixLoc, false, modelView);
  gl.uniformMatrix4fv(uNormalMatrixLoc, false, normalMatrix);

  gl.uniform3f(uLightPosLoc, 5, 5, 5);
  gl.uniform3f(uViewPosLoc, 0, 0, 0);
  gl.uniform4f(uColorLoc, 0.2, 0.6, 1.0, 0.4); // bluish glass

  gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
  gl.drawElements(gl.TRIANGLES, indices.length, gl.UNSIGNED_SHORT, 0);

  requestAnimationFrame(render);
}

requestAnimationFrame(render);
</script>

This example demonstrates a cube that you can rotate by dragging your mouse, with basic lighting and a glass-like transparency effect. It’s a great project to understand how vertices, shaders, and transformation matrices work together in WebGL.

Interactive Demo

Here’s a live example of the Rotatable Cube in action:

HTML5 Rotatable Cube Demo
HTML5 Rotatable Cube
Web Browser Displaying HTML5 Rotatable Cube

HTML5 Rotated Cube
Web Browser Displaying HTML5 Rotated Cube

HTML5 Rotatable Cube Video

Want to Learn More?

If you enjoyed this project and want to deepen your JavaScript skills, check out my resources:

  • Book: Learning JavaScript – a beginner-friendly guide to mastering JavaScript programming from the ground up.
  • Course: Learning JavaScript – an online course that complements the book with hands-on lessons and coding exercises.
  • One-on-One Tutorials: Interested in personalized help? I offer private programming tutorials including JavaScript. Reach out via my contact page to schedule your session.

Final Thoughts

Building 3D graphics with WebGL might seem intimidating at first, but with practice and the right guidance, you can create amazing interactive experiences right inside the browser. This rotating cube is just the beginning – keep experimenting, and happy coding!

Feel free to leave your questions or share your own WebGL projects in the comments below!

Recommended Resources:

Disclosure: Some of the links above are referral (affiliate) links. I may earn a commission if you purchase through them - at no extra cost to you.

About Edward

Edward is a software engineer, web developer, and author dedicated to helping people achieve their personal and professional goals through actionable advice and real-world tools.

As the author of impactful books including Learning JavaScript, Learning Python, Learning PHP, Mastering Blender Python API, and fiction The Algorithmic Serpent, Edward writes with a focus on personal growth, entrepreneurship, and practical success strategies. His work is designed to guide, motivate, and empower.

In addition to writing, Edward offers professional "full-stack development," "database design," "1-on-1 tutoring," "consulting sessions,", tailored to help you take the next step. Whether you are launching a business, developing a brand, or leveling up your mindset, Edward will be there to support you.

Edward also offers online courses designed to deepen your learning and accelerate your progress. Explore the programming on languages like JavaScript, Python and PHP to find the perfect fit for your journey.

📚 Explore His Books – Visit the Book Shop to grab your copies today.
💼 Need Support? – Learn more about Services and the ways to benefit from his expertise.
🎓 Ready to Learn? – Check out his Online Courses to turn your ideas into results.