Rendering Implicit Surfaces and Distance Fields: Sphere Tracing

News (August, 31): We are working on Scratchapixel 3.0 at the moment (current version of 2). The idea is to make the project open source by storing the content of the website on GitHub as Markdown files. In practice, that means you and the rest of the community will be able to edit the content of the pages if you want to contribute (typos and bug fixes, rewording sentences). You will also be able to contribute by translating pages to different languages if you want to. Then when we publish the site we will translate the Markdown files to HTML. That means new design as well.

That's what we are busy with right now and why there won't be a lot of updates in the weeks to come. More news about SaP 3.0 soon.

We are looking for native Engxish (yes we know there's a typo here) speakers that will be willing to readproof a few lessons. If you are interested please get in touch on Discord, in the #scratchapixel3-0 channel. Also looking for at least one experienced full dev stack dev that would be willing to give us a hand with the next design.

Feel free to send us your requests, suggestions, etc. (on Discord) to help us improve the website.

And you can also donate). Donations go directly back into the development of the project. The more donation we get the more content you will get and the quicker we will be able to deliver it to you.

7 mns read.

We can mathematically combine and manipulate implicit surfaces in all sort of ways to create shapes that would be extremely hard to produce with more traditional modeling methods (especially polygons). Generally the method of assembling shapes to create more complex shapes is called constructive solid geometry or CSG. CSG is a method of what we call procedural modelling. You don't model shapes by moving vertices by hand, but by combining simple shapes using mathematics and automated processes.

Figure 1: union.

Figure 2: subtraction.

Figure 3: intersection.

Let's take a few basic examples. One of the most basic operations is to add two objects together something that is best described in mathematics with the union operator. You can achieve this effect by just returning the minimum distance to the two objects you wish to combine as shown in figure 1.

You can also subtract the volume of a shape from another, something that again in mathematics might be best described with a set difference operator (in plain English a subtraction). To do so, you first need to invert the sign of the distance estimator of the first shape. If you look at figure 2, you can see that the inside of the sphere now returns positive signed distance to the surface, and any point outside of the sphere will return a negative signed distance. Then take the maximum of the two distances. With this effect, you can create a hole in the second object that has the shape of the first object.

Finally our last basic example will consist of computing the surface resulting of the intersection of two surfaces. To do so (as illustrated in figure 3) just use the largest distance (using the max operator).

What these operations have in common is that that they use what we call boolean operators.

But these are just a few examples.

What you need to see and realise is that you can use pretty much any mathematical function you want on these distances, in order to create all sort of pretty cool effects. So literally anything you can imagine from using a cosine, an exponential function, or a modulo operator can be used ...

Since we just mentioned the modulo operator, take a moment to think about the type of effect you will get if you apply a modulo operator to the distance to an object? Something like distance % 10 for example. Well this will have for effect to duplicate the object in every direction to infinity. You operate on the object space. You can also deform that space to create all sort of effects like twisting and bending (using sine and cosine).

Figure 4: mixing a cube and a sphere.

You can combine all these effects but more importantly when it comes to CSG, you are not limited to two objects. You can for example compute the intersection of two objects and combine the resulting shape with a third object, etc. People working on solid geometry, call this a CSG tree. Computing or evaluating these trees efficiently is a topic of research.

The two effect will study in this chapter is morphing or mixing and blending. The idea of morphing (mixing) is to create a shape that is somewhere in between the first and the second shape. For example you can morph between a sphere and cube (figure 4). The process to do so is very simple, you just (linearly) mix the distances to the two objects:

float dist = distanceToA * (1 - mixValue) + distanceToB * mixValue

Where mixValue is a float contained in the range [0,1].

The effect of blending is more interesting. The idea is that when you compute the union between two objects you get sharp edges where the two objects intersect. If you want to create like a welding effect, this is not desirable. What you want is some sort of smooth transition between one surface to the other, in areas where the two surfaces are close to each other. Interesting enough many of the equations you will find in papers on implicit surfaces that relate to blending equation "just don't work". We don't know if these are just typos that kept being repeated from paper to paper but that's bad and frustrating. Let's give credit to Inigo Quilez for sharing on his website a few blending functions that just work. In the code below we implemented the exponential version. We might explain this function in a future revision of this lesson; for now we will only provide the code (see below the implementation chapter).


The code we provide to implement some of these ideas is rather basic and straightforward. Though from a programming standing note that we use a pattern called a functor in programming (this method is also a form of what we call generic programming). All the functions to operate on the two distances are actually defined into classes (or more exactly structure in this case). This allows us to eventually store within these classes member variables such as a blending factor as in the case of the blendFunc structure for instance. But more importantly because these are classes or structures, they define a type and as such they can be used in template class (note that the CSG class is a template and that the first template parameter is one of these structures in which we implement the function that operates on the distance via the () operator) . If you are unfamiliar with this patter, we recommend you to read about it on the web. This code also uses some nice features from the more advanced C++ standards such as the variadic arguments Args&& ... args and the using directive that can be used instead of typedef. Time to progress with your C++ coding skills!

struct unionFunc { float operator() (float a, float b) const { return std::min(a, b); } }; struct subtractFunc { float operator() (float a, float b) const { return std::max(-a, b); } }; struct intersectionFunc { float operator() (float a, float b) const { return std::max(a, b); } }; struct blendFunc { blendFunc(const float &_k) : k(_k) {} float operator() (float a, float b) const { float res = exp(-k * a) + exp(-k * b); return -log(std::max(0.0001f, res)) / k; } float k; }; struct mixFunc { mixFunc(const float &_t) : t(_t) {} float operator() (float a, float b) const { return a * (1 - t) + b * t; } float t; }; template<typename Op, typename ... Args> class CSG : public ImplicitShape { public: CSG( const std::shared_ptr<ImplicitShape> s1, const std::shared_ptr<ImplicitShape> s2, Args&& ... args) : op(std::forward<Args>(args) ...), shape1(s1), shape2(s2) {} float getDistance(const Vec3f& from) const { return op(shape1->getDistance(from), shape2->getDistance(from)); } Op op; const std::shared_ptr<ImplicitShape> shape1, shape2; }; using Union = CSG<unionFunc> using Subtract = CSG<subtractFunc> using Intersect = CSG<intersectionFunc> using Blend = CSG<blendFunc, float> using Mix = CSG<mixFunc, float> std::vector<std::shared_ptr<ImplicitShape>> makeScene() { std::vector<std::shared_ptr<ImplicitShape>> shapes; #if 0 shapes.push_back(std::make_shared<Plane>(Vec3f(0, 1, 0), Vec3f(0, -2, 0))); shapes.push_back(std::make_shared<Blend>( std::make_shared<Cube>(Vec3f(1.5)), std::make_shared<Torus>(2, 0.65), 5)); #elif 0 shapes.push_back(std::make_shared<Blend>( std::make_shared<Plane>(Vec3f(0, 1, 0), Vec3f(0, 0, 0)), std::make_shared<Torus>(2, 0.65), 5)); #else shapes.push_back(std::make_shared<Plane>(Vec3f(0, 1, 0), Vec3f(0, -2, 0))); shapes.push_back(std::make_shared<Mix>( std::make_shared<Cube>(Vec3f(1)), std::make_shared<Sphere>(Vec3f(0), 1), 0.5)); #endif return shapes; }