Origin of Symmetry

September 23, 2021

This is an entry for @sableRaph's weekly creative coding challenge. The theme for this week was right angles, and I took this opportunity to recreate the tuning fork landscape from the album art for Muse's Origin of Symmetry.

Depth of Field

The technically interesting part of this sketch is the depth of field blur. A 2D canvas can apply a blur filter reasonably quickly, but how can one extend this to a 3D scene?

If one makes the main canvas of the sketch 2D but keeps an offscreen WebGL canvas, then each object in the scene can be rendered to the WebGL canvas individually, then drawn to the 2D main canvas with the appropriate amount of blur. This comes with a few caveats:

In practice, since the tuning forks don't have crazy interlocking shapes, this method works out decently, so long as I keep the canvas size small for performance reasons.

Manual depth sorting

Part of the compromise of my blur method is that I had to manually sort the objects by depth so that objects in front can be drawn after objects in the back. So how does one do that?

If you can calculate a z position for each object, from the perspective of the camera, then sorting them by depth is relatively easy:

// Most negative z (farthest away) gets sorted to the start of the list
objects.sort((a, b) => a.cameraZ - b.cameraZ)

So then the question becomes, how does one calculate that z value? Well, p5's WebGL mode creates a matrix behind the scenes that represents the transformation applied by all the translate, rotate, and scale calls that have been made so far. Multiplying a point by that matrix is analogous to calling a function on that point that returns a new point which has all of those transformations applied. So, we can create that matrix ourselves!

When constructing a matrix, take a look at the list of matrix methods on MDN. For each p5 transformation you make, there will be an equivalent DOMMatrix method. The self-suffixed methods modify the matrix in place.

Also, big warning! DOMMatrix methods use degrees instead of radians!!!

Here is what this all looks like for my sketch:

const transform = new DOMMatrix()

// In this sketch, the camera simply rotates in place. Apply whatever
// transformations you would be applying in your scene here.
transform.rotateAxisAngleSelf(0, 1, 0, sceneRotation / PI * 180)

for (const obj of objects) {
  const transformed = new DOMPoint(obj.x, obj.y, obj.z).matrixTransform(transform)
  obj.cameraZ = transformed.z
}