K I Martin recently popularized a perturbation technique to accelerate Mandelbrot set rendering in his SuperFractalThing program. I wrote up some of the mathematics behind it, extending Martin's description to handle interior distance estimation too. Unfortunately it's very easy to get glitchy images that are wrong in sometimes subtle ways.

The most obvious reference point is usually in a central minibrot, which means it is strictly periodic:

-1.760732891182472726272e+00 + 1.302137831089206469511e-02 i 4.0194366942304651e-14 @ -1.760732891182472889620498413132e+00 +R 1.302137831089204904674633295328e-02 iR

The current version of mightymandel computes an error estimate and shades worse errors redder. A better reference point for this image is in a non-central minibrot near the tip of a solid red patch:

-1.760732891182472726272e+00 + 1.302137831089206469511e-02 i 4.0194366942304651e-14 @ -1.76073289118248636633168329119453103e+00 +R 1.30213783108675495217125732772512438e-02 iR

Nearby higher period non-central minibrots tend to work even better, and their limit is a pre-periodic point - one that becomes periodic after a finite number of iterations. I explored a bit the basins of attraction of preperiodic points for a couple of embedded Julia sets (which are the features that are most often glitchy).

-1.7607328089719322109e+00 + 1.3021307542195548201e-02 i 1.5258789062500003e-05 @ 1 1/2 2 1/2 3 1/3 6 4/5 35 A

The saturated dots at the tips and spirals in this period 35 embedded Julia set are the preperiodic points of interest. They have period 3 (matching the outer influencing island) and preperiods 35 (matching the inner influencing island) and 36.

Then zooming deeper to the near the period 35 island and into one of its hairs finds a doubly-embedded Julia set between the period 35 outer influencing island and a period 177 inner influencing island. Rendering the Newton Basins now needs more than double precision floating point, and my Haskell code using qd's DoubleDouble took almost 4 hours on a quad core. This time the points of interest have period 35 (matching the outer island) and preperiods 177 (matching the inner island) and 178.

-1.76073288182181309054484516839 + 0.01302138541499395659022491468 i 2.2737367544323211e-13 @

Here's the same view rendered with mightymandel, first with the central minibrot as reference:

-1.76073288182181252065e+00 + 1.30213854149941790026e-02 i 2.2737367544323211e-13 @

Then a non-central minibrot:

-1.76073288182181252e+00 + 1.302138541499417901e-02 i 2.2737367544323211e-13 @ -1.760732881821779248788320301573183e+00 +R 1.302138541488527885806724080050218e-02 iR

And finally a limiting pre-periodic reference point:

-1.76073288182181252e+00 + 1.302138541499417901e-02 i 2.2737367544323211e-13 @ -1.7607328818218582927504155115035552 +R 1.3021385414920574393027950216090353e-2 iR

No single reference point gives a red-free image, but combining multiple reference points, each appropriate to various parts of the image, looks like it would be a promising approach - and the knowledge about where the view is in relation to outer and inner influencing islands and their (potentially echoed) embedded Julia sets could perhaps be used to calculate a few candidate reference points automatically.

]]>I finally got around to postprocessing the photos I took of the pages in my notebook written on topics concerning the Mandelbrot set. You can see it here: Mandelbrot Notebook

One of the (too-many) long term projects I have is to write a book about the Mandelbrot set that bridges the gap between popular science books ("wow fractals are cool") and mathematical texts ("theorem: something hard and obscure"). I've never written a book before and found myself getting distracted by irrelevancies (like page layout, fonts etc) and re-editing text over and over instead of actually adding new content, so I've been experimenting with git version control commit messages:

git clone https://git.gitorious.org/maximus/book.git cd book git log --color -u --reverse

The working title is How to write a book about the Mandelbrot set.

]]>Previously I made a trailer for Haystack Situations, but never got around to sorting out a way to make prints available. At the end of last year I set it up to generate a new image every day in the form of a podcast, but (sur)real life intervened and I didn't get around to publicising it until now.

Clicky the piccy for the Haystack Situations homepage with thumbnails of them all, the latest week's worth at the top are available as PDF, and there's a PDF podcast RSS feed (icon at the top right) should you get addicted to hunting needles on a daily basis.

