Rendering Escape Fractals in Three.js

April Walker
7 min readMar 9, 2020

--

Fractals are a class of shapes with very unusual properties. They’re typically described as being “self-similar,” in the sense that as you magnify the surface of a fractal you may find patterns repeating themselves. A well-known example is the Koch Flake shown below.

It looks a lot like the flake is built up by smaller version of itself, and that intuition turns out to be true. The snowflake can be built iteratively from stacking triangles on each others’ sides, and the fractal itself is the hypothetical endpoint if this process is repeated infinitely many times. If you were to follow the logic of its construction closely, it turns out that the area of the flake stays finite while the perimeter steadily increases towards infinity — this is what makes fractals mathematically interesting.

While fractals are created iteratively, not all of them can be created by a process as simple as the Koch flake. This means that there are fractals which can’t be created by copy-pasting the same shape onto itself in predictable ways.

A Pythagoras tree is another type of fractal built iteratively like the Koch snowflake. Credit: Juan Carlos Ponce Campuzano

This suggests that none of these fractals are self-similar; since without a copy-paste process there’s nothing there to enforce self-similarity. Unexpectedly, it turns out that this intuition is wrong. There is a class of fractals which cannot be built by a simple copy-pasting process, but which somehow still show self-similarity. The most famous example is the Mandelbrot set shown below.

The Mandelbrot set. Points on the complex plane that belong to it are colored black.

There is a very clear repeating bulb, so why can’t this be constructed iteratively like the koch flake? The problem comes from how much hidden complexity there is.

The Mandelbrot set upon zooming in.

The Mandelbrot set belongs to a class of fractals known as escape fractals. The name hints at the definition. As an escape fractal, the Mandelbrot set consists of all complex numbers (z_0) that stay bounded after infinitely many applications of this recurrence relation:

That is, z_n well never increase towards infinity. While it’s possible to define the Mandelbrot set, it’s impossible to fully calculate. That is, given an arbitrary complex number there’s no guaranteed way to know whether it belongs to the Mandelbrot set or not. This doesn’t mean we don’t know any points in the set, however. An example of a point we know for certain is in the Mandelbrot set is the number -2, in fact every number between -2 and 0 are in the set.

Despite this limitation, we can program algorithms to generate approximations of it as above. This is where “escape” comes from. Since z_0 is a complex number, we can prove that the recurrence relation above will only grow when the magnitude of z_0 is greater than 2. This gives us a method of proving that some points on the complex plane aren’t in the set. If a point does diverge, then it must do so in a finite number of iterations. As soon as there’s a z_n with a magnitude greater than 2, we know that it’s going to increase indefinitely and “escape” from the set.

After a given number of iterations, the points that escape are known for certain not to be in the set, while the remaining ones are likely but not guaranteed to be members. This gives a way of narrowing down the set by increasing the number of iterations of the recurrence relation above, but because the process of narrowing down the set can be continued forever, we can never fully calculate the set. Here’s an animated version of this process.

Here I use a trick that makes escape fractals look especially interesting. Points on the complex plane are colored according to how many iterations it took for them to reach the escape threshold. This is better that all-or-nothing rendering like in the images above, which turns out grainy. Programming this in GLSL is relatively straightforward:

float mandelbrot(vec2 c){
float alpha = 1.0;
vec2 z = vec2(0.0 , 0.0);
for(int i=0; i < 200; i++){ // 200 iterations float x_sq = z.x*z.x;
float y_sq = z.y*z.y;
vec2 z_sq = vec2(x_sq - y_sq, 2.0*z.x*z.y);
z = z_sq + c; if(x_sq + y_sq > 4.0){
alpha = float(i)/200.0; // should be same as max iterations
break;
}
}
return alpha; // between 0.0 and 1.0
}

Alpha can then be fed into a colormap. Adding a shader is pretty easy in Three.js. Here’s boilerplate code that will render a plane, place the camera right in front of it, and display a GLSL fragment shader material on its surface.

