This article makes use of SVG images and the HTML canvas element, some older browsers are unable to support these features. Check your browser at html5test.com

Imagine you are in a situation where you have a 3D object you want to display. There are a number of ways this object can be displayed, I'm going to run you through the maths and code to do so.

Orthogonal projection

This is one of the most basic projection techniques, it simply discards one of the axis information to create a two-dimensional representation of one of the faces, a fancy way of saying sides, of the object.

In the following, example of a cube, the z-axis information is discarded to show the front face of the object.

I won't show the projections for the other faces, you'll just have to imagine what they would look like.

First, the vertices. These are the corner points for the cube, at this point there is no sense of scale so all of the values can simply be between -1.0 and 1.0 to keep it simple and easy to scale up later.

const xmin = -1.0;
const xmax = 1.0;
const ymin = -1.0;
const ymax = 1.0;
const zmin = -1.0;
const zmax = 1.0;
const verts: Vertex[] = [
  new Vertex(xmin, ymin, zmin),
  new Vertex(xmin, ymin, zmax),
  new Vertex(xmin, ymax, zmin),
  new Vertex(xmin, ymax, zmax),
  new Vertex(xmax, ymin, zmin),
  new Vertex(xmax, ymin, zmax),
  new Vertex(xmax, ymax, zmin),
  new Vertex(xmax, ymax, zmax)
];

The code for a Vertex object:

class Vertex {
  readonly x: number;
  readonly y: number;
  readonly z: number;

  constructor(x: number, y: number, z: number) {
    this.x = x;
    this.y = y;
    this.z = z;
  }
}

Then, the vertices are mapped into faces.

const faces: Polygon[] = [
  new Polygon([verts[0], verts[1], verts[5], verts[4]]), // Front
  new Polygon([verts[2], verts[3], verts[7], verts[6]]), // Rear
  new Polygon([verts[0], verts[1], verts[3], verts[2]]), // Bottom
  new Polygon([verts[4], verts[5], verts[7], verts[6]]), // Top
  new Polygon([verts[0], verts[2], verts[6], verts[4]]), // Left
  new Polygon([verts[1], verts[3], verts[7], verts[5]])  // Right
];

The code for a Polygon object:

class Polygon {
  readonly vertices: Vertex[];

  constructor(vertices: Vertex[]) {
    this.vertices = vertices;
  }

  get vertexCount(): number {
    return this.vertices.length;
  }

  vertex(index: number): Vertex {
    if (i < 0) {
      throw new Error('Vertex index must be a positive integer')
    }
    if (i >= this.vertexCount) {
      throw new Error('Vertex index out of bounds');
    }
    return this.vertices[i];
  }
}

The code that runs the rendering.

const canvas = document.getElementById('my-canvas');
const context = canvas.getContext('2d');

// Make the cube half the width of the canvas
const size = canvas.width / 2;

function fx(vertex): number {
  return vertex.x * size / 2;
}
function fy(vertex): number {
  return vertex.y * size / 2;
}

// Makes 0 the center of the canvas
context.translate(canvas.width / 2, canvas.height / 2);

for (let i = 0; i < faces.length; ++i) {
  drawPolygon(context, faces[i], fx, fy);
}

The code that draws the polygons.

function drawPolygon(context, polygon, fx, fy) {
  context.beginPath();

  // The -1 * is used to flip the y coordinate as y value increases
  // as you move down the canvas.
  context.moveTo(fx(polygon.vertex(0)), -1 * fy(polygon.vertex(0)));
  for (let i = 1; i < polygon.count(); ++i) {
    context.lineTo(fx(polygon.vertex(i)), -1 * fy(polygon.vertex(i)));
  }
  context.closePath();
  context.stroke();
}

To draw a simple square that was a lot of hard work, this work will pay off later though!

Oblique projection

This projection technique is a bit more complicated than the last example, but not by much. The z-axis is represented, to give a feel for the three-dimensional shape. Oblique projection is one form of parallel projection and mathematically can be described as:

(x,y)=(x+az,y+bz)(x,y) = (x+az,y+bz)