You can get the source here: 2014-03-01_haystack_situations.tgz, but beware the not-so-clean and even less documented code - I can try to help if you have trouble compiling and running (just ask), it needs these Debian packages (and possibly others):

]]>aptitude install build-essential libgsl0-dev libcairo-dev lmodern

Figure 4.22 on pages 204-205 of **The Science Of Fractal Images**
is presented with this description:

A region along the cardioid is continuously blown up and stretched out, so that the respective segment of the cardioid becomes a line segment. ... Our blow-up factor is chosen accordingly to the result that all disks in Figure 4.22 have the same size.

While waiting for a train the other day, I remembered this image, and wanted to recreate it, which required deriving the equations (they were not presented in the book).

A Möbius transformation of the complex plane is a rational function of the form

\[ f(z) = \frac{a z + b}{c z + d} \]

with \( a d − b c \ne 0 \). It has inverse

\[ f^{-1}(z) = \frac{d z - b}{-c z + a} \]

Möbius transformations are conformal, preserving angles but not lengths, and map generalized circles to generalized circles (generalized circles include lines as the special case of a circle through infinity). A Möbius transformation can be constructed from 3 points and their images. For our purposes, we can choose the images of \(P_0, P_1, P_\infty\) to be \(0, 1, \infty\), which gives

\[ F(z) = \frac {(z-P_0)(P_1-P_\infty)}{(z-P_\infty)(P_1-P_0)} \]

This \(F\) stretches and flattens a circle through \(P_0, P_1, P_\infty\) into a straight line, with the blow-up factor increasing towards \(\infty\). If \(P_\infty\) is chosen as a cusp, all the primary discs along the path to the cusp get stretched to be roughly the same size.

So far this works for circles, but cardioids require another conformal map to transform them into circles so that the Möbius transformation can work its magic. A cardioid with its cusp at \(0\) and its front at \(4\) is transformed into a unit circle centered at \(1\) by \(f(z) = \sqrt{z}\) (with the branch cut of the square root along the negative real axis). So to transform the period 1 cardioid of the Mandelbrot set (which has cusp at \(1/4\) and front at \(-3/4\)) to a unit circle centered at \(0\) the resulting conformal map is

\[ G(z) = \sqrt{1 - 4 z} - 1 \]

with inverse

\[ G^{-1}(z) = \frac{1 - (z+1)^2}{4} \]

Now, given \(P_0, P_1, P_\infty\) on the cardioid, form the Möbius transformation \(F\) for \(G(P_0), G(P_1), G(P_\infty)\), then \(F(G(c))\) maps \(c\) near the boundary of the Mandelbrot set cardioid to a flattened strip near the real axis. But when rendering images, we start from a rectangle with coordinates \([0..\text{width})\times[0..\text{height})\), so we have to work backwards to find the corresponding \(c\) in the parameter plane. Inverting \(k = F(G(c))\) gives \(G^{-1}(F^{-1}(k)) = c\).

Moreover, for distance estimation colouring it's useful to colour pixels according to the distance estimate relative to the pixel spacing, which is no longer fixed when applying a conformal mapping with non-constant scaling. The scale factor associated with a conformal mapping is the absolute value of its derivative, and the relevant derivatives are

\[ \begin{aligned} \frac{\partial}{\partial z} \frac{a z + b}{c z + d} &= \frac{a d - b c}{(c z + d)^2} \\ \frac{\partial}{\partial z} \frac{1 - (z+1)^2}{4} &= \frac{-(z+1)}{2} \\ \frac{\partial}{\partial z} f(g(z)) &= \frac{\partial f}{\partial z}(g(z)) \frac{\partial g}{\partial z}(z) \end{aligned} \]

The third equation is the chain rule for derivative of function composition, which can be intuitively expressed as "multiply all the derivatives at each step".

Putting together all the transformations and their derivatives and choosing the preimages \(P_0, P_1, P_\infty\) as bond points and cusps on the boundary of the Mandelbrot set, gives some stretched cusp pictures.

*Map of strip locations.*

*Elephant valley (cyan).*

*Seahorse valley (magenta).*

*"Double rabbit valley" (yellow).*

Based on a drawing from a year an a half ago:

I showed it to Brent Yorgey in the **#diagrams** IRC channel and he made this:

