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.

]]>Live improv techno performance on 2010-02-17 at Goldsmiths Digital Studio Expo, using Pd, Gem, GridFlow, etc.

Downloads of the digital render:

]]>Some work in progress demos of an audio-visual performance under development.
*MarkIII* is named after the 3rd-order
Markov chains
it uses to generate sequences of sounds.

Demo 1 is the first proof of concept - sequences of harmonics of a simple tone.

Demo 2 extendes this to sequences of rhythms as well as harmonics of more complex tones.

Demo 3 returns to simple harmonics, but adds visualisation of the Markov Chains in 3D Euclidean space.

Demo 4 is a live recording of the same setup, filmed with a digital camera.

Demo 5 adds more complex organ-based sound, while also changing the visualisation to follow one point in rotations of a 3-sphere in 4D space.

Demo 6 follows two points in a minor scale in this 3-sphere.

Demo 7 improves the rotation algorithm but still suffers from gimbal lock.

Demo 8 adds additional voices and further rotation and colour tweaks.

Demo 9 has a completely new and improved rotation algorithm, however it still rotates the "long way" sometimes and some numerical instability at times.

MarkIII Demos were made with Pure-data using the PdLua, Gem and Zexy libraries.

The source code is available under GNU GPL3+ license, but is still changing rapidly and is unfinished and undocumented:

svn co https://code.goto10.org/svn/maximus/2010/markiii

Downloads:

1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | |
---|---|---|---|---|---|---|---|---|---|

higher quality | n/a | 33MB | 84MB | 44MB | 84MB | 135MB | 72MB | 71MB | 67MB |

smaller size | 8MB | 5MB | 10MB | 13MB | 10MB | 14MB | 7MB | 9MB | 7MB |

More download options available at the MarkIII Demos Internet Archive page.

]]>Live improv techno performance on 2009-11-13 at Kiblix, Maribor, Slovenia, using Pd, Gem, GridFlow, etc.

Downloads of the digital render:

]]>