When modelling with distance estimation functions you sometimes require a limited amount of repetition of an object. It’s possible to code this up using branching functions, but when using a GPU to do raytracing branches are expensive. In this article I’ll quickly go through the derivation of a fast fixed repeat function for us in GLSL or the HLSL of your choice.

First up let’s look at a simple distance function.

float sphere(vec3 pos, vec3 radius) { return length(pos) - radius; } float de(vec3 p) { return sphere(pos, 1.0); }

This gives us a sphere of radius 1 centred at the origin…

To repeat an object in a distance estimation function we simply need to code some kind of modulus operator on the position to instantiate the object multiple times.

Our sample code becomes

float repeat(vec3 pos, vec3 repeat) { return mod(pos, repeat) - 0.5 * repeat; } float de(vec3 pos) { return sphere(repeat(pos, 1), 3); }

The only problem with this is that the rendering becomes very dark if you’re using shadows. To show you why I’ve turned shadows off and you can see that we now have an infinite number of spheres in all directions.

What about if we only want spheres repeated in the xz plane? In that case we need to modify our repeat function to only use the modulus operation on those two axis.

Now our code becomes

float repeatxz(vec3 pos, vec3 repeat) { vec3 q = mod(pos, repeat) - 0.5 * repeat; return vec3(q.x, pos.y, q.z); }

Note that on the GPU it’s quicker to do a modulus on all three components of the vector, and then selectively return those elements, than do the modulus on only two components.

We now get a field of spheres stretching out to the horizon.

What about if we only want to repeat the spheres within a certain sized area? In this case we’re going to need to change our distance function so that it only repeats within that range.

Let’s hop into pseudo code and assume we only want repetition between -10 and 10, we’ll also only do a single axis.

float repeatfixed(float pos, float repeat) { if (pos<-10) return pos + 10; else if (pos>=-10 && pos<10) return mod(pos, repeat) - 0.5 * repeat; else if (pos>=10) return pos - 10; }

The problem with this code is that we’re going to end up with branch instructions. On the GPU branching is proper evil so we’d like to find a way to remove them. A normal trick to do that is to use min() and max() operators.

First up lets look at where pos is <-10. For our new function we want the three conditional branches above to be added together over the full range. That means that for pos<-10 the output function should return pos+10 for all values <-10, and 0 for anything above. It turns out that we can do this using a min() instruction like this...

return min(pos, -10) + 10;

Likewise we can do the +10 bit using a similar max() function

return max(pos, 10) - 10;

And now we just have to find a way to do the middle clause, the repetition itself, so that it returns zero outside the range -10 and 10.

Let’s start by taking our position and clamping it inside the range. To do this we use a min and a max function like this.

return max(min(pos, 10), -10);

We now pass that value into the repeat function to get this…

return mod(max(min(pos, 10), -10), repeat) - 0.5 * repeat;

And now if we put it all together we get the following simple function, with no branches.

float repeatfixed(float pos) { return min(pos, -10) + 10 + mod(max(min(pos, 10), -10), repeat) - 0.5 * repeat; + max(pos, 10) - 10; }

This function also remains branchless if we extend it to work on a vec3 operator, which gives us the final repeatxzfixed() function below.

vec3 repeatxzfixed(vec3 pos, vec3 repeat, vec3 limit) { vec3 q = min(-limit, pos) + limit + mod(max(min(pos, dist), -dist), repeat) - 0.5 * repeat + max(pos, limit) - limit; return vec3(q.x, pos.y, q.z); } float de(vec3 pos) { return sphere(repeatxzfixed(pos, 1), vec3(3), vec3(10)); }

And now we get a limited number of repeats as we wanted.

You can probably also see that we don’t need an xz variant of this function anymore as we can just put the limit at the right position to prevent repetitions in the y axis. I’ll leave that as an exercise for the reader.