Subchapter 178.1
references/3d-web-games.mdMarkdown24 KBView on GitHub
A comprehensive reference for building 3D games on the web, covering foundational theory, major frameworks, shader programming, collision detection, and immersive WebXR experiences.
Sources: MDN Web Docs – Games Techniques: 3D on the web (opens in a new tab)
Understanding the core concepts behind 3D rendering is essential before working with any framework.
WebGL uses the right-hand coordinate system:
All 3D objects are positioned relative to this coordinate system.
(x, y, z) with additional attributes: color (RGBA, values 0.0-1.0), normal (direction the vertex faces, used for lighting), and texture coordinates.The pipeline transforms 3D objects into 2D pixels on screen, in four major stages:
1. Vertex Processing
Combines individual vertex data into primitives (triangles, lines, points) and applies transformations:
2. Rasterization
Converts 3D primitives into 2D fragments aligned to the pixel grid.
3. Fragment Processing
Determines the final color of each fragment using textures and lighting:
4. Output Merging
Converts 3D fragments into the final 2D pixel grid. Off-screen and occluded objects are culled for efficiency.
The camera defines what is visible:
Three.js is one of the most popular 3D engines for the web. It provides a high-level API over WebGL with a large ecosystem of plugins, examples, and community support.
<!doctype html>
<html lang="en-GB">
<head>
<meta charset="utf-8" />
<title>Three.js Demo</title>
<style>
html, body, canvas {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
font-size: 0;
}
</style>
</head>
<body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r79/three.min.js"></script>
<script>
const WIDTH = window.innerWidth;
const HEIGHT = window.innerHeight;
/* all code goes here */
</script>
</body>
</html>Or install via npm:
npm install --save three
npm install --save-dev vite
npx viteRenderer – displays the scene in the browser:
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(WIDTH, HEIGHT);
renderer.setClearColor(0xdddddd, 1);
document.body.appendChild(renderer.domElement);Scene – container for all 3D objects, lights, and the camera:
const scene = new THREE.Scene();Camera – defines the viewpoint (PerspectiveCamera is most common):
const camera = new THREE.PerspectiveCamera(70, WIDTH / HEIGHT);
camera.position.z = 50;
scene.add(camera);Parameters: field of view (degrees), aspect ratio. Other camera types include Orthographic and Cube.
// Geometry defines the shape
const boxGeometry = new THREE.BoxGeometry(10, 10, 10);
const torusGeometry = new THREE.TorusGeometry(7, 1, 16, 32);
const dodecahedronGeometry = new THREE.DodecahedronGeometry(7);
// Material defines the surface appearance
const basicMaterial = new THREE.MeshBasicMaterial({ color: 0x0095dd }); // No lighting
const phongMaterial = new THREE.MeshPhongMaterial({ color: 0xff9500 }); // Glossy
const lambertMaterial = new THREE.MeshLambertMaterial({ color: 0xeaeff2 }); // Matte
// Mesh combines geometry + material
const cube = new THREE.Mesh(boxGeometry, basicMaterial);
cube.position.set(-25, 0, 0);
cube.rotation.set(0.4, 0.2, 0);
scene.add(cube);const light = new THREE.PointLight(0xffffff);
light.position.set(-10, 15, 50);
scene.add(light);Other light types: Ambient, Directional, Hemisphere, Spot.
Note: MeshBasicMaterial does not respond to lighting. Use MeshPhongMaterial or MeshLambertMaterial for lit surfaces.
let t = 0;
function render() {
t += 0.01;
requestAnimationFrame(render);
cube.rotation.y += 0.01; // continuous rotation
torus.scale.y = Math.abs(Math.sin(t)); // pulsing scale
dodecahedron.position.y = -7 * Math.sin(t * 2); // bobbing position
renderer.render(scene, camera);
}
render();Math.abs() when animating scale with Math.sin() to avoid negative scale values.requestAnimationFrame for smooth, browser-optimized frame updates.Babylon.js is a full-featured 3D engine with a built-in math library, physics support, and extensive documentation.
<script src="https://cdn.babylonjs.com/v7.34.1/babylon.js"></script>
<canvas id="render-canvas"></canvas>const canvas = document.getElementById("render-canvas");
const engine = new BABYLON.Engine(canvas);
const scene = new BABYLON.Scene(engine);
scene.clearColor = new BABYLON.Color3(0.8, 0.8, 0.8);
function renderLoop() {
scene.render();
}
engine.runRenderLoop(renderLoop);const camera = new BABYLON.FreeCamera("camera", new BABYLON.Vector3(0, 0, -10), scene);
const light = new BABYLON.PointLight("light", new BABYLON.Vector3(10, 10, 0), scene);const box = BABYLON.Mesh.CreateBox("box", 2, scene); // name, size, scene
const torus = BABYLON.Mesh.CreateTorus("torus", 2, 0.5, 15, scene); // name, diameter, thickness, tessellation, scene
const cylinder = BABYLON.Mesh.CreateCylinder("cylinder", 2, 2, 2, 12, 1, scene);
// name, height, topDiameter, bottomDiameter, tessellation, heightSubdivisions, sceneconst boxMaterial = new BABYLON.StandardMaterial("material", scene);
boxMaterial.emissiveColor = new BABYLON.Color3(0, 0.58, 0.86);
box.material = boxMaterial;box.position.x = 5;
box.rotation.x = -0.2;
box.scaling.x = 1.5;
// Animation inside render loop
let t = 0;
function renderLoop() {
scene.render();
t -= 0.01;
box.rotation.y = t * 2;
torus.scaling.z = Math.abs(Math.sin(t * 2)) + 0.5;
cylinder.position.y = Math.sin(t * 3);
}
engine.runRenderLoop(renderLoop);BABYLON global object contains all framework functions.BABYLON.Vector3 and BABYLON.Color3 are used extensively for positioning and coloring.A-Frame is Mozilla’s declarative, HTML-based framework for building VR/AR experiences on the web. It uses an entity-component system and runs on WebGL under the hood.
<!doctype html>
<html lang="en-US">
<head>
<meta charset="utf-8" />
<title>A-Frame Demo</title>
<script src="https://aframe.io/releases/1.6.0/aframe.min.js"></script>
<style>
body { margin: 0; padding: 0; width: 100%; height: 100%; font-size: 0; }
</style>
</head>
<body>
<a-scene>
<!-- entities go here -->
</a-scene>
</body>
</html>The <a-scene> element is the root container. A-Frame auto-includes a default camera, lighting, and input controls.
<!-- Built-in primitive shapes -->
<a-box position="0 1 -3" rotation="0 10 0" color="#4CC3D9"></a-box>
<a-sky color="#DDDDDD"></a-sky>
<!-- Generic entity with explicit geometry and material -->
<a-entity
geometry="primitive: torus; radius: 1; radiusTubular: 0.1; segmentsTubular: 12;"
material="color: #EAEFF2; roughness: 0.1; metalness: 0.5;"
rotation="10 0 0"
position="-3 1 0">
</a-entity>const scene = document.querySelector("a-scene");
const cylinder = document.createElement("a-cylinder");
cylinder.setAttribute("color", "#FF9500");
cylinder.setAttribute("height", "2");
cylinder.setAttribute("radius", "0.75");
cylinder.setAttribute("position", "3 1 0");
scene.appendChild(cylinder);<a-camera position="0 1 4" cursor-visible="true" cursor-color="#0095DD" cursor-opacity="0.5">
</a-camera>
<a-light type="directional" color="white" intensity="0.5" position="-1 1 2"></a-light>
<a-light type="ambient" color="white"></a-light>Default controls: WASD keys for movement, mouse for looking around. A VR mode button appears in the bottom-right corner.
Declarative animation via HTML attributes:
<a-box
color="#0095DD"
rotation="20 40 0"
position="0 1 0"
animation="property: rotation; from: 20 0 0; to: 20 360 0;
dir: alternate; loop: true; dur: 4000; easing: easeInOutQuad;">
</a-box>Animation properties: property (attribute to animate), from/to (start/end values), dir (alternate or normal), loop (boolean), dur (milliseconds), easing (easing function).
Dynamic animation via JavaScript:
let t = 0;
function render() {
t += 0.01;
requestAnimationFrame(render);
cylinder.setAttribute("position", `3 ${Math.sin(t * 2) + 1} 0`);
}
render();<a-sky> for background colors or 360-degree images.PlayCanvas is a WebGL game engine with two workflow options:
GLSL (OpenGL Shading Language) is a C-like language that runs directly on the GPU, enabling custom control over the rendering pipeline’s vertex and fragment processing stages.
Shaders are small programs that execute on the GPU instead of the CPU. They are strongly typed and rely heavily on vector and matrix mathematics. There are two types relevant to WebGL:
The vertex shader’s job is to set gl_Position, a built-in GLSL variable storing the vertex’s transformed position:
void main() {
gl_Position = projectionMatrix * modelViewMatrix * vec4(position.x, position.y, position.z, 1.0);
}projectionMatrix – handles perspective or orthographic projection (provided by Three.js).modelViewMatrix – combines model and view transformations (provided by Three.js).vec4(x, y, z, w) – a 4-component vector; w defaults to 1.0 for positional vertices.You can manipulate vertices directly:
void main() {
gl_Position = projectionMatrix * modelViewMatrix * vec4(position.x + 10.0, position.y, position.z + 5.0, 1.0);
}The fragment shader’s job is to set gl_FragColor, a built-in GLSL variable holding the RGBA color:
void main() {
gl_FragColor = vec4(0.0, 0.58, 0.86, 1.0);
}RGBA components are floats from 0.0 to 1.0. Alpha 0.0 is fully transparent; 1.0 is fully opaque.
Embed shader source in script tags with custom type attributes:
<script id="vertexShader" type="x-shader/x-vertex">
void main() {
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
</script>
<script id="fragmentShader" type="x-shader/x-fragment">
void main() {
gl_FragColor = vec4(0.0, 0.58, 0.86, 1.0);
}
</script>Apply them with ShaderMaterial:
const shaderMaterial = new THREE.ShaderMaterial({
vertexShader: document.getElementById("vertexShader").textContent,
fragmentShader: document.getElementById("fragmentShader").textContent,
});
const cube = new THREE.Mesh(boxGeometry, shaderMaterial);gl_Position.gl_FragColor.1.0 instead of 1 for floats.Collision detection determines when 3D objects intersect, which is fundamental for game physics, interaction, and gameplay logic.
An AABB wraps an object in a non-rotated rectangular box aligned to the coordinate axes. It is the fastest common collision test because it uses only logical comparisons (no trigonometry).
Limitation: AABBs do not rotate with the object. For rotating entities, either resize the bounding box each frame or use bounding spheres instead.
Check whether a point lies inside a box by testing all three axes:
function isPointInsideAABB(point, box) {
return (
point.x >= box.minX &&
point.x <= box.maxX &&
point.y >= box.minY &&
point.y <= box.maxY &&
point.z >= box.minZ &&
point.z <= box.maxZ
);
}Check whether two boxes overlap on all three axes:
function intersect(a, b) {
return (
a.minX <= b.maxX &&
a.maxX >= b.minX &&
a.minY <= b.maxY &&
a.maxY >= b.minY &&
a.minZ <= b.maxZ &&
a.maxZ >= b.minZ
);
}Bounding spheres are invariant to rotation (the sphere stays the same regardless of how the object spins), which makes them ideal for rotating entities. However, they fit poorly on non-spherical shapes and cause more false positives.
Check whether the distance from the point to the sphere center is less than the radius:
function isPointInsideSphere(point, sphere) {
const distance = Math.sqrt(
(point.x - sphere.x) ** 2 +
(point.y - sphere.y) ** 2 +
(point.z - sphere.z) ** 2
);
return distance < sphere.radius;
}Performance optimization: avoid the square root by comparing squared distances:
const distanceSqr =
(point.x - sphere.x) ** 2 +
(point.y - sphere.y) ** 2 +
(point.z - sphere.z) ** 2;
return distanceSqr < sphere.radius * sphere.radius;Check whether the distance between centers is less than the sum of radii:
function intersect(sphere, other) {
const distance = Math.sqrt(
(sphere.x - other.x) ** 2 +
(sphere.y - other.y) ** 2 +
(sphere.z - other.z) ** 2
);
return distance < sphere.radius + other.radius;
}Find the point on the AABB closest to the sphere center by clamping, then check the distance:
function intersect(sphere, box) {
const x = Math.max(box.minX, Math.min(sphere.x, box.maxX));
const y = Math.max(box.minY, Math.min(sphere.y, box.maxY));
const z = Math.max(box.minZ, Math.min(sphere.z, box.maxZ));
const distance = Math.sqrt(
(x - sphere.x) ** 2 +
(y - sphere.y) ** 2 +
(z - sphere.z) ** 2
);
return distance < sphere.radius;
}Three.js provides built-in Box3 and Sphere objects plus visual helpers for bounding volume collision detection.
// Box3 from an object (recommended -- accounts for transforms and children)
const knotBBox = new THREE.Box3(new THREE.Vector3(), new THREE.Vector3());
knotBBox.setFromObject(knot);
// Sphere from geometry
const knotBSphere = new THREE.Sphere(
knot.position,
knot.geometry.boundingSphere.radius
);Important: setFromObject() accounts for position, rotation, scale, and child meshes. The geometry’s boundingBox property does not.
// Point inside box or sphere
knotBBox.containsPoint(point);
knotBSphere.containsPoint(point);
// Box vs. box
knotBBox.intersectsBox(otherBox);
// Sphere vs. sphere
knotBSphere.intersectsSphere(otherSphere);Note: containsBox() checks if one box fully encloses another, which is different from intersectsBox().
Three.js does not natively provide sphere-vs-box testing. Add it manually:
THREE.Sphere.__closest = new THREE.Vector3();
THREE.Sphere.prototype.intersectsBox = function (box) {
THREE.Sphere.__closest.set(this.center.x, this.center.y, this.center.z);
THREE.Sphere.__closest.clamp(box.min, box.max);
const distance = this.center.distanceToSquared(THREE.Sphere.__closest);
return distance < this.radius * this.radius;
};BoxHelper creates a visible wireframe bounding box around any mesh and simplifies updates:
const knotBoxHelper = new THREE.BoxHelper(knot, 0x00ff00);
scene.add(knotBoxHelper);
// After moving or rotating the mesh, update the helper
knot.position.set(-3, 2, 1);
knot.rotation.x = -Math.PI / 4;
knotBoxHelper.update();
// Convert to Box3 for intersection tests
const box3 = new THREE.Box3();
box3.setFromObject(knotBoxHelper);
box3.intersectsBox(otherBox3);Advantages of BoxHelper: auto-resizes with update(), includes child meshes, provides visual debugging. Limitation: box volumes only (no sphere helpers).
For more sophisticated collision detection and response, use a physics engine:
Physics engines create a physical body attached to the visual mesh, with properties like velocity, position, rotation, and torque. A physical shape (box, sphere, convex hull) is used for collision calculations.
Math.sqrt() in tight loops; compare squared distances instead.WebXR is the modern web API for building virtual reality (VR) and augmented reality (AR) experiences in the browser. It replaces the deprecated WebVR API.
The WebXR Device API provides access to XR hardware (headsets, controllers) and enables stereoscopic rendering. It captures real-time data including:
Every WebXR experience requires two things:
All major 3D web frameworks support WebXR:
renderer.xr. See Three.js VR documentation (opens in a new tab).This file
Nearby