For this assignment, I decided to modify my ray tracer from homework 5 and turn it into a ray marcher. Ray marchers work similarly to ray tracers, except that instead of trying to calculate the intersection of a geometry directly, the ray is advanced forward incrementally until the object is hit. Generally, this is used for volumetric rendering and 3D fractals due to complex or undefinable geometry. As I am prepared to show, ray marching offers many advantages over regular ray tracing because it offers tons of extra features for "free".
Suppose we want to render a simple sphere. A naive way to do ray marching would be to simply choose a small delta and for each ray from the camera, advance the ray by this small amount until it crosses over into the "inside of the sphere". Here, we can define a Boolean "inside" function as simply (r^2 < 1). The problem with this method of course is that it would take many many steps to advance the ray, and when it finally reached the object, there isn't a clean way to find a surface normal, so we need to do something smarter.
There is a simple way to solve both problems, and it is a technique called Distance Estimation. The procedure works as follows: For a given ray in the ray marcher, computer the closest distance to the geometry from the ray point. Once this distance is known, we can advance the ray that amount safely without overshooting geometry. This is repeated until the distance to the geometry is smaller than a threshold. Call this distance estimator function DE(p). This function is defined for every point and is continuous, positive outside the object, negative inside, and zero on the surface. But how can we find the surface normal for the intersection? Multivariable calculus reminds us that to find the normal of a scalar field, you simply take its gradient (a 3 dimensional derivative). This is easily done numerically for each component and produces nearly perfect normals.
We showed how we could use a distance estimator function to quickly ray march and get nice normals, but how do we find these functions? For a sphere it's simply DE(p) = length(P) - length(R), and other primitives are fairly simple as well. But what about rendering multiple primitives in a single scene? The procedure in that case is to simply find a DE for each object, evaluate them at the given point, and then take the minimum of those distances. Unfortunately, this is linear with respect to the number of primitives in the scene and doesn't take advantage of the acceleration models of regular ray tracing. But not all hope is lost! When the geometry is very regular, amazing things begin to happen.
Suppose we were to take our point and find the modulo of the x and y components prior to calculating the DE. Suddenly, a truly infinite amount of geometry can be rendered at the same computational cost of just the single object! You get an infinite field of spheres!
Besides simple geometry like the spheres, it is also possible to find a distance estimate to more complicated fractals. It may be surprising that a DE could exist at all for these structures, but the infinite complexity of fractals is derived from extremely simple rules, so it should at least be believable that such functions exist and are easily computable. The functions usually involve conditional folding, which maps points from one part of space to another. For instance, flipping always to the same side of a plane or the inside of a sphere. The recursive triangle below is made with three planar folds and a uniform scale by 2.
The figure below is part of the Mandelbox, a fractal made with both cubic and spherical folding.
Now, I will show some of the cool effects that can be done "for free" by ray marching.
One cool thing we can do is add glow to our objects. Since we are marching past some geometry, we can keep track of the closest point the ray ever got to the object, so even if it doesn't hit, we can assign it a color based on how close it got. This allows objects to emit a glow around them.
Ambient Occlusion is a lighting model where instead of coloring ambient light as a solid color, it is colored brighter the less obstructed the surface is. Surfaces in nooks and crannies therefore appear darker and surfaces with lots of view to the sky appear brighter. This effect can can be approximated for free by simply taking into consideration the number of steps the marcher took before it finally hit an object. The more steps the ray took before the collision, the more occluded it is likely to be given the nature of a DE. The images below show the the difference between regular ambient (left) and ambient occlusion (right). As you can see, this lighting reveals more details and is more realistic and similar to global illumination.
Fog is trivial to compute. Each pixel gets faded to a particular fog color depending on the distance of the ray to the object. This can be done easily with regular ray tracing as well.
Depth of Field
Depth of field is an effect seen in cameras where objects in the focal plane appear in focus and objects too close or too far appear blurry. The way to emulate this is for each pixel, instead of shooting several rays in the same direction and averaging them, spread them around a small circle on the camera plane and send them out such that they would converge on the focal plane. This allows depth of field to take the place of oversampling as it does the two things at once.
Finally, animation is something easy to do with these fractals. One way is to tune some of the parameters and animate the resulting changes that the resulting fractal set makes. Another way is to zoom around and see more of the detail of the fractal itself. For the latter, I have produced the following video. The longest scene was rendered overnight over several hours. Enjoy!
In conclusion, ray marching is a nice way to create some really nice looking eye candy for 3d viewers using regular patterns and fractals. there are still improvements I can make to speed up the processing such as moving the bulk of the calculations to the GPU. I also was trying to get some cool coloring effects but couldn't find one that was inspiring enough. If you would like some source code, it is available here.