3D Projection Fundamentals
A cube rotates in 3D space while you control the camera — watch how vertices travel through matrices to reach the screen.
Every 3D scene you see is a lie. Vertices transform through matrices, faces catch light, and a 2D image emerges from multiplication. This demo shows the pipeline: model matrix places objects in the world, view matrix positions the camera, projection matrix flattens depth. Drag the sliders to see how each transformation shapes the final image.
Object Rotation
Camera Position
Projection
The Transformation Pipeline
Every 3D object starts as vertices in its own coordinate space — object space. To appear on your screen, each vertex travels through three matrix multiplications: the model matrix places the object in the world, the view matrix positions the camera, and the projection matrix flattens 3D onto 2D. The cube above shows this pipeline in action. Drag the sliders to see how each transformation affects the final image.
Object Space to World Space: The Model Matrix
A cube defined in object space has vertices at (±1, ±1, ±1) — eight corners relative to its own
center. The model matrix transforms these to world coordinates: rotating, scaling, and translating the object
into position. Without transformations, all objects would sit at the origin, overlapping each other.
The model matrix combines rotation around each axis. For rotation around the Y-axis by angle θ:
Ry(θ) = [[cos(θ), 0, sin(θ), 0], [0, 1, 0, 0], [-sin(θ), 0, cos(θ), 0], [0, 0, 0, 1]]
Similar matrices exist for X and Z rotations. The final model matrix is the product Rz × Ry × Rx — rotation order matters. This matrix maps each vertex from object coordinates to world coordinates.
// Build model matrix from Euler rotations
function buildModelMatrix(rx, ry, rz, scale) {
const cx = Math.cos(rx), sx = Math.sin(rx);
const cy = Math.cos(ry), sy = Math.sin(ry);
const cz = Math.cos(rz), sz = Math.sin(rz);
// Rotation order: Z * Y * X (applied right-to-left)
return [
[cy*cz, -cy*sz, sy, 0],
[sx*sy*cz + cx*sz, -sx*sy*sz + cx*cz, -sx*cy, 0],
[-cx*sy*cz + sx*sz, cx*sy*sz + sx*cz, cx*cy, 0],
[0, 0, 0, scale]
];
}
World Space to Camera Space: The View Matrix
The camera sits somewhere in the world, looking in some direction. The view matrix transforms world coordinates so the camera sits at the origin, looking down the negative Z-axis. This is conceptually inverse — instead of moving the camera to the origin, we move the entire world so the camera appears fixed.
If the camera is at position (camX, camY, camZ) looking toward the origin, the view matrix
first translates everything by -cameraPosition, then rotates to align the look direction
with -Z. The combined view matrix is often called the "camera transform" or "look-at" matrix.
function buildViewMatrix(camPos, target, up) {
// Forward direction (camera looks toward target)
const f = normalize(subtract(target, camPos));
// Right direction (perpendicular to forward and up)
const r = normalize(cross(f, up));
// True up direction (orthonormal to forward and right)
const u = cross(r, f);
// Camera-space coordinate system as columns, then translate
return [
[r[0], u[0], -f[0], 0],
[r[1], u[1], -f[1], 0],
[r[2], u[2], -f[2], 0],
[-dot(r, camPos), -dot(u, camPos), dot(f, camPos), 1]
];
}
The view matrix positions every vertex in camera space, where the camera is at the origin and the view direction aligns with -Z. Objects in front of the camera have positive Z values in world space, which become negative Z values in camera space.
Camera Space to Clip Space: The Projection Matrix
The projection matrix converts 3D camera coordinates to 2D screen coordinates while preserving depth for hidden surface removal. Perspective projection makes distant objects appear smaller — lines that are parallel in 3D converge in the projected image.
The field of view (FOV) defines how much of the world the camera sees. A 60° FOV shows a moderate view (slightly wide-angle). Higher FOV values show more but distort edges (fisheye effect). Lower values show less but feel telephoto.
function buildProjectionMatrix(fov, aspect, near, far) {
const f = 1.0 / Math.tan(fov * Math.PI / 360); // fov/2 in radians
const rangeInv = 1 / (near - far);
return [
[f/aspect, 0, 0, 0],
[0, f, 0, 0],
[0, 0, (near + far) * rangeInv, -1],
[0, 0, near * far * rangeInv * 2, 0]
];
}
This matrix maps the viewing frustum (a truncated pyramid extending from the camera) to the normalized clip space cube (-1 to +1 on X, Y, and Z). Points outside this cube are clipped — they don't appear on screen.
Vertices and Faces: The Geometry Pipeline
A 3D model is defined by vertices (points in space) and faces (triangles connecting those points). A cube has 8 vertices and 6 faces, each face being a square made of 2 triangles. The pipeline applies the combined MVP (Model × View × Projection) matrix to each vertex, producing clip-space coordinates.
const cubeVertices = [ [-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] // front face ]; const cubeFaces = [ [0, 1, 2, 3], // back (Z = -1) [4, 5, 6, 7], // front (Z = +1) [0, 4, 7, 3], // left (X = -1) [1, 5, 6, 2], // right (X = +1) [3, 2, 6, 7], // top (Y = +1) [0, 1, 5, 4] // bottom (Y = -1) ]; // Each face is drawn by traversing 4 vertices (as 2 triangles) // ...in the render function
After projection, the x and y coordinates map directly to screen pixels,
while w (from the projection matrix) is used for perspective division. Dividing
x, y, and z by w produces normalized device
coordinates that map to screen space.
Face Normals and Shading
Each face has a normal vector — a perpendicular arrow pointing away from the surface. The normal defines which direction the face is pointing, used for lighting calculations. A face whose normal points away from the camera (dot product with view direction > 0) is back-facing and typically not drawn — this is backface culling.
function computeFaceNormal(v0, v1, v2) {
// Two edge vectors on the face
const edge1 = sub(v1, v0);
const edge2 = sub(v2, v0);
// Cross product gives perpendicular vector
return normalize(cross(edge1, edge2));
}
function faceBrightness(normal, lightDir) {
// Lambertian diffuse shading
// dot product of normal and light direction
const d = Math.max(0, -dot(normal, lightDir));
return 0.3 + 0.7 * d; // ambient + diffuse
}
The brightness of each face depends on its angle relative to the light source. Faces pointing directly toward the light appear brightest. Faces angled away receive less light. Faces pointing away are in shadow. This simple model — Lambertian diffuse shading — creates the illusion of 3D form from flat lighting.
Light Sources: Directional Lighting
The light in this demo is a directional light — an infinitely distant source like the sun. The light direction is constant everywhere, defined by an angle rotating around the scene. Directional lights are simpler than point lights (which attenuate with distance) and spotlights (which have direction and falloff), but they capture the essential lighting calculation: how does surface orientation affect brightness?
// Light direction rotates around the scene const lightAngle = lightAngleSlider * Math.PI / 180; const lightDir = [ Math.sin(lightAngle), -0.707, // pointing somewhat downward Math.cos(lightAngle) ]; // For each visible face const brightness = faceBrightness(faceNormal, lightDir); const color = interpolateColor(faceBaseColor, brightness);
The light angle slider changes the light direction in real-time — watch how faces brighten when the light rotates toward their normal direction. This is the core of real-time 3D lighting: computing brightness from the geometric relationship between surface orientation and light direction.
What This Reveals
3D rendering is geometry in motion: vertices transform through matrices, faces catch light, and a 2D image emerges from 4×4 multiplication. The pipeline — model, view, projection — is universal. Every 3D engine from games to CAD uses this same transformation sequence. The cube above demonstrates what GPUs do millions of times per second: take a vertex, multiply by three matrices, divide by W, draw the point. Understanding this pipeline demystifies 3D graphics. There is no magic — only linear algebra applied consistently.