Back in 2009 I posted a short video Reflex preview of truncated 4D polytopes. Today I revisited that old code, reimplementing the core algorithms in OpenGL Shader Language (GLSL) using the FragM environment.

3D polytopes are also known as polyhedra. They can be defined by their Schlaefli symbol, which looks like {4,3} for a cube. This can be understood as having faces made up of squares ({4}), arranged in triangles around each vertex ({3}). This notation extends to 4D, where a hypercube (aka tesseract) has symbol {4,3,3}, made up of cubes {4,3} with {3} around each vertex.

These symbols are all well and good, but to render pictures you need concrete measurements: angles, lengths, etc. In H.S.M. Coxeter's book Regular Polytopes, there is the solution in the form of some recursive equations. Luckily the recursion depth is bounded by the number of dimensions, which is small (either 3 or 4 in my case, though in principle you can go higher). This is important for implementation in GLSL, which disallows recursion. In any case, GLSL doesn't have dynamic length lists, so different functions are needed for different length symbols.

I don't really understand the maths behind it (I don't think I did even when I first wrote the code in Haskell in 2009), but I can describe the implementation. The goal is to find 4 vectors, which are normal to the fundamental region of the tiling of the (hyper)sphere. Given a point anywhere on the sphere, repeated reflections through the planes of the fundamental region can eventually (if you pick the right ones) get you into the fundamental region. Then you can do some signed distance field things to put shapes there, which will be tiled around the whole space when the algorithm is completed.

Starting from the Schlaefli symbol {p,q,r} (doing 4D because it has some tricksy bits, 3D is largely similar), the main task is to find the radii of the sub polytopes ({p,q}, {p}, {q,r}, etc). This is because these radii can be used to calculate the angles of the planes of the fundamental region, using trigonometry. The recursive formula starts here:

float radius(int j, ivec3 p)
{
return sqrt(radius2_j(j, p));
}

Here j will range from 0 to 3 inclusive, and the vector is {p,q,r}. Then radius2_j() evaluates using the radii squared of the relevant subpolytopes, according to j. I think this is an application of Pythagoras' theorem.

float radius2_j(int j, ivec3 p)
{
switch (j)
{
case 0: return radius2_0(p);
case 1: return radius2_0(p) - radius2_0();
case 2: return radius2_0(p) - radius2_0(p.x);
case 3: return radius2_0(p) - radius2_0(p.xy);
}
return 0.0;
}

The function radius2_0() is overloaded for different length inputs, from 0 to 3:

float radius2_0() { return 1.0; }
float radius2_0(int p) { return delta() / delta(p); }
float radius2_0(ivec2 p) { return delta(p.y) / delta(p); }
float radius2_0(ivec3 p) { return delta(p.yz) / delta(p); }

Here it starts to get mysterious, the delta funtion uses trigonometry to find the lengths. I don't know how/why this works, I copy/pasted from the book. Note that it looks recursive at first glance, but in fact each delta calls a different delta(s) with strictly shorter input vectors, so it's just a non-recursive chain of function calls.

float delta()
{
return 1.0;
}
float delta(int p)
{
float s = sin(pi / float(p));
return s * s;
}
float delta(ivec2 p)
{
float c = cos(pi / float(p.x));
return delta(p.y) - delta() * c * c;
}
float delta(ivec3 p)
{
float c = cos(pi / float(p.x));
return delta(p.yz) - delta(p.z) * c * c;
}

Now comes the core function to find the fundamental region: the cosines of the angles are found by ratios of successive radii, the sines are found by Pythagoras' theorem, 3 rotation matrices are constructed, then one axis vector is transformed. Finally these vectors are combined using the 4D cross product (which has 3 inputs), giving the final fundamental region (I'm not sure why the cross products are necessary, but I do know the main property of cross product is that the output is perpendicular to all of the inputs.). Note that some signs need wibbling, either that or permute the order of the inputs.

