Fractals are some of the most mesmerizing visuals in computer graphics, but rendering them in 3D space requires
special techniques beyond standard polygonal rendering. This article will take you into the world of ray marching,
where we’ll use distance fields, lighting, and soft shadows to render a Mandelbulb fractal — one of the most famous 3D
fractals.
By the end of this post, you’ll understand:
The basics of ray marching and signed distance functions (SDFs).
How to render 3D objects without polygons.
How to implement Phong shading for realistic lighting.
How to compute soft shadows for better depth.
How to animate a rotating Mandelbulb fractal.
This article will build on the knowledge that we established in the Basics of Shader Programming
article that we put together earlier. If you haven’t read through that one, it’ll be worth taking a look at.
What is Ray Marching?
Ray marching is a distance-based rendering technique that is closely related to ray tracing.
However, instead of tracing rays until they hit exact geometry (like in traditional ray tracing), ray marching steps
along the ray incrementally using distance fields.
Each pixel on the screen sends out a ray into 3D space. We then march forward along the ray, using a signed
distance function (SDF) to tell us how far we are from the nearest object. This lets us render smooth implicit
surfaces like fractals and organic shapes.
Our first SDF
The simplest 3D object we can render using ray marching is a sphere. We define its shape using a
signed distance function (SDF):
The sdfSphere() function returns the shortest distance from any point in space to the sphere’s surface.
We can now step along that ray until we reach the sphere. We do this by integrating our sfpSphere() function into our
mainImage() function:
First of all here, we convert the co-ordinate that we’re processing into screen co-ordinates:
This uv value is now used to in the calculations to form our ray equation:
We now iterate (march) down the ray to a maximum of maxSteps (currently set to 100) to determine if the ray
intersects with our sphere (via sdfSphere).
Finally, we render the colour of our sphere if the distance is within tolerance; otherwise we consider this part of
the background:
You should see something similar to this:
Adding Lights
In order to make this sphere look a little more 3D, we can light it. In order to light any object, we need to be able
to compute surface normals. We do that via a function like this:
We make decisions about the actual colour via a lighting function. This lighting function is informed by the surface
normals that it computes:
We can now integrate this back into our scene in the mainImage function. Rather than just making a static colour
return when we establish a hit point, we start to execute the lighting function towards the end of the function:
You should see something similar to this:
Mandelbulbs
We can now upgrade our rendering to use something a little more complex then our sphere.
SDF
The Mandelbulb is a 3D fractal inspired by the 2D Mandelbrot Set. Instead of working in 2D complex numbers, it uses
spherical coordinates in 3D space.
The core formula: \(z→zn+c\) is extended to 3D using spherical math.
Instead of a sphere SDF, we’ll use an iterative function to compute distances to the fractal surface.
This function iterates over complex numbers in 3D space to compute the Mandelbulb structure.
Raymarching the Mandelbulb
Now, we can take a look at what this produces. We use our newly created SDF to get our hit point. We’ll use this
distance value as well to establish different colours.
You should see something similar to this:
Rotation
We can’t see much with how this object is oriented. By adding some basic animation, we can start to look at the complexities
of how this object is put together. We use the global iTime variable here to establish movement:
You should see something similar to this:
Lights
In order to make our fractal look 3D, we need to be able to compute our surface normals. We’ll be using the
mandelbulbSDF function above to accomplish this:
We now use this function to light our object using phong shading:
Soft Shadows
To make the fractal look more realistic, we’ll implement soft shadows. This will really enhance how this object looks.
Pulling it all together
We can now pull all of these enhancements together with our main image function:
Finally, you should see something similar to this: