- Mathematics
- Describing the World
- Projecting an Object onto a Drawing
- Writing to
`.png`

- The Visibility Problem
- Animation
- Animating with
`Codec.Picture.Gif`

- Conclusions

While working on the rendering my solution for the Irreversible Pocket Cube, I ended up writing a faux isometric rendering engine which could turn my specific Rubix Cube datastructure into something like an isometric projection of that cube. This my attempt to do it the right way.

The full code lives at ambuc/Cornea.

# Mathematics

To quote Wikipedia,

where $\alpha$ and $\beta$ are the pitch and yaw of the camera angle; that is, how far from the equator and meridian in spherical coordinates our viewpoint resides, if the center of the scene is at the origin. This projection matrix works for any 3d point $ \begin{bmatrix} a_x & a_y & a_z \end{bmatrix}^\intercal $ and results in 2d coordinates $ \begin{bmatrix} b_x & b_y & 0 \end{bmatrix}^\intercal $.

That’s really all the projection math we need. The name `metric`

is sort of a
play on *isometric*, except the function is extensible and can return a
transformation matrix for any viewing angle, not just the default $(35.264,45)$
isometric viewing angle.

# Describing the World

Let’s define an `Obj`

object as either

- a single point, a
`[Float]`

list of three floating point numbers describing its $(x,y,z)$ coordinates, or - a list of points describing a line or polyline, or
- a list of points describing a face

## Styling the World

We want to be able to `style`

each of these objects. Because I like
`Graphics.Rasterific`

, we’ll use their types and define `style`

as a mapping
between a `[Primitive]`

and a final `Drawing px ()`

. In practice we would use
the builtin `withTexture (uniformTexture <color>) . fill`

for a solid fill, or
```
withTexture (uniformTexture <color>) . stroke <width> JoinRound (CapRound,
CapRound)
```

for a stroke (used for a line, polyline, or point). In practice it
would be nice to provide synonyms in the form of `solid k`

and `mark k n`

.

# Projecting an Object onto a Drawing

Let’s define a convenient type synonym `View`

for our pitch/yaw tuple:

and write a `proj v <obj>`

function which can take a coordinate and project it
into the plane.

Great. Now we can plot a coordinate by simply projecting it into the plane. We should be able to plot a line or polyline by projecting each of its coordinates, and a face (or polygon) the same way.

Very cool. Let’s construct a sample `world`

and try writing it to a `.png`

.

# Writing to `.png`

Here’s our calibration world:

Now we need a function which can turn a list of these `(Obj,Style)`

tuples into
a single `Drawing px ()`

item. We really want to map `drawFrom`

over each of
them, and then `sequence_ []`

the list; we can write that as `mapM_`

instead,
which maps a monad over a list of inputs, discarding the intermediate results
along the way.

The very final piece of our puzzle is something which takes a `Drawing px ()`

and actually turns it into an `Image px`

, which can be written to `.png`

with
Rasterific’s `writePng`

.

We’ll need to supply `render`

with a width/height/scale

Great. Let’s write this `Drawing px ()`

out to an `Image px ()`

and eventually
to a file `IO ()`

.

That’s super! Let’s try something more complicated: define `myWorld`

to be a
bunch of intersecting squares along the x-y and y-z planes:

And render it as before:

Uh-oh. We’re running into the visibility problem, wherein we don’t know which order to render the faces in so that the things nearer the camera would be visible over the things farther from the camera.

# The Visibility Problem

There are a *bunch* of ways to solve this problem, but I ended up computing a
vector from the origin to the centroid of each object, defining a vector from
the origin to the position of the camera, and ranking the objects by the scalar
projection of each object’s
centroid-vector onto the camera-vector.

Now, instead of just `mapM_ (`drawFrom` v) world`

we can write:

Which uses a `Data.List`

builtin `sortBy (comparing <comparison_fn>) list`

to
reorder a list according a custom `comparison_fn`

. You could write one like ```
(\a
b -> if a > b then a else b)
```

, or we could use `comparing`

to rank by an
implicit function of each item.

In this case, that implicit function is `closeness v . centroid . fst`

, which
takes each `(Obj, Style)`

tuple, extracts the `Obj`

, finds its centroid, and
finds the closeness by calculating the scalar product of that centroid-origin
vector with the viewpoint-origin vector.

This gets us something a little more reasonable:

# Animation

This is pretty cool, but what if we want to animate our world?

Let’s define a series of pitches and yaws to make a cool wobbling sweep around
our world. Again, `mapM_`

lets us map our `writePng`

function over a list of
3-tuples containing the pitch, yaw, and frame number. We can write them out to
`/tmp/canvas*.png`

…

…and compose them together into a `.gif`

later
with `convert -loop 0 /tmp/canvas* \<filename\>`

.

Here’s that contrasted with an animation from before we solved the visibility problem:

# Animating with `Codec.Picture.Gif`

`Codec.Picture`

supports assembling `Image`

s into gifs in Haskell, without
having to write the frames out to a file and assembling them with `convert`

.
It’s a lot slower, though. I’ll present it here just out of interest, but in
practice I found it anywhere from 8x-10x slower.

This took ~3min and I see a bit of flickering – probably because the command-line
`convert`

utility understands color and framerate better than I do.

# Conclusions

This is a neat little library, just powerful enough to handle the matrix multiplication necessary to take a scene depicted in raw coordinates and render it in a useful way. I have no idea what its performance will be like for a larger scene, but I’ll continue to use it in the future and we’ll see what sorts of optimizations can be made.

I’ve put it on Github here: ambuc/cornea in the hopes that someone else might get a kick out of it.