mat4 fr4(ivec3 pqr)
{
float r0 = radius(0, pqr);
float r1 = radius(1, pqr);
float r2 = radius(2, pqr);
float r3 = radius(3, pqr);
float c1 = r1 / r0;
float c2 = r2 / r1;
float c3 = r3 / r2;
float s1 = sqrt(1.0 - c1 * c1);
float s2 = sqrt(1.0 - c2 * c2);
float s3 = sqrt(1.0 - c3 * c3);
mat4 m1 = mat4(c1, s1, 0, 0, -s1, c1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
mat4 m2 = mat4(c2, 0, s2, 0, 0, 1, 0, 0, -s2, 0, c2, 0, 0, 0, 0, 1);
mat4 m3 = mat4(c3, 0, 0, s3, 0, 1, 0, 0, 0, 0, 1, 0, -s3, 0, 0, c3);
vec4 v0 = vec4(1, 0, 0, 0);
vec4 v1 = m1 * v0;
vec4 v2 = m1 * m2 * v0;
vec4 v3 = m1 * m2 * m3 * v0;
return (mat4
( normalize(cross4(v1, v2, v3))
, -normalize(cross4(v0, v2, v3))
, normalize(cross4(v0, v1, v3))
, -normalize(cross4(v0, v1, v2))
));
}

4D cross product can be implemented in terms of 3D determinants:

vec4 cross4(vec4 u, vec4 v, vec4 w)
{
mat3 m0 = mat3(u[1], u[2], u[3], v[1], v[2], v[3], w[1], w[2], w[3]);
mat3 m1 = mat3(u[0], u[2], u[3], v[0], v[2], v[3], w[0], w[2], w[3]);
mat3 m2 = mat3(u[0], u[1], u[3], v[0], v[1], v[3], w[0], w[1], w[3]);
mat3 m3 = mat3(u[0], u[1], u[2], v[0], v[1], v[2], w[0], w[1], w[2]);
return vec4(determinant(m0), -determinant(m1), determinant(m2), -determinant(m3));
}

The projection from 4D to 3D is stereographic, because signed distance fields are implicit we need both directions (one to go from input point in 3D to 4D, then after transformation / tesselation we need to go back to 3D to calculate distances):

vec4 unstereo(vec3 pos)
{
float r = length(pos);
return vec4(2.0 * pos, 1.0 - r * r) / (1.0 + r * r);
}
vec3 stereo(vec4 pos)
{
pos = normalize(pos);
return pos.xyz / (1 - pos.w);
}
float sdistance(vec4 a, vec4 b)
{
return distance(stereo(a), stereo(b));
}

The tiling is done iteratively (the limit of 600 is there because unbounded loops on a GPU are not really advisable, if this limit is set too low, e.g. 100, then visible artifacts occur - it's probably possible to prove an optimal bound somehow):

float poly4(vec4 r)
{
for (int i = 0, j = 0; i < 4 && j < 600; ++j)
{
if (dot(r, FR4[i]) < 0)
{
r = reflect(r, FR4[i]);
i = 0;
}
else
{
i++;
}
}
float de = 1.0 / 0.0;
// signed distance field stuff goes here
return de;
}

The user interface part of the code has some variables for selecting dimension and symmetry group (Schlaefli symbol). It also has 4 sliders for selecting the truncation amount in barycentric coordinates (which makes settings transfer in a meaningful way between polytopes), and 6 checkboxes for enabling different planes (there are 4 ways to choose the first axis and 3 left to choose from for the second axis, divided by 2 because the order doesn't matter).

vec4 s = inverse(transpose(FR4)) * vec4(BX, BY, BZ, BW);

The signed distance field stuff is quite straightforward in the end, though it took a lot of trial and error to get there. The first way I tried was to render tubes for line segments, by projecting the input point 'r' onto the planes of the fundamental region and doing an SDF circle:

float d = sdistance(s, r - FR4[0] * dot(r, FR4[0])) - thickness;

The planes are drawn by projecting 's' onto the cross product of the plane with 'r'. I don't know why this works:

vec4 p = normalize(cross4(FR4[0], FR4[1], r));
float d = sdistance(s, s - p * dot(s, p)) - 0.01;

Finally the DE() function for plugging into the DE-Raytracer.frag that comes with FragM has some animated rotation based on time, and the baseColor() function textures the solid with light and dark circles (actually slices of hyperspheres).

Full code download: reflex.frag.

]]>
Melinda Green's webpage
The 4D Mandel/Juli/Buddhabrot Hologram
has a nice video at the bottom, titled
*ZrZi to ZrCr - only points Inside the m-set*.
I recalled my 2013 blog post about the
Ultimate Anti-Buddhabrot
where I used Newton's method to find the limit Z cycle of each C value
inside the Mandelbrot set and plotted them. The (anti-)Buddhagram is
just like the (anti-)Buddhabrot, but the Z points are plotted in 4D space
augmented with their C values. Then the 4D object can be rotated in
various ways before projection down to 2D screen, possibly via a 3D step.

My first attempt was based on my ultimate anti-Buddhabrot code, computing all the points in a fine grid over the C plane. I collected all the points in a large array, then transformed (4D rotation, perspective projection to 3D, perspective projection to 2D) them to 2D and accumulated with additive blending to give an image. This worked well for videos at moderate image resolutions, achieving around 6 frames per second (after porting the point cloud rasterization to OpenGL) at the highest grid density I could fit into RAM, but at larger sizes the grid of dots became visible in areas where the z→z²+c transformation magnified it.

Then I had a flash of inspiration while trying to find the surface normals for lighting. Looking at the formulas on Wikipedia I realized that each "pringle" is an implicit surface \(F_p(c, z) = 0\), with \(F_p(c, z) = f_c^p(z) - z\) and the usual \(f_c(z) = z^2 + c\). \(p\) is the period of the hyperbolic component. Rendering implicit surfaces can be done via sphere-marching through signed distance fields, so I tried to construct a distance estimate. I tried using \(|F_p(c, z)| - t\) as a first try, where \(t\) is a small thickness to make the shapes solid, but that extended beyond the edges of each pringle and looked very wrong. The interior of the pringle has \(\left|\frac{\partial F_p}{\partial z}\right| \le 1\) so I added that to the distance estimate (using max() for intersection) to give:

float DE(vec2 c, vec2 z0) { vec2 z = z0; vec2 dz = vec2(1.0, 0.0); float de = 1.0 / 0.0; for (int p = 1; p <= MaxPeriod; ++p) { dz = 2.0 * cMul(dz, z); z = cSqr(z) + c; de = min(de, max(length(z - z0), length(dz) - 1.0)); } return 0.25 * de - Thickness; }

Note that this has complexity linear in MaxPeriod, my first attempt was quadratic which was way too slow for comfort when MaxPeriod got bigger than about 10. The 0.25 at the end is empirically chosen to avoid graphical glitches.

I have not yet implemented a 4D raytracer in FragM, though it's on my todo list. It's quite straightforward, most of the maths is the same as the 3D case when expressed in vectors, but the cross-product has 3 inputs instead of 2. Check S. R. Hollasch's 1991 masters thesis Four-Space Visualization of 4D Objects for details. Instead I rendered 3D slices (with 4th dimension constant) with 3D lighting, animating the slice coordinate over time, and eventually accumulating all the 3D slices into one image to create a holographic feel similar to Melinda Green's original concept.

Source code is in my fractal-bits repository:

]]>git clone https://code.mathr.co.uk/fractal-bits.git

