A webgl shader experiment

2018-01-11

How to create real fast demo animations with webgl. [EPILEPSY WARNING]

Once in a while, I’ll see this gif posted to reddit, and I am briefly mesmerized as I try to understand what I’m looking at. It seems like some relatively simple sum of sinusoids, but the combination of parameters and coloring creates a really neat optical illusion effect. Last time it came up, I tracked down the source (youtube, chinese blog post) and recreated it for fun. Here’s a simplified embedded version:

order:

With that blog post available, and a loose grasp of Mathematica syntax, it was easy enough to understand what’s going on. It’s a bit easier for me to grok in traditional notation:

Equation

In short, the thing is just a heightfield (z) created by summing N simple waves of equal magnitude, and distributed with rotational symmetry about the origin (the cos and sin terms are a rotation). R is a spatial zoom factor, and V is animation speed. It’s the S term in the “wrapping” step of computing the color (c) that really makes it interesting.

The most direct way to map heightfield values to color is to map min->0, max->255 (which is more or less what happens when S=1). You can think of this coloring method as using a colormap that’s a single gradient from black to white. If, instead, the color map “wraps around” a few times, so it goes black-white-black-white-black-white-…, then we get a much more interesting result, where the weird banding leads to those intricate patterns.

An earlier iteration, in python/numpy:

import numpy as np

def directional_wave(x, y, t=0, p=0):
    return np.cos(x * np.cos(t) - y * np.sin(t) + p)


def wrap(z):
    return np.abs(np.mod(z + 1, 2) - 1)


# generate one frame
xvec = np.linspace(-25*np.pi, 25*np.pi, 1000)  # approximate range used in the blog post
x, y = np.meshgrid(xvec, xvec)
z = sum([directional_wave(x, y, i*np.pi/N) for i in range(N)])
z = wrap(z)

I could render individual frames like this, but what I really wanted was a real-time animation with interactive control of some parameters. I don’t know of a great way to do that in Python, plus it’s almost certainly not fast enough to run at, say, 10+ frames per second. I figured I’d try it out in Javascript (easy to render animations) and Go (fast), and in Go I got as far as implementing the math, including a well-behaved mod function:

func mod(x, y float64) float64 {
	return math.Mod(math.Mod(x, y)+y, y)
}

func wrap(x float64) float64 {
	return math.Abs(mod(x+1, 2) - 1)
}

func getFrame(frame [][]uint8, p float64) {
	c, s := make([]float64, N), make([]float64, N)
	for n := 0; n < N; n++ {
		t := float64(n) * 3.1415 / N
		c[n], s[n] = math.Cos(t), math.Sin(t)
	}
	for px := 0; px < FW; px++ {
		x := W * (float64(px)/FW - 0.5)
		for py := 0; py < FH; py++ {
			y := H * (float64(py)/FH - 0.5)
			z := float64(0)
			for n := 0; n < N; n++ {
				z += math.Cos(x*c[n] - y*s[n] + p)
			}
			frame[py][px] = uint8(256 * wrap(z))
		}
	}
}

But then I decided that graphics support in Go would be a pain. In Javascript, I redid it all again using fillRect for each pixel on a canvas, and found it to be too slow, as expected. Somewhere in the course of researching Javascript animation and performance profiling (it might have been this article), I hit upon the idea of using shaders. I completely ignored the idea at first, assuming it would be too complicated, or just wouldn’t work. When I found myself considering a trig lookup table in Javascript, I decided shaders were probably a better approach.

So I found some references, and a few minimal shader examples, and tinkered with them until I got something that looked pretty. The code that does the work looks like this:

float fmod(float x, float y) {
    return (x - y * floor(x / y));
}

float wrap(float x) {
    return abs(fmod(x+1.0,2.0)-1.0);
}

void main() {
    float phase = speed * millisecs/1000.0;
    float t;
    float A = width/W;
    float x = A*(gl_FragCoord.x - W/2.0);
    float y = A*(gl_FragCoord.y - H/2.0);
    float z = 0.0;
    for(int n=0; n<10; n++) {
        t = float(n) * 3.14159 / order;
        z += cos(x*cos(t) + y*sin(t) + phase);
        if(n == int(order)) {
            break;
        }
    }
    z = wrap(scale * z / order);
    gl_FragColor = vec4(z, z, z, 1);
}

Here’s the rest of it (I wish I knew how to use CSS properly)

I had some problems along the way, not helped by a complete lack of shader error messages. At some point I realized that <script type="glsl"> produces error messages, while <script type="x-shader/fragment-shader"> doesn’t - though I’m still unsure of any other difference between these.

Other lessons learned:

  • Interactivity parameters can be implemented with “uniform” variables passed in from Javascript.
  • 0 isn’t a float, 0.0 is. Shader language is not C.
  • Standard C functions you might expect might not be available (mod).
  • Unbounded loops aren’t allowed, but breaking out of a long for-loop is fine.

I don’t know what I’m doing with WebGL - I just know enough to cobble together some stuff that more or less works. Now that I have some functioning boilerplate, I’ll probably ignore it as much as I can, and just create more pretty animations.

Some helpful references:

mathwebjavascriptgraphicswebgllatex

Hanging cables

Plots in Hugo