3D In 2D Canvas

In my recent simulation of an AC generator, I show the same device from two different views: A top view and a front view. To accomplish that, I used a clever technique called 3D Projection. Here, I’m going to talk about how I did that in JavaScript and rendered it on canvas.

What is 3D Projection?

Basically, it means that I define items in 3D space (each point is defined as an array [x, y, z]) and that I plot them in 2D space. For simplification, the 2D render can only be seen in the xy, xz, or yz plane.

Defining in 3D

Each point is defined as an array, [x, y, z].

Since we are using Canvas, it is easiest to define a rectangular face as a 2D array of 5 points: The first vertex, the remaining 3 vertices, and then the initial vertex again. This allows us to moveTo() the first index and then lineTo() till the remaining length of the array.

Since faces exist in 2D, and we’re defining 3 coordinates per point, for a single face, either the x, y, or z, coordinate will remain constant for all points.

A cuboid is defined a 3D array of faces. Depending on our views, we might not need all 6 faces to define a cuboid and can get away with only two or three faces.

Example:

var cuboid = [  
  // xy face
  [
    [xa1, ya1, za],
    [xa2, ya2, za],
    [xa3, ya3, za],
    [xa4, ya4, za],
    [xa5, ya5, za],
  ],
  // xz face
  [
    [xb1, yb, zb1],
    [xb2, yb, zb2],
    [xb3, yb, zb3],
    [xb4, yb, zb4],
    [xb5, yb, zb5],
  ],
  ...
]

Rendering in 2D.

Since we’re defining faces parallel to the primary planes, it is easiest to render the views of the primary planes themselves.

Imagine a shape on the xy plane. All points that define it can be written as (xi, yi, 0). Similarly, any shape on the xz plane defines all points as (xi, 0, zi). Basically, whichever axis you’re not rendering the shape on is 0.

This makes things easy for us.

Creating a function called plotFace, which takes three parameters:

  • face: The 2D array of points
  • path: The path to plot the shape on
  • a: The first axis of the plane
  • b: The second axis of the plane

Since in our arrays, the 0 index represents x and so on, we can simplify the function if a and b are directly passed as integers.

The plotFace function basically movesTo the initial point and linesTo the remaining ones.

function plotFace(face, path, a, b) {  
  var len = face.length;
  path.moveTo( face[a], face[b] );
  for ( var i = 0; i < len; i++ ) {
    var point = face[i];
    path.lineTo( point[a], point[b] );
  }
}

A simple map through all faces and using plotFace on each can plot the entire shape to a single path.

path = new Path2D();  
ctx.beginPath();  
item.faces.map(function(face) {  
  plotFace(face, path, a, b);
})
ctx.closePath();  

Finally, rendering the context using fill() will get our shape.

ctx.fill(path);  

…And we’re done!

What’s next

Currently, we’re only projecting in the xy, xz, or xy plane. Next would be to be able to project in any arbitrary plane. I have yet to figure out the math for it, and I’m trying to do it without external help, so it might be a while before I publish a new article.

After that, perhaps manually raycasting to create shadows? That could be interesting, both aesthetic and performance wise.

Further Reading

Discuss on Twitter