Wikipedia on Autostereograms doesn't exactly say how to construct them, so I drew some diagrams and scribbled some equations, and came up with this.

Given background distance and eye separation in inches, resolution in dots per inch, width in pixels, and count the number of vertical strips in the image background, compute the accomodation distance as follows:

accomodation = background * (1 - (width / count) / (separation * resolution))

This will be less than the background distance for positive eye separation (wall-eyed viewing) and greater for negative eye separation (cross-eyed viewing).

Then compute a depth value for each pixel, with the far plane at background inches from the camera. Ray marching a distance field is one way to do this, see Syntopia's blog for details. The scene should be between the camera and the far plane. Sharp depth discontinuities are disturbing, so position it as close to the far plane as possible.

The next step is converting the depth to a horizontal offset at the accomodation plane, using similar triangles:

delta = (depth - accomodation) * separation / depth;

Then compute the normalized texture coordinate increment that matches that offset:

increment[i] = 1 / (delta * resolution)

The i here is the horizontal index of the pixel, you need the whole scanline at a time
if you want to center the texture instead of aligning it to an image edge. Now we have the
speed of texture coordinate change, we can **integrate** this
to get the actual texture coordinate for each pixel:

double sum = 0; for (int i = 0; i < width; ++i) { sum += increment[i]; coordinate[i] = sum; }

and then do the texture lookup, rebasing it to the center of the image (twice % because negatives behave weird in C):

int u = floor((coordinate[i] - coordinate[width / 2]) * texture_width); u %= texture_width; u += texture_width; u %= texture_width; int v = j; v %= texture_height; v += texture_height; v %= texture_height; pixel[j][i] = texture[v][u];

Image above uses eye separation = -3 (cross-eyed), background distance = 12, 1920x1080 at 100dpi, count 32, the scene is a power 8 Mandelbulb copy-pasted from Fragmentarium, the texture is a slice of a NASA starfield image made seamless in GIMP.

]]>I blogged about fish variations in 2013. This \(\{6,4\}\) version was implemented in Fragmentarium and is coloured in 6 colours. You can download the source code: hyperbolic_fish.frag

]]>A computer-aided drawing using conformal mapping and hyperbolic tiling. The title is taken from ancient Greek mythology.

I found some old but cool software for numerical conformal mapping: Zipper. It's slightly weird to use, but it seems quite powerful. For my purposes I didn't like the way it skipped output for points that weren't inside the target region, so I patched its FORTRAN 77 code to output nonsense values instead. This makes sure that the Nth line of output corresponds to the Nth line of input.