The a and b terms specify the ratio to apply to z, different oblique projections use different ratios.

Cavalier projection

Cavalier projection is an oblique projection where the z length is preserved.

a=cosθb=sinθ(x,y)=(x+az,y+bz)=(x+zcosθ,y+zsinθ)\begin{align*} a &= \cos \theta \\ b &= \sin \theta \\ (x,y) &= (x + az, y + bz) \\ &= (x + z \cos \theta, y + z \sin \theta) \end{align*}

The following diagram is a Cavalier projection at an angle of 30°.

Cabinet projection

The length along the z axis is shortened by a half or two thirds to provide a more realistic representation of the proportions.

a=12cosθb=12sinθ(x,y)=(x+az,y+bz)=(x+z(12cosθ),y+z(12sinθ))\begin{align*} a &= \frac 1 2 \cos \theta \\ b &= \frac 1 2 \sin \theta \\ (x,y) &= (x + az, y + bz) \\ &= (x + z(\frac 1 2 \cos \theta), y + z(\frac 1 2 \sin \theta)) \end{align*}
a=23cosθb=23sinθ(x,y)=(x+az,y+bz)=(x+z(23cosθ),y+z(23sinθ))\begin{align*} a &= \frac 2 3 \cos \theta \\ b &= \frac 2 3 \sin \theta \\ (x,y) &= (x + az, y + bz) \\ &= (x + z(\frac 2 3 \cos \theta), y + z(\frac 2 3 \sin \theta)) \end{align*}

Axonometric projection

Isometric projection

a=cosθb=sinθ(x,y)=(ax+az,y+bzbx)=(x×cosθ+z×cosθ,y+z×sinθx×sinθ)\begin{align*} a &= \cos \theta \\ b &= \sin \theta \\ (x,y) &= (ax + az, y + bz - bx) \\ &= (x \times \cos \theta + z \times \cos \theta, y + z \times \sin \theta - x \times \sin \theta) \end{align*}

The math expression above expressed in JavaScript:

const a = Math.cos(angle);
const b = Math.sin(angle);

function fx(vertex): number {
  return vertex.x * a + vertex.z * a;
}
function fy(vertex): number {
  return vertex.y + vertex.z * b - vertex.x * b;
}

The resulting projection:

Projection can also be expressed more elegantly using a transformation matrix.

P=[cosθ0cosθsinθ1sinθ000]P = \begin{bmatrix} \cos \theta & 0 & \cos \theta \\ -\sin \theta & 1 & \sin \theta \\ 0 & 0 & 0 \end{bmatrix}

The matrix is created using a simple Matrix class that supports 3 × 3 matrices:

const a = Math.cos(angle);
const b = Math.sin(angle);
return new Mat3([
   a, 0, a,
  -b, 1, b,
   0, 0, 0
]);

The resulting projection:

Rotation transformations

Using a matrix

Rx(θ)=[1000cosθsinθ0sinθcosθ]Ry(θ)=[cosθ0sinθ010sinθ0cosθ]Rz(θ)=[cosθsinθ0sinθcosθ0001]\begin{align*} R_x(\theta) &= \begin{bmatrix} 1 & 0 & 0 \\ 0 & \cos \theta & -\sin \theta \\ 0 & \sin \theta & \cos \theta \end{bmatrix} \\ R_y(\theta) &= \begin{bmatrix} \cos \theta & 0 & \sin \theta \\ 0 & 1 & 0 \\ -\sin \theta & 0 & \cos \theta \end{bmatrix} \\ R_z(\theta) &= \begin{bmatrix} \cos \theta & -\sin \theta & 0 \\ \sin \theta & \cos \theta & 0 \\ 0 & 0 & 1 \end{bmatrix} \end{align*}

Translation around multiple axis can be achieved simply by multiplying rotation matrices together to produce the transform matrix, which creates a traditional, roll, pitch, yaw rotation model.

R=Rx(α)Ry(β)Rz(γ)R = R_x(\alpha) R_y(\beta) R_z(\gamma)

Bibliography