Shader Programming: Massively parallel art [Episode 10: Level up the geometry]

In today's episode we're going to make our geometry more interesting.

This is the final episode of the Shader Programming series, at least for now! I'm only wrapping it up because I want to move onto something much more relevant to Steem. Don't despair if you've been enjoying this series; I still plan to make occasional shader posts :)

PART 10: Better Geometry

I'm fairly happy with the core of our raymarching renderer at this point. It can calculate surface normals for any scene we give it, and can apply diffuse and specular lighting.

What we need to do now is feed the renderer a more interesting scene!

Today's session is going to focus almost exclusively on our GetSceneDistance() function, and the magic we can perform therein.

GetSceneDistance(): A recap

Remember from back in Episode 7 that the sole job of GetSceneDistance() is to accept a 3D coordinate in worldspace, and return a float representing the distance to the closest part of the scene.

This allows our raymarching loop to step through the world until it collides with something. If you don't remember how that works, do check out Episode 7 again to refresh yourself.

So, our entire scene is modelled inside GetSceneDistance(), and it's here that we can add extra geometry.

Here's what it looks like at the moment:

// Scene distance function
float GetSceneDistance(vec3 p) {
    // Define sphere position and radius
    vec3 ballPos = vec3(0,0,1);
    float ballRadius = 1.;

    return distance(p, ballPos) - ballRadius;
}

Instance of calculating the distance from p to the sphere here in GetSceneDistance(), let's abstract that out into a Sphere() function.

// Sphere primitive
float Sphere(vec3 p, vec3 worldPos, float radius) {
    return distance(p, worldPos) - radius; 
}

// Scene distance function
float GetSceneDistance(vec3 p) {
    return Sphere(p, vec3(0,0,1), 1.);
}

That gives us a parameterised Sphere primitive for which we can specify a world position and radius.

Multiple objects at once

So far our rendering has been limited to a single scene object. What if you wanted to render two spheres at different world positions?

Well, remember again what the job of GetSceneDistance() is; to return the distance to the closest part of the scene at any point.

If our individual Sphere primitive returns a distance to a sphere, all we need to do is call it multiple times for each sphere, and take the minimum of the two distances as the distance to the overall scene. GLSL includes a handy min() function that returns the minimum of two floats.

// Scene distance function
float GetSceneDistance(vec3 p) {
    float d = Sphere(p, vec3(-.3,0,.5), .5); // First sphere
    d = min(d, Sphere(p, vec3(.5,.25,1.2), .5)); // Second sphere
    return d; // Return the shortest distance found
}

two spheres.PNG

Box it in

Let's see if we can implement another primitive type: A plane, very handy for adding a floor and walls to our scene.

Imagine a floor plane. For a point p anywhere in the world, what's the distance to the nearest point of the floor plane? Simple -- the height of the p above the floor. In this case, simply p.y.

To make a Plane primitive which we can show in any orientation, not just as a floor, we need a function where we can pass in a surface normal for the plane. Taking the dot product of p and this normal will serve to select the correct component of p. We can also specify an offset from the origin, which will move the in the direction of its surface normal.

// Plane primitive
float Plane(vec3 p, vec3 normal, float offset) {
    return dot(p, normal) + offset;
}

Now that we have a plane, let's add a floor and 3 walls to our scene:

// Scene distance function
float GetSceneDistance(vec3 p) {
    float d = Sphere(p, vec3(-.3,0,.5), .5);     // First sphere
    d = min(d, Sphere(p, vec3(.5,.25,1.2), .5)); // Second sphere
    
    d = min(d, Plane(p, vec3(0,1,0), 1.));  // Floor
    d = min(d, Plane(p, vec3(1,0,0), 3.));  // Left wall
    d = min(d, Plane(p, vec3(-1,0,0), 3.)); // Right wall
    d = min(d, Plane(p, vec3(0,0,-1), 3.)); // Back wall
    
    return d; // Return shortest distance found
}

two spheres in a room.PNG

Let's add another primitive - a humble Cube. I won't explain here how this one's derived, but you can watch an excellent Tutorial. Be aware that even if you don't know how a particular SDF (signed distance function) works, you can still incorporate it in your scene as they all, ultimately, just return a distance to a surface.

A great many more primitives are available on the brilliant website of Inigo Quilez.

// Box primitive. Note size vector to define box size.
function Box(vec3 p, vec3 worldPos, vec3 size) {
    p -= worldPos;
    vec3 q = abs(p) - size;
    return length(max(q,0.0)) + min(max(q.x,max(q.y,q.z)),0.0);
}

Here's a scene which pulls everything so far together.

// Scene distance function
float GetSceneDistance(vec3 p) {
    float d = Sphere(p, vec3(-.7, -.5 , 1.), .5);     // Sphere on floor
    d = min(d, Box(p, vec3(.7, -.5, .3), vec3(.2)));  // Box
    d = min(d, Sphere(p, vec3(.7, 0, .3), .3));       // Sphere on box
    
    d = min(d, Plane(p, vec3(0,1,0), 1.));  // Floor
    d = min(d, Plane(p, vec3(1,0,0), 3.));  // Left wall
    d = min(d, Plane(p, vec3(-1,0,0), 3.)); // Right wall
    d = min(d, Plane(p, vec3(0,0,-1), 3.)); // Back wall
    
    
    return d; // Return shortest distance found
}

spheres and box.PNG

I'm going to wrap up the shader programming series here for now, but there are a whole load of other things that we could add (and maybe will in the future). In particular, I'd like to show you how easy is is to do CSG with distance fields to make more interesting shapes, how to set different objects in the scene to have different colours and materials, how to add texture mapping, how to warp space in magnificent ways, how to animate the scene....

The possibilities are limitless, and if you've followed the series to date I hope you're at least inspired to seek out further knowledge. Inigo's site is a wonderful place to start.

Shader gallery

The cool things we've made so far in the series.

Episode 10: Cooler geometry
Episode 9: Specular lighting
Episode 8: Diffuse lighting
Episode 7: First raymarcher
Episode 5: Rotozoomer
Episode 4: Plasma

H2
H3
H4
3 columns
2 columns
1 column
Join the conversation now