diff -ruw conformal/zipper/inverse.f conformal-new/zipper/inverse.f --- conformal/zipper/inverse.f 2010-10-12 16:51:27.000000000 +0100 +++ conformal-new/zipper/inverse.f 2013-08-17 02:41:54.644940816 +0100 @@ -76,11 +76,8 @@ c a circle to the disk is not 1-1 on the outside. c thus we will just delete these points. if(x*x+y*y.gt.(1.d0+1.d-8))then - write(*,*)' ' - write(*,*)' z(j) outside region, so pullback outside disk,' - write(*,*)' and will be eliminated from output.' - write(*,*)' j=',j,' inverse of z(j)=', z(j) - goto 984 + x=-100 + y=-100 endif write(3,999)x,y 984 continue

Zipper's demo csh script provided me with enough clues to write my own driver script in bash:

#!/bin/bash # generate raw mipmap data for the source tex.png for d in 1024 512 256 128 64 32 16 8 4 2 1 do convert tex.png -geometry "${d}x${d}" tmp.ppm tail -c "$(( d * d * 3))" tmp.ppm done > texture.rgb # generate the shape to fill with butterflies ./curve > init.dat # massage the shape into zipper input ./zipper/polygon >/dev/null <<-"EOF" init.dat 500 poly.dat EOF # compute the conformal mapping ./zipper/zipper >/dev/null # calculate the pre-images in the unit disk for each pixel rm "preimage.grid" touch "preimage.grid" for j in $(seq 1800) do ./row "${j}" > "grid.dat" ./zipper/inverse >/dev/null <<-"EOF" grid.dat grid.pre EOF cat "grid.pre" >> "preimage.grid" done # fill with a textured hyperbolic tiling ./tile < preimage.grid > tiling.ppm # finally composite with the bottle and stopper in gimp

I wanted to generate an image with 2:3 aspect ratio, so I chose the logical coordinates of the image to be in [0,4]x[0,6]. I wanted a reasonable resolution, so the device dimensions are 1200x1800. I generate mipmaps from the base texture for anti-aliasing.

Generating the boundary curve is quite easy, as zipper/polygon reads input from a text file with one point per line, with the last point specifying which point inside the simple closed curve formed from the previous points will be mapped to the origin of the unit disk. My curve has a regular octagon at the bottom with the centre of the top edge at (2,1). The edge length is e and the centre of the octagon is at (2,h), with r being the distance from the centre to each vertex. The neck of the bottle has height e/2, and then it extends up and out at 45 degrees to far beyond the page boundaries (to avoid a visible edge within the final image).

#include <math.h> #include <stdio.h> const double pi = 3.141592653589793; int main(int argc, char **argv) { double e = 1 / (1.5 + sqrt(2)); double h = e * (1 + sqrt(0.5)); double r = sqrt((e/2)*(e/2) + (h-e/2)*(h-e/2)); double x, y; for (int i = 0; i < 8; ++i) { double t = pi / 2 + pi / 8 + i * pi / 4; x = 2 + r * cos(t); y = h + r * sin(t); printf("%f %f\n", x, y); } y += e/2; printf("%f %f\n", x, y); x += 20; y += 20; printf("%f %f\n", x, y); x -= 20 + e + 20; printf("%f %f\n", x, y); x += 20; y -= 20; printf("%f %f\n", x, y); printf("%f %f\n", 2.0, 1.0); return 0; }

Zipper's inverse program has a hardcoded limit on the number of points it can transform, and rather than try and fix it I split my input into smaller batches - one for each row of pixels in the output image. Outputting a row's coordinates to a text file works like this:

#include <stdio.h> #include <stdlib.h> int main(int argc, char **argv) { int j = atoi(argv[1]); double y = j / 1800.0 * 6; for (int i = 1; i <= 1200; ++i) { double x = i / 1200.0 * 4; printf("%f %f\n", x, y); } return 0; }

The last step in the driver script takes the transformed coordinates (which are now inside the unit disc, or (-100,-100) for points that were outside the region) and computes the colour for each pixel. First define some constants and global variables (not the best coding style, but it worked for this small program):

#include <math.h> #include <stdio.h> const double pi = 3.141592653589793; const double c = -0.5; // cos(2 * pi / 3) const double s = 0.8660254037844387; // sin(2 * pi / 3) const double sqrt3 = 1.7320508075688772; // sqrt(3) static double center, radius; static int lod = 0;

Here center and radius will be set to the circle that forms the right hand edge of an equilateral hyperbolic triangle centred at the origin in the Poincaré disk, and lod is used to pass the computed mipmap level of detail from the main loop through to the texture lookup.