strip :: (Double -> Double) -> Double -> Double -> Int -> Double -> Diagram SVG R2 strip f lo hi n offset = [lo, lo + (hi - lo) / (fromIntegral n - 1) .. hi] # map (square . f) # hcat' with {sep = offset, catMethod = Distrib} # fc black example = vcat' with { sep = 3, catMethod = Distrib } (replicate 7 str) # centerXY # pad 1.5 where str = strip (\x -> cos x + 1) (-pi) pi 23 3--Lozenge (Brent Yorgey)

Then I showed him how they interleave and he made this:

strip :: (Double -> Double) -> Double -> Double -> Int -> Double -> Diagram SVG R2 strip f lo hi n offset = [lo, lo + (hi - lo) / (fromIntegral n - 1) .. hi] # map (square . f) # hcat' with {sep = offset, catMethod = Distrib} lozenge = vcat' with { sep = 3, catMethod = Distrib } (replicate 7 str) # centerXY where str = strip (\x -> cos x + 1) (-pi) pi 23 3 example = mconcat [ lozenge # fc black , lozenge # fc red # translateY (-1.5) # translateX (width lozenge / 2 - 4.5) ]--Lozenges (Brent Yorgey)

These are so much simpler than my original code which was horrible C numerical stuff to converge a waveform to the right shape, printing numbers which I fed into GNUplot and then editing the image with GIMP to correct the aspect ratio. I don't have that original code any more, but here's a similar version:

#include <math.h> #include <stdio.h> #include <stdlib.h> const double pi = 3.141592653589793; int cmp(const void *x, const void *y) { const double *a = x; const double *b = y; if (*a < *b) return -1; if (*a > *b) return 1; return 0; } double y(double x) { return (cos(pi * x) + 1) / 2; } int main(int argc, char **argv) { if (argc < 2) { return 1; } int n = atoi(argv[1]); double a[2]; double x[2][n+1]; int w = 0; a[0] = 0.001; for (int i = 0; i <= n; ++i) { x[0][i] = (2.0 * i) / (2.0 * n + 1.0); } x[1][0] = 0; while (1) { printf("a\t= %g\n", a[w]); for (int i = 0; i <= n; ++i) { printf("x[%d]\t= %g\t%g\n", i, x[w][i], y(x[w][i])); } double s = y(x[w][0]); for (int i = 1; i <= n; ++i) { s += 2 * y(x[w][i]); } a[1 - w] = 1 / s; for (int i = 1; i <= n; ++i) { x[1 - w][i] = x[w][i-1] + a[w] * ((y(x[w][i-1]) + y(x[w][i]))/2 + y(x[w][n+1-i])); } w = 1 - w; qsort(&x[w][0], n+1, sizeof(x[w][0]), cmp); } return 0; }gcc -std=c99 -lm lozenge.c ./a.out 19 | head -n 2100 | tail -n 20 > lozenge.dat ; gnuplotunset key unset xtics unset ytics unset border set style fill solid set terminal png size 8192,8192 set output "lozenge-raw.png" plot [-0.2:1.2] [-0.7:0.7] "lozenge.dat" using 3:(0):\ ($3 - $4 * 0.0512405/3):($3 + $4 * 0.0512405/3):\ ((-1+$4) * 0.0512405/3):((1-$4) * 0.0512405/3) with boxxyerrorbars,\ "lozenge.dat" using (1-$3):(0):\ (1-$3 - $4 * 0.0512405/3):(1-$3 + $4 * 0.0512405/3):\ ((-1+$4) * 0.0512405/3):((1-$4) * 0.0512405/3) with boxxyerrorbarsgimp (crop image)

A further experiment led to this gradient with 5 colours out of phase:

Do check out the links above to the Haskell diagrams pastebin, it's quite awesome.

]]>Distance estimation is commonly used in fractal rendering. In
Appendix D of **The Science Of Fractal Images**, Yuval
Fisher describes an algorithm for rendering the Mandelbrot set by
drawing discs that can't possibly contain any of the boundary of the
set, using the distance estimate at the centre to bound the radius
of the discs. Any point inside a disc need not be considered, which
speeds up rendering by reducing the number of points (and their discs)
that need to be computed:

