Intrigued by this series of tweets by Keenan Crane (and this exploration by Varun Vachhar), I spent a recent Sunday morning dusting off high school memories of complex numbers in order to write some pretty shaders.
Keenan’s tweet showed a few formulas visualized in 2D space, and I was immediately faced with a couple of questions:
Hopefully this post helps break down what’s going on here (or at least to the best of my understanding), and how we can use what we learn to play with and visualize complex numbers.
GLSL, or the OpenGL Shading Language, is a graphics programming language that can be used to program visual effects. Shaders are both incredibly versatile and also conceptually simple — you write a set of instructions, and those instructions are run for every pixel on the screen. The result of those instructions tells the pixel what color it should display as.
Whilst conceptually simple, shaders can be really complex to work with. Instead of thinking “I want to make the 62nd pixel across and the 38th pixel down be red” (as seen in my seminal work Red Pixel, 2019), we need to think in mathematical terms about the shape of functions in 2D space. Lots of trigonometry and obscure functions to make shapes. The Book of Shaders is a lovely introduction.
The visuals we'll be creating today are all really just vizualising math — drawn on a flat screen and given colors. Zach Lieberman (of the School for Poetic Computation and MIT Media Lab) put this well recently:
So! Math…
To start with, let’s talk about complex numbers, because they’re fun and weird.
At a high level, Complex numbers are numbers that are split into two parts — real and imaginary. They are represented in the form , where and are real numbers, and is the imaginary unit (representing , an impossible number).
Just like me, GLSL doesn't really understand complex numbers, so our job here is to represent them in a way that we're both familiar with.
Luckily, as and are real numbers, we can do real number things with them! They can be represented in GLSL as regular floating point values (float
), and stored in a floating-point vec2
vector (just a set of two float
s). By representing our complex number like this, we can perform complex arithmetic on it and have GLSL able to follow along.
Also, as with anything we can represent with two values, we can then visualize these values on a 2D surface (known as the "complex plane" when dealing with complex numbers). On the complex plane, the horizontal x-axis represents , our real component (, the real axis), and the vertical y-axis represents , our imaginary component (, the imaginary axis).
Now that I've written some and it’s given me the false confidence that I know what I’m talking about, let’s break down the formula in Keenan’s first tweet above:
Here’s what we're working with —
uv.xy
if you're already familiar with shader programming)What we're doing here is effectively plotting the complex logarithm between two points on the complex plane. It‘s easy for me to write that as if I know what it means or what we’ll see when we do the math, but I’m confident it’ll look good regardless. Let‘s press on!
Right now, we have a complex plane (our GLSL canvas), we have some complex numbers we can put on the plane (our pixels and points), and we have the ability to perform arithmetic on them (the instructions we’ll write in GLSL). Let’s write some shader code!
vec2 uv = (gl_FragCoord.xy - 0.5 * u_resolution.xy) / min(u_resolution.y, u_resolution.x);vec2 z = uv;
Here, gl_FragCoord
is the xy
coordinate of the fragment (the pixel we're working on), and u_resolution
is the resolution of our canvas we'll be rendering to. What we're doing here is setting our bounds — rather than working with values matching the resolution (say, 500, 500
for a 500px square canvas), we're taking our pixel coordinate and mapping it to the range -0.5, 0.5
(with some aspect ratio correction thrown in using min
).
As mentioned before, we'll be using in math terms as our uv
in shader terms, meaning that for our purposes, each pixel on the screen is now represented as a complex number.
Let’s also define our static points and between which we'll draw a line:
// Lower-left pointvec2 p = vec2(-0.25, -0.25);// Upper-right pointvec2 q = vec2(0.25, 0.25);
And with all the parameters now defined, we can convert our formula to GLSL:
// Divide z-p by z-q using complex divisionvec2 division = cx_div((z - p), (z - q));// Calculate the log of our divisionvec2 log_p_over_q = cx_log(division);// Extract the imaginary numberfloat imaginary = log_p_over_q.y;
And there we have it — a float
value for our shader to paint to the screen.
Our imaginary
value gives us everything we need, but it‘s just a float
. Shaders output four numbers per pixel (red, green, blue, and alpha — RGBA), and we just have the one. To make things a little more visually-interesting, we can use Inigo Quilez’s procedural palette
function to add some color to our current grayscale value:
// From https://iquilezles.org/www/articles/palettes/palettes.htmvec3 palette( in float t, in vec3 a, in vec3 b, in vec3 c, in vec3 d ){return a b*cos(2. * PI *(c*t d) );}
This function takes in a float value — which for us is will be the imaginary part of our logarithm — along with four vec3
s that can be altered endlessly to produce beautiful output.
With all of these pieces combined (and some over-complicated tone-mapping using our palette
function) we get the following:
#version 300 esprecision mediump float;out vec4 fragColor;uniform vec2 u_resolution;uniform float u_time;#define PI 3.141592653589793#define cx_div(a, b) vec2(((a.x*b.x a.y*b.y)/(b.x*b.x b.y*b.y)),((a.y*b.x-a.x*b.y)/(b.x*b.x b.y*b.y)))vec2 as_polar(vec2 z) {return vec2(length(z),atan(z.y, z.x));}vec2 cx_log(vec2 a) {vec2 polar = as_polar(a);float rpart = polar.x;float ipart = polar.y;if (ipart > PI) ipart=ipart-(2.0*PI);return vec2(log(rpart),ipart);}vec3 palette( in float t, in vec3 a, in vec3 b, in vec3 c, in vec3 d ) {return a b*cos( 0.38*2.*PI*(c*t d) );}void main() {vec2 uv = (gl_FragCoord.xy - 0.5 * u_resolution.xy) / min(u_resolution.y, u_resolution.x);vec2 z = uv;float angle = sin(u_time/5.) * 2. * PI;float length = .2;// Spin our points in a circle of radius lengthfloat c = cos(angle);float s = sin(angle);vec2 p = vec2( s*length, c*length);vec2 q = vec2( s*-length, c*-length );// Divide z-p by z-q using complex divisionvec2 division = cx_div((z - p), (z - q));// Calculate the log of our divisionvec2 log_p_over_q = cx_log(division);// Extract the imaginary numberfloat imaginary = log_p_over_q.y / PI;vec3 col = palette( imaginary, vec3(0.50,.52,0.53), vec3(.46,.32,.35), vec3(.82,.84,.65), vec3(0.53,0.23,0.22));fragColor = vec4(col, 1.0);}
As with any shader, by jamming a bunch of random numbers into the many float
variables we have here (and by making some tweaks to how our values are rendered across .xy
screen space), we can get output like the following —
Beautiful! Who would imagine.
If you’ve got time and patience, I’ve got a little more math for you.
Keenan‘s original tweet actually showed a slightly more advanced representation of complex numbers, showing meromorphic functions (functions where certain points shoot off up to infinity) —
When we take a look at this function:
We recognize some old friends — , , — but also some new faces:
That sigma statement in this case can be written out longhand as . In fact, if we set our limits and to , we get —
What this formula represents is the imaginary part of the complex logarithm of the ratio of two complex polynomials.
Okay, I know. That was a lot of math. I’m sorry.
With our and still representing x
and y
points on the complex plane, we can scatter some points around the complex plane and, like before, represent this formula in GLSL:
#version 300 esprecision mediump float;out vec4 fragColor;uniform vec2 u_resolution;uniform float u_time;#define PI 3.1415926535897932384626433832795#define cx_mul(a, b) vec2(a.x*b.x - a.y*b.y, a.x*b.y a.y*b.x)#define cx_div(a, b) vec2(((a.x*b.x a.y*b.y)/(b.x*b.x b.y*b.y)),((a.y*b.x - a.x*b.y)/(b.x*b.x b.y*b.y)))#define cx_sin(a) vec2(sin(a.x) * cosh(a.y), cos(a.x) * sinh(a.y))#define cx_cos(a) vec2(cos(a.x) * cosh(a.y), -sin(a.x) * sinh(a.y))vec2 cx_tan(vec2 a) {return cx_div(cx_sin(a), cx_cos(a)); }vec2 cx_log(vec2 a) {float rpart = sqrt((a.x*a.x) (a.y*a.y));float ipart = atan(a.y,a.x);if (ipart > PI) ipart=ipart-(2.0*PI);return vec2(log(rpart),ipart);}vec2 as_polar(vec2 z) {return vec2(length(z),atan(z.y, z.x));}vec2 cx_pow(vec2 v, float p) {vec2 z = as_polar(v);return pow(z.x, p) * vec2(cos(z.y * p), sin(z.y * p));}float im(vec2 z) {return ((atan(z.y, z.x) / PI) 1.0) * 0.5;}vec3 pal( in float t, in vec3 a, in vec3 b, in vec3 c, in vec3 d ){return a b*cos(2.*PI*(c*t d));}// Define our pointsvec2 a0 = vec2(0.32, -0.45);vec2 a1 = vec2(-0.49, -0.32);vec2 a2 = vec2(-0.31, 0.38);vec2 a3 = vec2(-0.12, 0.04);vec2 b0 = vec2(-0.71, 0.53);vec2 b1 = vec2(0.01, 0.23);vec2 b2 = vec2(-0.24, 0.31);vec2 b3 = vec2(-0.01, -0.42);void main() {// Set up our imaginary planevec2 uv = (gl_FragCoord.xy - 0.5 * u_resolution.xy) / min(u_resolution.y, u_resolution.x);vec2 z = uv * 2.;// Calculate the sum of our first polynomialvec2 polyA = a0cx_mul(a1, z)cx_mul(a2, cx_pow(z, 2.0))cx_mul(a3, cx_pow(z, 3.0));// Calculate the sum of our second polynomialvec2 polyB = b0cx_mul(b1, z)cx_mul(b2, cx_pow(z, 2.))cx_mul(b3, cx_pow(z, 3.));// Calculate the ratio of our complex polynomialsvec2 result = cx_div(polyA, polyB);float imaginary = cx_log(result).y;float col = (imaginary / PI);fragColor = vec4(pal(col, vec3(.52,.45,.61),vec3(.40,.42,.31),vec3(.26,.30,.35),vec3(.15,.4,.4)),1.0);}
By putting some wild values in, we can get some wild images out —
I don’t know about you, but I think Red Pixel, 2019 can go straight in the trash.
I find GLSL (and shaders in general) to be a fascinating medium. It is fundamentally a medium that can represent anything that can be pictured on a two-dimensional plane — 3D models, fractals, paintings, poetry, ink, photographs. If you can see it, you can create a shader to represent it. But more than this, if you can't see it, you can create a shader to represent it.
There are a lot of numbers out there.
Go see them.
❋Fork Meromorphi mla 166 by mla
Fork Meromorphic Poly by akohdr