The texture lookup code is probably the worst code I've written for some time. There's a huge global structure to contain the raw texture data read from disk, and copy/paste coding to grab the right mipmap level's data and send it to stdout.

static struct { unsigned char m10[1024][1024][3]; unsigned char m9[512][512][3]; unsigned char m8[256][256][3]; unsigned char m7[128][128][3]; unsigned char m6[64][64][3]; unsigned char m5[32][32][3]; unsigned char m4[16][16][3]; unsigned char m3[8][8][3]; unsigned char m2[4][4][3]; unsigned char m1[2][2][3]; unsigned char m0[1][1][3]; } texture; void texture2D(int j, int i) { i >>= (10 - lod); j >>= (10 - lod); switch (lod) { case 10: putchar(texture.m10[j][i][0]); putchar(texture.m10[j][i][1]); putchar(texture.m10[j][i][2]); break; // ... and so on all the way down to ... case 0: putchar(texture.m0[j][i][0]); putchar(texture.m0[j][i][1]); putchar(texture.m0[j][i][2]); break; } }

The hyperbolic tiling is generated by reflections in the sides of the hyperbolic triangle mentioned earlier. Hyperbolic reflection in the Poincaré disk model is circle inversion. There are three sides, so there are three transforms, and each side is rotated about the origin by 2pi/3. The circle inversion is implemented once in transform1().

void rotate(double x, double y, double *x0, double *y0) { *x0 = c * x + s * y; *y0 = -s * x + c * y; } void unrotate(double x, double y, double *x0, double *y0) { *x0 = c * x - s * y; *y0 = s * x + c * y; } void transform1(double x, double y, double *x0, double *y0) { double x1, y1, r1, x2, y2; x1 = (x - center) / radius; y1 = y / radius; r1 = x1*x1+y1*y1; x2 = x1 / r1; y2 = y1 / r1; *x0 = x2 * radius + center; *y0 = y2 * radius; } void transform2(double x, double y, double *x0, double *y0) { double x1, y1, x2, y2; rotate(x, y, &x1, &y1); transform1(x1, y1, &x2, &y2); unrotate(x2, y2, x0, y0); } void transform3(double x, double y, double *x0, double *y0) { double x1, y1, x2, y2; unrotate(x, y, &x1, &y1); transform1(x1, y1, &x2, &y2); rotate(x2, y2, x0, y0); }

The process of generating a tiling is by reflecting the original point in all the sides. If the new points are all further away from the origin, then the original point must be in the root triangle, otherwise pick the the new point that is closest to the origin and repeat (giving up if it takes too many steps). Once within the root triangle, reflect in the x-axis if necessary so that there are an even number of reflections in total - this makes the orientation of all the triangles the same. This is particularly important as some tilings might have paths with different numbers of reflections from nearby original points in the same triangle to their final points in the root triangle, which would give an unsightly seam.

Once this terminates, if we're in the root triangle then we need to look up the pixel colour from the texture. But a hyperbolic triangle is curved, and a texture is a flat square grid. I used ternary coordinates, computing the distance from each edge in the hyperbolic triangle, and using their ratios to compute a point in a flat triangle embedded in a square. The distance() function here is actually Euclidean distance which is not quite right, but the maths for hyperbolic distance is much more involved, and I'd need to work out how to find the closest point on a given line to a given point in the Poincaré disk model.

double distance(double x, double y) { double dx = x - center; double dy = y; return sqrt(dx * dx + dy * dy) - radius; } void colour(double x, double y, double r, int depth) { if (0 < depth && r < 1) { double x1, y1, r1, x2, y2, r2, x3, y3, r3; transform1(x, y, &x1, &y1); r1 = x1*x1+y1*y1; transform2(x, y, &x2, &y2); r2 = x2*x2+y2*y2; transform3(x, y, &x3, &y3); r3 = x3*x3+y3*y3; if (r <= r1 && r <= r2 && r <= r3) { if (depth & 1) { y = -y; } rotate(x, y, &x1, &y1); unrotate(x, y, &x2, &y2); double a = distance(x, y); double b = distance(x1, y1); double c = distance(x2, y2); double k = a + b + c; a /= k; b /= k; c /= k; double u = b + c / 2; double v = sqrt3 * c / 2; int i = fmin(fmax(1024 * u, 0), 1023); int j = fmin(fmax(1024 * v, 0), 1023); texture2D(j, i); } else if (r1 <= r2 && r1 <= r3) { colour(x1, y1, r1, depth - 1); } else if (r2 <= r1 && r2 <= r3) { colour(x2, y2, r2, depth - 1); } else if (r3 <= r1 && r3 <= r2) { colour(x3, y3, r3, depth - 1); } else { putchar(255); putchar(255); putchar(255); } } else { putchar(255); putchar(255); putchar(255); } }

The main program computes the generating triangle (here a {3,7} tiling) and reads the raw texture mipmap data. Then it spits out a PPM header, and computes colours for all the input coordinates. The most interesting part is the level of detail computation: h is the hyperbolic distance between two neighbouring pixels, the ratio of this to a base distance h0 determines how much the image has been stretched or shrunk - looking up into an appropriately filtered and downscaled texture for parts that are shrunk is the essence of mipmapping, providing a computationally cheap way to avoid aliasing.

int main(int argc, char **argv) { double dh = cos(pi / 7) / sin(pi / 3); double de = sqrt((dh - 1) / (dh + 1)); radius = (1 - de * de) / (2 * de); center = radius + de; FILE *tex = fopen("texture.rgb", "rb"); fread(&texture, sizeof(texture), 1, tex); fclose(tex); printf("P6\n1200 1800\n255\n"); double x, y; double ox = 0, oy = 0; double h0 = acosh(dh) * sqrt3 / 256; while (2 == scanf("%lf %lf\n", &x, &y)) { double h = acosh(1 + 2 * ((x - ox) * (x - ox) + (y - oy) * (y - oy)) / ((1 - x * x - y * y) * (1 - ox * ox - oy * oy))); lod = 10 - log2(h / h0); lod = lod < 0 ? 0 : lod > 10 ? 10 : lod; colour(x, y, x*x+y*y, 32); ox = x; oy = y; } return 0; }

There are a few enhancements left to do (mainly interpolation within and between texture mipmap levels for smoother appearance), and it might be fun in the future to make an animation of the stopper being removed, but it's good ennough for now.

]]>I drew part of a \(\{6,3\}\) tiling of the plane with stylized fish, the full tiling would look more like:

The shape of the fish is constructed with compass and straightedge. First create a hexagon with origin \(O\), vertices \(V_i\) and edge midpoints \(E_i\). Define \(\operatorname{line}(p, q)\) as the line passing through \(p\) and \(q\), and \(\operatorname{circle}(p, q)\) as the circle centered at \(p\) passing through \(q\), the remaining points of the fish can be found:

\[\begin{align*} c &= \operatorname{line}(E_3, E_4) \cap \operatorname{line}(V_4, V_0) \\ i &= \operatorname{line}(E_4, E_5) \cap \operatorname{line}(V_5, V_3) \\ e &= \operatorname{circle}(V_4, c) \cap \operatorname{line}(E_4, E_5) \text{ furthest from } E_4 \\ g &= \operatorname{circle}(V_5, i) \cap \operatorname{line}(E_4, E_3) \text{ furthest from } E_4 \end{align*}\]

It turns out that this construction is also valid in hyperbolic space, because nothing depends on the existence of unique parallels. Here's a diagram showing four hexagons about each vertex in the Poincaré disk model of hyperbolic geometry:

And another showing three octagons about each vertex:

In hyperbolic space areas and angles are connected. The key step in making hyperbolic tilings is finding the side lengths of the fundamental triangle, between the origin \(O\), vertex \(V\), and midpoint of neighbouring edge \(E\). All the angles are known given the Schläfli symbol \(\{p,q\}\): they are \((\pi/p, \pi/q, \pi/2)\). The side lengths can be calculated using the hyperbolic law of cosines:

\[\begin{align*} \cosh(|OV|) = \frac{\cos(\pi/2) + \cos(\pi/p) \cos(\pi/q)}{\sin(\pi/p) \sin(\pi/q)} \\ \cosh(|OE|) = \frac{\cos(\pi/q) + \cos(\pi/p) \cos(\pi/2)}{\sin(\pi/p) \sin(\pi/2)} \\ \cosh(|EV|) = \frac{\cos(\pi/p) + \cos(\pi/q) \cos(\pi/2)}{\sin(\pi/q) \sin(\pi/2)} \end{align*}\]

Here are some fish tiling variations in hyperbolic space, with Poincaré half-plane model representations too:

I wrote the implementation in Haskell. My code consists of a library for compass-and-straightedge construction with instances for Euclidean (flat) and hyperbolic (negatively curved) space with spherical (positively curved) space a possibility in the future, along with embeddings into Euclidean space for visualisation using the Diagrams library. The cool part is that the same code generates all the variations from a few parameters.

]]>Welcome!

]]>The 43MB video linked above is pretty much generated by these 70 lines of GLSL fragment shader:

#extension GL_EXT_gpu_shader4 : enable uniform vec4 u; // unit uniform vec4 v; // unit uniform vec4 w; uniform float bigness; uniform vec3 lightPos; void main() { vec2 z = bigness * gl_TexCoord[0].xy; vec4 p = u * z.x + v * z.y + w; // nearest sphere vec4 o1 = round(p); vec4 o2 = o1 + vec4(0.5); if (p.x < o1.x) o2.x -= 1.0; if (p.y < o1.y) o2.y -= 1.0; if (p.z < o1.z) o2.z -= 1.0; if (p.w < o1.w) o2.w -= 1.0; vec4 d1 = o1 - p; vec4 d2 = o2 - p; float l1 = dot(d1,d1); float l2 = dot(d2,d2); vec4 o = o1; bool odd = false; if (l2 < l1) { o = o2; odd = true; } // intersection circle float ou = dot(u, o - w); float ov = dot(v, o - w); vec4 c = u * ou + v * ov + w; vec4 oc = o - c; float d = dot(oc, oc); float r2 = 0.25 - d; vec2 dz = z - vec2(ou, ov); float dr2 = dot(dz, dz); vec4 colour = vec4(0.0, 0.0, 0.0, 0.0); float alpha = 0.0; float brightness = 0.0; float specular = 0.0; if (dr2 < r2) { // inside circle float k = o.x + o.y + o.z + o.w; bool even = abs(2.0 * floor(k / 2.0) - k) < 0.001; if (odd) { if (even) { colour = vec4(1.0, 0.7, 0.2, 1.0); } else { colour = vec4(0.7, 1.0, 0.2, 1.0); } } else { if (even) { colour = vec4(1.0, 0.2, 0.7, 1.0); } else { colour = vec4(0.7, 0.2, 1.0, 1.0); } } // sphere shading const float shine = 16.0; float height = sqrt(r2 - dr2); vec3 pos = vec3(z, height); vec3 light = normalize(lightPos - pos); vec3 normal = normalize(vec3(dz, height)); float dlm = max(dot(light, normal), 0.0); vec3 reflected = 2.0 * dlm * normal - light; specular = pow(max(reflected.z, 0.0), shine); brightness = 0.0625 + dlm; } else { discard; } gl_FragColor = vec4(mix(colour.rgb * brightness, vec3(1.0), specular), 1.0); }

In 3D Euclidean (flat) space, you could imagine filling it with cubes of side length 1, much like graph paper in 2D. Then you could fill each cube with a sphere of radius 1/2. In 3D the gap between those spheres (at the corners of each cube) has diameter sqrt(3) - 1 (about 0.732). But, in 4D space a similar hypercube lattice each filled with spheres leaves a gap with diameter sqrt(4) - 1 (exactly 1). This means you can fit another radius 1/2 sphere at each corner gap.

The spheres with integer coordinates (the ones at the corners) can be grouped together, and the ones with integer+1/2 coordinates (the ones in the middle of the cubes) can form another group. Moreover, you can use a parity argument to further subgroup each half: determining whether the sum of the coordinates is odd or even gives four groups of spheres in total, arranged in a hard to imagine 4D chessboard-like pattern.

In 3D space you might imagine taking a 2D slice through the grid of spheres. Depending where and at what angle you slice, you might get a regular square grid of circles and gaps or a more unpredictable pattern of differently sized circles and gaps where you chop through different parts of the spheres. You can do the same in the 4D lattice, which can similarly give a regular square grid of circles or irregular-looking patterns.

So, enough imagining: how to actually visualize it. The first step is to define a 2D slice through 4D space:

P(u,v) = u*U + v*V + W

Here lowercase letters are scalar real numbers, and uppercase letters are vectors. For convenience, assume U and V have length 1 and point in different enough directions. W is the origin of the plane, and U and V can be thought of as the local axes within the 2D plane.

The next step given a point (u,v) in this plane (which will eventually be drawn as a pixel on the screen), is to find the center of the nearest sphere in the lattice. The nearest integer sphere can be found by rounding each coordinate of P(u,v), but the nearest half-integer sphere is a bit more tricky - the method in the shader source code above checks in each dimension if the rounded coordinate is bigger or smaller than unrounded coordinate, and picks the neighbouring half-integer coordinate so that the unrounded coordinate is between the two sphere coordinates. Then compare the actual distance between these sphere centers and P(u,v), and pick the closest.

Now we have the closest sphere centered at O, but we want to find the circle that is formed if our plane is slicing through that sphere. I thought the algebra would be hairier than it was, but luckily symmetry comes to the rescue. The center of the circle P(ou, ov) is the nearest point on the plane to the center of the sphere, which can be found by minimizing the distance function (knowing that the differential of a function is 0 at extrema). Leaving out the workings (and assuming U and V are unit length):

ou = dot(U, O - W) ; ov = dot(V, O - W)

