Julia Set Visualization with WebGL for Elm

Creating beautiful fractal animations with Elm

Introduction

Fractals always have been beautiful to me. With WebGL for Elm we have the possibility to visualize fractals in our browser in a functional and statically typed language. Because WebGL shaders run on the GPU, the visualization of Julia sets is very performant. If you are not familiar with the basic WebGL concepts, have a look at

In the following we are working with a quadratic mesh M which consists of two triangles with the vertices (-1,-1), (-1,1), (1,1) and (-1,-1), (1,1), (1,-1). We begin by explaining how to visualize Julia sets on M.

Julia Set Visualization

Julia sets are generated by certain complex-valued functions. In this article we will focus on the quadratic mapping

The Julia set of f on our mesh M is the boundary of the set of points for which the series

does not tend to infinity. For the visualization algorithmus this means we have to specify a number N of maximum iterations for (1) and a strategy to decide if the series (1) diverges for a point z in our mesh. One possibility to decide on divergence is the assumption that (1) tends to infinity as soon as

for some nN and positive C. The color of each pixel is then determined by the following steps:

  1. Pick a number N and a constant C.
  2. For every pixel in our mesh we compute (1) until we reach N or condition (2) is satisfied.
  3. If we reach N, the pixel is colored in black. If (2) is satisfied for some nN, the pixel is rendered in a color dependent on n (we will use WebGL textures for that reason later on).

Elm implementation

A live demo can be found at elm-julia-set-visualization.herokuapp.com and the code is available at github.com/ninjaconcept/elm-julia-set-visualization.

Let us add some explanations on parts of the code. Our mesh is generated by two triangles

type alias Vertex =
{ position : Vec2 }
mesh : Mesh Vertex
mesh =
WebGL.triangles
[ ( (Vertex (vec2 -1 -1))
, (Vertex (vec2 -1 1))
, (Vertex (vec2 1 1))
)
, ( (Vertex (vec2 1 -1))
, (Vertex (vec2 -1 -1))
, (Vertex (vec2 1 1))
)
]

storing only the position of each vertex. AnimationFrame is used to render different Julia sets by varying the parameter c of the function f defined in the previous section. Here, we use the elapsed time to vary the real part cX and the imaginary part cY of c between -1 and 1.

type Msg
= Animate Float | ...
subscriptions : Model -> Sub Msg
subscriptions model =
AnimationFrame.diffs Animate
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
Animate value ->
let
timeElapsed = model.timeElapsed + value
                cX =
sin (0.0006 * (getX model.c + timeElapsed))
                cY =
sin (0.0002 * (getY model.c + timeElapsed))
in
{ model | c = vec2 cX cY, timeElapsed = timeElapsed } ! []

The interesting part happens in the fragment shader. Here we need to implement the steps described in the previous section. We choose N = 80 as the number of maximum iterations and C = 800 as upper bound. The model parameter c (see above) is also declared as uniform variable for further usage in the fragment shader. Additionally, we provide a texture as well as the width and height of our view. The texture is used to map the number of iterations we need to satisfy (2) (or the maximum number of iterations) on a color within the texture. The fragment shader reads as follows

fragmentShader : Shader {} Uniforms {}
fragmentShader =
[glsl|
precision mediump float;
uniform vec2 c;
uniform sampler2D texture;
uniform int screenWidth;
uniform int screenHeight;
      const int max_iterations = 80;
      vec2 complex_square(vec2 v) {
return vec2(
v.x * v.x - v.y * v.y,
v.x * v.y * 2.0
);
}
      vec2 julia_function(vec2 z, vec2 c) {
return c + complex_square(z);
}
      float iteration_count_to_texture_position(int count) {
if(count == max_iterations) {
return 0.0;
} else {
return float(count)/float(max_iterations);
}
}
      void main() {
vec2 z = 3.5*vec2((gl_FragCoord.x - 0.5*float(screenWidth))/float(screenWidth), (gl_FragCoord.y - 0.5*float(screenHeight))/float(screenHeight));
        int count = max_iterations;
        for(int i = 0 ; i < max_iterations; i++)  {
z = julia_function(z, c);
          if(dot(z, z) > 800.0) {
count = i;
break;
}
}
        gl_FragColor = texture2D(texture,
vec2(iteration_count_to_texture_position(count), 0.0));
}
|]

Conclusion

It was fun working with WebGL for Elm! I’m looking forward to learning more about WebGL and using the acquired knowledge for further posts.