Intro

As Signed Distance Functions start making it into mainstream and commercial applications, it's important to find replacements or alternatives to common things artists used to do in polygon-land. One such thing is displacement as a means to enhance or add detail to a shape. While much of what I'll say here applies to any kind of displacement pattern or SDF based "detail mapping", I'll focus on pure fBM style displacement in this article, the one we use to make procedural terrains for example (you might know it as "Fractal Noise" too). And the reason to find an alternative to it is that the traditional way of constructing and applying fBM (and displacement/detail) does not work well with SDFs. But, I've found an alternative that I think it's competitive. So keep reading!

The problem

The problem at its core is this: the (regular, arithmetic) addition of two SDFs is not an SDF. Furthermore, the addition of an SDF and some other field/function (SDF or not) is also not an SDF (except for very carefully manufactured functions, that is). So, when adding a regular fBM or sine wave or any other displacement function to a "host" SDF, we don't get a valid SDF anymore.

That of course doesn't stop us from still trying, and we often do add arbitrary functions to our SDFs in the hopes of changing their shape. Sometimes that works for a while before it breaks. For example, when rendering SDFs through a raymarcher, adding a sine wave to a sphere can work for small amplitudes and frequencies, if the raymarcher happens to be designed to tolerate small deviations from true SDFs. However it will sooner or later breaks as we make the sine wave larger or its wavelength shorter (the technical reason being that the gradient of the resulting field will be much larger and smaller than 1.0).

But said, since the method does work to some degree for small displacements, it's still used for things that traditionally have been solved through displacement in polygon-land, such as the fractal terrains I mentioned in the intro; even though they don't work well or making them work has big performance penalties for the renderer.

This is unfortunate because fBM signals are very popular, well understood, widely implemented by all sort of modeling, texturing and painting software, and widely used by artists. It's really a pity we can't simply use them with SDFs in a reliable manner.

Or can we?

A solution

Well, we know addition of functions doesn't work well with SDFs, so let's try to workaround it by redefining addition, see if we can repurpose and save fBMs.

The most important aspect of the addition in an fBM or fractal process, is not the arithmetic aspect or computation per-se. What's important is that waves of different amplitudes and wavelengths are being additively combined together, meaning, they sit on top of each other. That corresponds to how shapes in nature organize themselves too (hence the success of fBM in procedural modeling and texturing). So what we need is to define a new "addition" of SDFs that allows us to combine shapes together and grow them on top each other.

The "combining" part of our requirement is easy and we can do it with a simple union or smooth-union operation. That would be a min() or smoothmin() probably. In other words, as we generate SDFs of shorter and smaller amplitudes and wavelength, we can combine them with the "host" shape and to each other through the regular SDF union operations.

The "on top of each other" part of the addition can be achieved by making sure that these SDF layers (called "octaves" traditionally in standard/vanilla fBM implementations) only exist in the vicinity of the previous layer (or "host" object that we are applying our fBM displacement to). That way only the surface of our object will be augmented with the higher frequency detail, and no new surfaces will be created elsewhere.

One easy way to accomplish this is by clipping the SDFs layers against a slightly inflated version of the "host" SDF, ideally through a smooth-intersection to keep the smoothness of the final shape. Depending on the shape of the SDF layers we can still have flyovers (disconnected pieces of surface), so this is not a bullet proof method, but it works well in practice.

An implementation

So, let's put all these ideas together. First we need a random and smooth SDF to use as base function for our fBM. Since traditional 3D noise() is not a distance function (it's an Signed Field/Function, but doesn't measure distances), we cannot use it. Instead, we'll use an infinite but simple grid of spheres of random sizes. Spheres have simple SDFs, are isotropic and so they feel like natural candidates. Making an infinite grid of them is easy as well with some basic domain repetition. If we restrict the radius of our random spheres to be smaller than half the edge-length of the grid, then for a given point in space we only need to evaluate the SDF of the 8 spheres at the corners of the grid cell the point belongs to.

The code below is a possible implementation of such sdBase(), and to the right is a direct rendering of it:

float sph( ivec3 i, vec3 f, ivec3 c ) { // random radius at grid vertex i+c float rad = 0.5*hash(i+c); // distance to sphere at grid vertex i+c return length(f-vec3(c)) - rad; } float sdBase( vec3 p ) { ivec3 i = ivec3(floor(p)); vec3 f = fract(p); // distace to the 8 corners spheres return min(min(min(sph(i,f,ivec3(0,0,0)), sph(i,f,ivec3(0,0,1))), min(sph(i,f,ivec3(0,1,0)), sph(i,f,ivec3(0,1,1)))), min(min(sph(i,f,ivec3(1,0,0)), sph(i,f,ivec3(1,0,1))), min(sph(i,f,ivec3(1,1,0)), sph(i,f,ivec3(1,1,1))))); }