But there might be no circle here at all, if (u,v) happens to fall in a gap. Luckily we know the sphere has radius 1/2, so we can check that ||P(ou, ov) - O|| < 1/2. The next step is to find the radius of the circle, which is quite straightforward by Pythagoras' Theorem - the line from the sphere center to the circle center is perpendicular to the plane of the circle, and the hypotenuse has length 1/2, and we already computed the length of the adjacent side.

Now (unless we bailed out with no circle) we have our starting point (u,v), the center of a circle that contains it (ou, ov), and the radius of the circle r. But we want to make it look like a sphere! So we need to find the height of the sphere's surface above the plane. Then for shiny lighting it's easy to calculate the surface normal (as a sphere is uniformly round, the normal points away from its center). With the surface position and normal, and a light moving around in 3D above our plane, standard Phong lighting can make it look like proper disco balls.

]]>The 8th annual Piksel festival for Electronic Art and Technological Freedom is in full swing, lots of stuff going on with live performances, exhibitions, workshops, and presentations.

At 15:00 on Sunday 21st November 2010 at Studio USF, Bergen, Norway I'll be giving a presentation on the art and tech behind my project RDEX - covering aesthetics of higher dimensions, emergent behaviour, GPU programming for audio-visuals, using databases for image similarity, and so on.

RDEX (reaction-diffusion explorer) is an installation and performance piece that explores in an autonomous hyperspace mathematical model, searching for interesting emergent behaviour (life-alike, alife).

At 21:00 Sunday 21st November 2010 at Studio USF, Bergen, Norway is a night of live performances. RDEX for live audio-visual performance has a sequencer to choose 4D parameter points from the database, with the resulting evolving patterns converted to sound using a variant of wave-terrain synthesis.

]]>(For the sake of this post, it is better to consider the language non-profane.)

Brainfuck is an esoteric programming language based on a minimal Turing machine: it has a data tape and a code array, the instructions in the code modify the data in cells on the tape. Despite the presence of only a few simple instructions in the language it is theoretically possible to compute everything that can be computed within the Church/Turing model of computation; further an even more minimal derivative of Brainfuck, Boolfuck, restricts the data on the tape to consist only of single bits (either 0 or 1, instead of bytes from 0 to 255).

Now, consider changing the structure of the data 'tape', in particular extending it into a 2D hyperbolic plane. Hyperbolic geometry allows many parallel lines to a given line through a given point not on the line, with the consequence that the hyperbolic plane has exponentially more space than the common flat Euclidean plane.

Now, it is possible to tile the hyperbolic plane with congruent shapes, in many more ways than flat space can be tiled. For example, with one particularly shaped pentagonal tile, one can tile the hyperbolic plane in uncountably infinitely many ways. To navigate in this new arrangement of cells in this hyperbolic pentagonal tape, one adds three new instructions to the language: in addition to moving the cursor in the data tape East and West, new instructions allow moving North, South-East and South-West.

This best visualised in the Poincaré half-plane model of hyperbolic geometry, in which shapes are distorted to give a horizon at infinity to the South (compare with the Poincaré disk model in which there is a circular horizon at infinity, most popularized by M C Escher's Circle Limit I-IV prints).

This 2D hyperbolic modification of Boolfuck I call
**Hyperfuck**, for fairly obvious reasons.

Now, the pentagonal tiling described above has a kind of tree-like memory property: if you proceed from a starting location and head North, you might enter the pentagon above from either its South-East edge or its South-West edge: this allows information to be encoded in the geometry of the initial configuration of the system. By modifying data in the cells and moving suitably, comparing the results of the movements with what you expect allows you to recover the stored bitstream. Further, this information need have no bound, for example one could encode an infinite library of every book ever written and that ever will be written (compare with Borges, the Shakespeare Typing Monkeys, and the text entry software Dasher).

However, a suitable source of information if a programmer is to be able to write useful programs more easily needs to be widely known and predictable: thus I propose the ASCII bitstream of a certain authorized version of the Christian Bible.

This liturgical modification of Hyperfuck I call
**King James Hyperfuck**,
for fairly obvious reasons.

A simple program can reconstruct the text; its source code:

+[^\;^+]

The current King James Hyperfuck interpreter is implemented in Haskell, using OpenGL to visualize the data tape, code tape, and output text. It also sends event information (the current instruction being executed) to Pure-data for sonification. There is also a (currently unfinished) SVG and ECMAScript version to run inside a web browser. The original prototype was implemented in C. Source code is available at maximus/kjhf at gitorious.org kjhf at code.mathr.co.uk.

]]>I played on Wednesday at OpenLab OpenNight 5, live audio/visuals with rdex.

Audio download options:

Alex McLean took a nice photo.

]]>