tessellation-webgl

Tessellation is the process of partitioning space into a set of smaller polygons.

This project aims at colorful art by using iterative tessellation. Each scene is completely random and supports infinite zooming. You can explore anywhere you like by using the left mouse button.

Unfortunately, WebGL doesn't support geometry nor tessellation shaders. I perform the tessellation itself on the CPU side (if possible, in a dedicated Web Worker for multithreading), however for good real-time performance during zooming, a big part of the computation is delegated to the GPU.

See it live here.

Donate

Preview

Illustration 1

Illustration 1

Illustration 1

Illustration 1

Explanations

Base idea

The principle of tessellation I use here is very basic: to tessellate a shape (quad or triangle), I simply compute a random segment that splits the shape in two subshapes. Then, repeat the process on each subshape as many times as needed. Each subshape inherit the color of their parent, with a slight mutation.

When zooming in, when needed I tessellate again each child shape in order to maintain a pretty constant number of shapes on screen.

Implementation

Tree structure

The natural structure of the scene is tree-like, where each tree node is a shape that has its subshapes as children. Only the leafs of the tree are displayed as polygons (when soft blending is enabled, I first display the leafs' parents as opaque shapes, then the leafs are displayed as semi-transparent). For lines, I display every subdivision segment in the tree.

Tree optimization: size reduction

The first optimization is to reduce the size of the tree as much as possible, because it reduces memory and CPU footprint, and it also reduces the amount of primitives to draw.

In this regard, the tree has 2 interesting properties.

The first property is: a child node's space is contained in its parent's space. This means that if a parent is out of view, then all of its children are out of view too.

The second property is: siblings' space form a partition of their parent's space. This means that siblings' space don't intersect. If a node covers the whole screen, it means that all its siblings are out of view.

Tree optimization: levels cache

I often need to iterate over the nodes of the tree. Especially, I regularly need to iterate over all nodes of a given depth. Doing this naively requires lots of going up and down in the tree.

Each node has a cache dedicated for this use case. Each node has several lists: a list of its children of depth 1 (direct children), 2 (grand-children), 3 etc. When a node is added to or removed from the tree, its parents are notified that their cache has been invalidated. It will be reconstructed the next time it will be needed. This is quite memory intensive because it does a lot of Array.push(). I found that Array.push() is faster than Array.concat(), maybe because the latter creates a whole new array while the first one edits an existing one.

Computation optimizations

The CPU-side computations are made of 3 things:

These computations are too heavy on the CPU and memory to be performed on each frame. This is why I only perform them on a regular basis, for instance every 100 milliseconds. However, I still want the zooming animation to be smooth. So, for each frame between two heavy CPU computations, the GPU interpolates the position of each primitive according to the current zooming speed. This way of delegating computation to the GPU works very well for my needs.

Even with the throttling of the computations described above, they are sometimes too heavy and cause periodic micro-freezes. Here is a graph of the CPU usage illustrating this issue:

Antialiasing result

The CPU spikes are caused by the heavy computing done every 100 ms in the monothreaded mode.

Multithreading: dedicated Web Worker

In order to avoid those micro-freezes, I decided to use a dedicated Web Worker as a way of doing multithreading. It works wonderfully and allows for a smooth experience when the monothreaded mode struggled to display a detailed scene. Here is an illustration of the distribution of the computation:

Antialiasing result

The Web Worker does all the heavy computation. The zooming interpolation is performed at draw time by the GPU. The only job of the main thread is to handle the interface, and to upload to the GPU the data computed by the Web Worker.

IE11 support

Surprisingly, Internet Explorer 11 has pretty good support for Web Worker and runs this multithreaded project well. With IE11, the advantage of multithreading is all the more visible because the monothreaded mode is extremely laggy while the multithreaded mode runs at 60 FPS easily. The only little quirk I found is that for some reason, for IE11 self.performance.now() is not defined in the worker context, so I had to fake it by regularly making the main thread send a timestamp to the worker thread.

Rendering

This project supports 3 custom renderers:

WebGL renderer optimizations

In WebGL, I often find the costliest operation is the CPU/GPU communication. This includes two things:

In order to reduce both, my WebGL renderer waits until the last moment to draw everything. This allows me only rebuild and reupload VBOs when their data has changed, and also to blend calls to drawArrays together when they use continuous VBO subparts and the same uniform values.

Antialiasing differences between Canvas2D and WebGL

By comparing my SVG, Canvas2D and WebGL renderers I noticed a difference in the way shapes are rendered. Here is an illustration:

Antialiasing result

WebGL renderer on the left, Canvas2D on the right. The scene is the same: polygons only, without any lines, drawn over a black background.

Notice how the result is different: in the WebGL version, each polygon touches its neighbours, whereas in the Canvas2D/SVG version there is a kind of gap creating unwanted lines between neighbours.

It seems that the difference comes from the handling of antialiasing:

This can be confirmed by using the SVG attribute shape-rendering="crispEdges", which basically disables antialiasing. By using this attribute, the ugly lines artifacts disappear, but then the scene is aliased. I suspect there is a similar attribute for Canvas2D, but I didn't bother searching.

Antialiasing result

x3 magnification of the same SVG scene made of triangles that touch their neighbours. On the left, with default parameters: antialiased but with ugly lines artifacts. On the right, with shape-rendering="crispEdges" parameter: no artifact but the scene is aliased.