Today in the **#haskell-blah** IRC channel, **lpsmith**
was wondering about how to speed up visualisation of voting simulations.
The models are described here:

Each point in the image corresponds to an election with the center of opinion located at that point. For every point, we simulate an entire election by scattering 200000 voters in a normal distribution around that point and collecting ballots from all of the voters; then we colour the point to indicate the winner.

-- Ka-Ping Yee

Imagine the N "candidates" are N fixed points in the Euclidean plane. The voters also are points in that plane, but imagine they are random points sampled from a 2-dimensional Gaussian distribution with prescribed variance and prescribed centerpoint (peak location) (x,y). Imagine these voters conduct an election to choose a winner. Voters prefer candidates that are nearer to them ("utility is a decreasing function of voter-candidate distance"). We then color the pixel at (x,y) with the color of the winner (selected from a fixed N-color palette). Do this for every (x,y) and the result is a multicolored picture. You get different pictures depending on the candidate-coordinates, the voting method used, and the variance of the Gaussian.

-- Warren D Smith

The distance estimation algorithm I came up with works like this. You have a fixed number of candidates as points in a 2D space. Form the perpendicular bisectors between each pair of candidates. This divides the space into a number of regions. Pick an individual voter. Find the minimum of the distances from the voter coordinates to the perpendicular bisectors between the pairs of candidates. Then a circle centered at the voter with this distance as its radius will be inside one of the regions in space. The candidate that the voter votes for will be the same everywhere in that circle.

But an electorate is a large number of voters. Do the above process
for all the voters in the electorate, tallying up the votes cast for
each voter, and forming a collection of all the voter radii. Once all
the votes are cast, you have the totals for each candidate, so you know
which candidate won, how many votes they received, and how many votes
the second-place candidate got. The result of the election to change
requires a number of voters at least half the difference between the
winner's votes and the runner up's votes to change their mind - call
this number the **swing**.

Now sort the collection of voter radii in ascending order, and pick the swing'th radius in the sorted collection. Now, if all the voters in the electorate shifted their opinion by that radius, only the swing voters could possibly have changed the candidate they voted for. So if the electorate shifted by less than that radius, the outcome of the election must remain the same. This allows us to fill a circle in the space of electorates (which we are aiming to visualize) with the election result, so we don't need to simulate elections for any of the other points that are inside it.

Here are some results of a 2000x2000 pixel image, first with 8k electorate samples taking around 1min and then with 80k electorate samples taking around 10min (consider that rendering each pixel individually would need 4M samples, estimated time to compute around 8 hours). Third is the reference image taken from Warren D Smith's website. All the images represent (0,0)-(200,200) with candidate coordinates (84, 114), (36, 108), (14, 124), and (186, 140), with σ=100, using Plurality voting method.

Source code in C99 using Cairo for graphics: votesim.c

]]>The Mandelbrot set is defined by iterations of a quadratic polynomial:

\[ \begin{aligned} F(z,c) &= z^2 + c \\ F^{n+1}(z, c) &= F^{n}(F(z,c),c) \end{aligned} \]

Atom domain
representation for the Mandelbrot set colours points according to
"near-miss" periods. The definition I'll use for \(a(c)\)
**atom period** of a point is:

\[ a(c) = q \text{ where } 1 \le q \le p \text{ minimizes } |F^q(0,c)| \]

taking \(p\) to be the period of the hyperbolic component containing \(c\) for interior points, or \(\infty\) for exterior points.

The atom periods of nearby points are likely to be the same, and
the **restricted atom periods** (limiting the maximum
iteration count to the desired atom period) form connected
**atom domains**. These domains may be expressed as:

\[ \{ c : |F^p(0,c)| \lt \min_{q=1}^{p-1} \{ |F^q(0,c) \} \} \]

The interior of each restricted atom domain with period \(p\) contains a nucleus \(c_0\) of period \(p\) with \(F^p(0,c_0) = 0\). On the boundary of the domain, \( |F^p(0,c)| = |F^q(0,c)| \) for some \(1 \le q \lt p \), which implies \(1 = |F^p(0,c)/F^q(0,c)| =: |G(c)| \). Assuming \(q\) to be constant throughout the whole domain, and approximating \(G\) by a power series at \(c = c_0 + h\) near the nucleus \(c_0\), gives:

\[ G(c) = G(c_0) + h \frac{\partial G}{\partial c}(c_0) + O(h^2) \]

Using the quotient rule \( (f/g)' = (f' g - f g')/g^2 \) and observiing that \(G(c_0) = 0\) results in

\[ \frac{\partial G}{\partial c}(c) = \frac{ \frac{\partial F^p}{\partial c} (0,c) }{ F^q(0, c) } \]

Taking \(|G(c)| = 1\) and assuming \(h\) is small enough to ignore the \(O(h^2)\) tail of the series gives an estimate for the size \(R_a\) of the atom domain:

\[ R_a = |h| = \left|\frac{ F^q(0,c_0) }{ \frac{\partial F^p}{\partial c}(0,c0) }\right| \]

So, given a nucleus \(c\) and its period \(p\) the size estimate of its atom domain can easily be calculated:

real_t atom_domain_size_estimate(complex_t c, int_t p) { complex_t z = c; complex_t dc = 1; real_t abszq = cabs(z); for (int_t q = 2; q <= p; ++q) { dc = 2 * z * dc + 1; z = z * z + c; real_t abszp = cabs(z); if (abszp < abszq && q < p) { abszq = abszp; } } return abszq / cabs(dc); }

But, how good is this approximate estimate? I checked it graphically. In these images, I used interior and exterior distance estimation (wiggly grey boundary of the mandelbrot set), and edge detection filter on the (unrestricted) atom domains (smooth grey arcs in the exterior of the set). I calculated the nucleus and period for each of the larger domains, and drew a circle around the nucleus (labeled with its period) with radius of the size estimate. A rectangle shows the location of the next image.

They seem to match up pretty well!

]]>Last week I remembered that years ago I made a cardboard model of a ring of eight cubes forming another cube, but interconnected in such a way that it could be folded through itself turning the original cube inside out. I had a search of the interwebs and found a page about the Magic Cube with some helpful animated gifs.

I spent some time scribbling on paper trying to figure out a net, and eventually came up with one using two pieces: one for each side of the cube. There is a a H shaped cut in the middle, and the tabs around the outside of each sheet attach to the inside edges of the other sheet. Each sheet forms eight corners (half a cube) - gluing the inner tabs to their neighbouring edges is quite simple.

Once each sheet is glued to itself it can be rotated through its central hole - set one up so it meshes nicely with the other and then the hard part: gluing all the tabs from each sheet to the other. It helps to use relatively stiff but not too thick card (I recycled some brightly coloured A4 folder dividers), and twisting the ring of cubes through itself while the glue is setting helps to make sure all is aligned correctly and also puts pressure on the right places.

Here are some pictures of work in progress:

You can download the Magicube net (600dpi SVG) and also the Magicube net generator source (Haskell using 'diagrams'), and have fun building your own!

]]>In a previous post I explored patterns of periods in the Mandelbrot set and promised a future post elaborating on external angles.

Here are some external rays landing on tips relative to the period 1 continent of the Mandelbrot set, with their angles labeled in turns using a finite binary expansion:

Actually, a finite binary expansion is really an infinite binary expansion, ending in an infinite string of 0s. But there's another infinite binary expansion for each number ending in infinite 0s, that ends in an infinite string of 1s: .xyz1000... = .xyz0111.... Writing down an infinite string is boring, so I write the repeated part in (parentheses).

Now we can get more abstract, replace "0" with "-" and "1" with "+":

The tuning algorithm for external angles works like this: start with a pair of periodic external angles (.(-),.(+)) whose rays land at the root of the same hyperbolic component H, and another external angle E. Now replace every 0 in E with -, and every 1 in E with +, to form the external angle F. Then F's ray lands near H in the same way that E's ray lands near the period 1 continent.

For example, the root of the period 3 island has external angles (.(011),.(100)), and the 1/2 bulb of the period 1 cardioid has external angles (.(01), (.10)). Applying the tuning algorithm, the 1/2 bulb of the period 3 cardioid has external angles (.(011 100),.(100 011)).

Now the two binary representations come in to play: for each tip, applying tuning gives two unequal angles with rays that land on the same point. Between the two angles there must be more of the Mandelbrot set, so it must be quite a hairy beast.

