DEV Community

Cover image for  Ray marching part 2: DE operations and CSG
Rami-Slicer
Rami-Slicer

Posted on • Updated on

Ray marching part 2: DE operations and CSG

In my previous post about ray marching I talked about what ray marching is and how you can do a basic implementation of it in GLSL. If you haven't read it yet, you will want to at least skim through it before following along to this one since we will be reusing the code there:

In this part, we will be adding more distance estimators, some operations to manipulate these distance estimators, and learn how to combine DEs into more complex scenes.

Without further ado, let's start!

Let's start simple by learning how to combine shapes. As you may know, there are 3 basic boolean operations: Union, difference, and intersection.
The three basic boolean operations, union, difference, and intersection (or intersrction according to this image)

To take advantage of these though, we need a way to add multiple objects. For this we can create a scene DE that allows us to use more than one DE. First we make a function for it, it'll just have a sphere for now:

// IMPORTANT: In Shadertoy this MUST be put after both the DEs and the operators. idk if it's the same for normal OpenHL
float sceneDE(vec3 pos) {
    return sphereDE(pos, vec3(0,0,0), 1.0);
}

Then find this line in the march() function (you may have used different variable names):

float _distance = sphereDE(current_pos, vec3(0.0), 1.0);

And change it so it uses sceneDE():

float _distance = sceneDE(current_pos);

Now we are ready to create the boolean functions.

Union is the simplest so let's start that. We'll create a new function, opUnion:

// a and b are interchangeable
float opUnion(float a, float b) {
    return min(a,b);
}

If you look closely, you can see that this function is basically just min(). The only reason we are defining this function is so the code is more understandable.
Now we can try it out by adding it to sceneDE()!

float sceneDE(vec3 pos) {
    return opUnion(
        sphereDE(pos, vec3(0,1,1), 1.0),
        sphereDE(pos, vec3(0,0,0), 1.0)
    );
}

If you did it correctly you should see two spheres right next to each other!

Let's try adding a difference operator next. This time we get to use the max() function:

// a and b are NOT interchangeable
float opDiff(float a, float b) {
    return max(-a, b);
}

Adjust sceneDE() to use the difference operator:

float sceneDE(vec3 pos) {
    return opDiff(
        sphereDE(pos, vec3(0,-1,-0.5), 1.0),
        sphereDE(pos, vec3(0,0,0), 1.0)
    );
}

Finally, we can add a intersection operator. It's incredibly simple, just like the union:

// a and b are interchangeable
float opIntersect(float a, float b) {
    return max(a,b);
}

And use it:

float sceneDE(vec3 pos) {
    return opIntersect(
        sphereDE(pos, vec3(0,-1,-0.5), 1.0),
        sphereDE(pos, vec3(0,0,0), 1.0)
    );
}

Now we have the three basic boolean operators! Here's a shader showcasing all three of them.

So far we've used nothing but spheres to create our scenes. I think it's about time we brought in another DE to spice things up. If you look at this page (my go-to reference for raymarching) the next DE after a sphere is the DE for a box. However, there's also another one for a box but with optional rounded edges. For flexibility's sake we'll use that one:

// Original from here: https://iquilezles.org/www/articles/distfunctions/distfunctions.htm
float roundBoxDE( vec3 p, vec3 b, float r) {
    vec3 q = abs(p) - b;
    return length(max(q,0.0)) + min(max(q.x,max(q.y,q.z)),0.0) - r;
}

Since this one doesn't have translation built in, we'll make a translation operation:

vec3 opTranslate(vec3 pos, vec3 moves) {
    return pos - moves;
}

Alright! Now we can use all of these in the scene DE:

float sceneDE(vec3 pos) {
    float u = opUnion(
        sphereDE(pos, vec3(-2.5,-1,-0.5), 1.0),
        roundBoxDE(opTranslate(pos, vec3(-2.5, -1.0, -1.5)), vec3(0.75,0.75,0.75), 0.0)
    );
    float d = opDiff(
        sphereDE(pos, vec3(0,-1,-0.5), 1.0),
        roundBoxDE(opTranslate(pos, vec3(0.0, 0.0, 0.0)), vec3(1.0,1.0,1.0), 0.0)
    );
    float i = opIntersect(
        sphereDE(pos, vec3(2.5,-1,-0.5), 1.0),
        roundBoxDE(opTranslate(pos, vec3(3.0, -0.0, 0.7)), vec3(1.0,1.0,1.0), 0.0)
    );
    return min(u,min(d,i));
}

Here's what it should look like!
Again, my fingers are getting super tired and I have other things I need to do, so I'm going to leave it off here.

Top comments (0)