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.
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!
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:
The a and b terms specify the ratio to apply to z, different oblique projections use different ratios.
Cavalier projection is an oblique projection where the z length is preserved.
The following diagram is a Cavalier projection at an angle of 30°.
The length along the z axis is shortened by a half or two thirds to provide a more realistic representation of the proportions.
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.
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:
Using a matrix
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.