Some examples of hairiness:

There are islands in the hairs (because there are islands everywhere, though they get very small very quickly), and in the previous post I noted that around a period P island the periods in each successive layer of hairs seems to increase by P each time. Now I make that more precise.

Suppose we have an island I0 with angles (.(-),.(+)). Then the tip of its antenna has angles (.-(+),.+(-)). There will be more islands in the wake of the antenna. Suppose I1 is the lowest period island visible in the wake (visible means it isn't separated from the viewpoint by a ray-pair of lower period). I1 is in fact defined uniquely:

Assume there are two lowest-period visible islands with p = period(I1a) = period(I1b). So there are 4 rays of period p landing on I1a and I1b in the wake. Neighbouring rays of period p are separated by 1/(2^p - 1) so the total width of the wake must be more than 3/(2^p - 1). But: 3/(2^p - 1) is more than 1/(2^(p-1) - 1) when p > 2 so there is room for a period (p-1) island in the wake, which contradicts the assumption.

So, the width W of the wake satisfies: 1/(2^p-1) < W < 1/(2^(p-1)-1), and p > 2. The lower bound is .0...01(0...01) and the upper bound is .0...1(00...1), where the group of 3 digits surrounds the p'th digit. Because of the strict inequalities, the position of the first 1 in the digits of W must be at the p'th digit - if it was any different it would be outside the bounds.

Brief recap: given the roots of an island, find the wake of the tip, then the lowest period island has period of the first index of 1 in the digits of the width of the wake. Call this period p, and the angles of the wake (W-,W+) then the island in the hair has angles (ceil[(2^p-1)(W-)]/(2^p-1), floor[(2^p-1)(W+)]/ (2^p-1)) (Proof left as an exercise because I don't feel like typing it up: I used the fact that (2^p-1)(W-) is not an integer).

So far this is just the hair of the antenna, but there are hairs all over. Because of tuning, the width of the wakes of each successive layer of hairs on a period P island is 1/2^P times the previous layer. The lowest period in the antenna hair is p, and call the period in the next layer of hairs p'. Now algebraic manipulation shows that if the wake W is wide enough for p then the wake W' in the next level is wide enough for an island of period p+P, and a proof by contradiction shows that W' is not wide enough for an island of period p+P-1. Combining with the earlier uniqueness, the lowest period islands in the next level of hairs must have period p' = p + P.

]]>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.

]]>A teaser trailer for Haystack Situations:

Prints will be available soon, stay tuned for further announcements.

Download options:

1080p 720p DVD WebM MP4 Ogg 52 MB 23 MB 21 MB 5.7 MB 5.7 MB 5.9 MB

The maths behind it involves icosahedral rotational symmetry and stereographic projection.

]]>In a previous post I wrote about continuous escape time for iterated function system fractals. Referring to the second case in the definition of E(x) about half way down that page,

\[ \begin{aligned} E(z_k) &= \frac{\log R - \log |z_k|}{\log |z_{k+1}| - \log |z_k|} \\ &= \log_{\frac{|z_{k+1}|}{|z_k|}} \frac{R}{|z_k|} \\ &\approx \log_{\rho^{-1}} \frac{R}{|z_k|} \\ z_k &\approx R \rho^{E(z_k)} \\ \rho &= \max \{ \text{scale factors of the } F_i \} \end{aligned}\]

Plugging this into the distance estimator for iteration towards \(\infty\) gives:

\[ d = \lim_{k \to \infty} \frac{|z_k| \log |z_k|}{ | \frac{d}{dz} z_k | } \]

where \(\frac{d}{dz} z_k\) is the product of the scale factors of all the \(F_i\) chosen when calculating the continuous escape time. The final piece of the puzzle is that we have \(z_k\) in terms of \(E(z_k)\), but we calculate \(E(z_0)\) - luckily what we want is just the fractional part of that - the other case in its definition just adds 1.

The fern at the top of this post was coloured with both continuous escape time (larger values darker brown) and distance estimate (lower values brighter white). Strictly speaking I cheated a bit and assumed all the transformations had uniform scaling, for affine transformations you might want to multiply Jacobian matrices instead of scale factors to account for the different scalings in different directions, with the distance estimator based on the final result.

]]>