var camera, scene, renderer;
var geometry, material, mesh;
var uniforms;
var aspect = window.innerWidth / window.innerHeight;function init() {
setup();
uniforms = { //GLSL types only
res: {type: 'vec2', value: new THREE.Vector2(window.innerWidth, window.innerHeight)},
};
geometry = new THREE.PlaneBufferGeometry(2, 2);
material = new THREE.ShaderMaterial({
fragmentShader: fragmentShader(), // can also just be a string
uniforms: uniforms
});
mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
animate();
}
// shader =====================================function fragmentShader(){
return '
uniform vec2 res;
void main(){ // gl_FragCoord in [0,1]
vec2 uv = gl_FragCoord.xy / res;

gl_FragColor = vec4(uv.x, uv.y, uv.y, 1.0);
}
'
}
// setup ======================================function animate(){ // loops indefinitely to animate
renderer.render(scene, camera);
requestAnimationFrame(animate);
}
function setup(){
camera = new THREE.OrthographicCamera( -1, 1, 1, -1, -1, 1);
scene = new THREE.Scene(); renderer = new THREE.WebGLRenderer( { antialias: false, precision:'highp' } );
renderer.setSize( window.innerWidth, window.innerHeight );
document.body.appendChild( renderer.domElement );
}
init();

GLSL shaders in Three.js work similarly to Shadertoy, with the reserved GLSL keywords prefixed with gl_ and gl_FragCoord is vec3 in Three.js but vec2 in Shadertoy. Additionally, three.js allows us to pass information from our script to the shader by using uniforms. In this case only res is used to normalize the fragment coordinate.

Since we’re interested in rendering the Mandelbrot set, we can plug our mandelbrot shader into fragmentShader().

function fragmentShader(){
return `
precision highp float;
uniform vec2 res;
uniform float aspect;
float mandelbrot(vec2 c){
float alpha = 1.0;
vec2 z = vec2(0.0 , 0.0);
for(int i=0; i < 200; i++){ // i < max iterations float x_sq = z.x*z.x;
float y_sq = z.y*z.y;
vec2 z_sq = vec2(x_sq - y_sq, 2.0*z.x*z.y);
z = z_sq + c; if(x_sq + y_sq > 4.0){
alpha = float(i)/200.0;
break;
}
}
return alpha;
}
void main(){ // gl_FragCoord in [0,1]
vec2 uv = 4.0 * vec2(aspect, 1.0) * gl_FragCoord.xy / res -2.0*vec2(aspect, 1.0);
float s = 1.0 - mandelbrot(uv);
vec3 coord = vec3(s, s, s);
gl_FragColor = vec4(pow(coord, vec3(7.0, 8.0, 5.0)), 1.0);
}
`
]

Aspect is used to ensure that the escape fractal isn’t stretched when the screen size varies. We can make the appropriate modification to uniforms to accommodate aspect:

uniforms = {
res: {type: 'vec2', value: new THREE.Vector2(window.innerWidth, window.innerHeight)},
aspect: {type: 'float', value: aspect}
};

This is necessary when the viewing window gets resized. If everything went well, you will see this:

This is a good jumping point for exploring more exotic escape fractals. I’ve made an interactive escape fractal viewer with adjustable parameters based on the following recurrence relation.

This may change slightly, see the source code

When a=1 and everything else is 0, this reduces to the recurrence relation for the Mandelbrot set. With this recurrence relation and related ones, you can find some pretty exotic fractals. Below is an image gallery of some of the interesting ones I’ve found so far.

The source code is here for you to modify. From the code above, I added some event listeners and a simple GUI which allows you to change the fractal’s parameters in real time.

I’m interested in visualizing mathematics.
Follow my blog: https://lostvalera.wordpress.com/
Follow me on Twitter: https://twitter.com/SereneBiologist

--

--