As an excuse to play with WebGL I decided to finally got around to playing with the Mandelbrot and Julia set. There’s a live demo at the end of the post, the zoom control/mouse movement needs some work but the basic idea works great.

### Links

A couple of useful places I used as reference :

Ozone3D – The Mandelbrot Set: Colors of Infinity

Fractals: Useful Beauty

dat.GUI – A great JavaScript GUI

### The Shader Code

##### Vertex Shader

Really simple, all we need is a fullscreen quad with smooth UV’s.

attribute vec3 vtx_pos; attribute vec2 vtx_uv; varying vec2 texcoord; void main(void) { texcoord = vtx_uv; gl_Position = vec4(vtx_pos, 1.0); }

##### Mandelbrot Set Fragment Shader

This one is very similar to the code in Ozone3D’s tutorial, I basically copied it for the most part (there’s only so many ways to skin a cat anyway).

The most notable aspect is **MAX_ITER**, which I replace with the actual iteration value before I compile the shader code. This was to get around the limitation of most webgl implementations which disallow you from using non-constant loop conditions, as they will be unrolled. This isn’t very convenient for real time display of any possible iteration size, so I simply recompile the shader when the iteration size changes.

Note that the “*uniform vec2 c*” uniform isn’t used here, it’s just there to keep the uniform layout identical for both shaders (I’m lazy and it’s just a demo).

Finally, I use a 1D texture (*passed in through a 2D sampler, but the actual texture is only 1 pixel tall*) to lookup the expected colour based on the iteration level.

#define iterations MAX_ITER precision highp float; varying vec2 texcoord; uniform sampler2D gradientSampler; uniform vec2 center; uniform float zoom; uniform vec2 c; void main(void) { // Mandelbrot vec2 C = vec2(1.0, 0.75) * (texcoord - vec2(0.5, 0.5)) * vec2(zoom, zoom) - center; vec2 z = C; gl_FragColor = vec4(0,0,0,1); for(float i = 0.0; i < float(iterations); i += 1.0) { z = vec2( z.x*z.x - z.y*z.y, 2.0 * z.x * z.y) + C; if(dot(z,z) > 4.0) { gl_FragColor = texture2D(gradientSampler, vec2(i / float(iterations), 0.5)); break; } } }

##### Julia Set Fragment Shader

The base code is identical to the Mandelbrot shader, except now we can pass in the desired complex parameter through c.

I can’t explain the maths as well as the pages I referenced earlier; but the main change is now the smooth UVs of the fullscreen quad are now assigned to Z instead of C, as the complex parameter is now a user defined constant and Z varies.

#define iterations MAX_ITER precision highp float; varying vec2 texcoord; uniform sampler2D gradientSampler; uniform vec2 center; uniform float zoom; uniform vec2 c; void main(void) { // Julia vec2 z = vec2(1.0, 0.75) * (texcoord - vec2(0.5, 0.5)) * vec2(zoom, zoom) - center; gl_FragColor = vec4(0,0,0,1); for(float i = 0.0; i < float(iterations); i += 1.0) { z = vec2( z.x*z.x - z.y*z.y, 2.0 * z.x * z.y) + c; if(dot(z,z) > 4.0) { gl_FragColor = texture2D(gradientSampler, vec2(i / float(iterations), 0.5)); break; } } }

### The Live Demo (full page demo)

- Toggle between the Mandelbrot/Julia set.
- Use the zoom control to zoom in (this isn’t ideal).
- Expect slow down on high iterations.
- If you want to tinker around with the code then I apologise for how messy it is, it’s my very first adventure into JavaScript and I kept bolting more stuff onto it until I ended up with this.

### ToDo

The Ozone3D page mentions that “a better approach is to use a multipass algorithm.”, which is next in line for this project.

It would also be cool to choose a complex parameter for the Julia set by clicking on the Mandelbrot itself, as both sets are connected.