Today we will be looking into ambient occlusion. We are going to work on the playground we used in Shadows in Metal part 2 and build up on that. First, let’s add a new object type - a rectangular box:
Next, let’s also add a new distance function for our new struct:
Then, update our scene to something new:
What we did here was to first draw a sphere with a radius of
8, one with a radius of
6 and take the difference between them. Since they have the same center the smaller one would not be visible unless we made a cross sectioning somehow. That was exactly why we used a third sphere, much larger and with a different center. We took the difference again and we could now see the result of the first difference. Finally, we added a box in there for a nicer, more diverse view. If you run the playground now, you should see something similar:
Next, let’s delete the lighting() and shadow() functions as we don’t need them anymore. Also, delete the Light struct and its two instances inside the kernel. Now let’s create an
ambient occlusion surrogate function:
We’re just using the normal’s
y component for light, which is like having a light directly above. Inside the kernel, right after creating the normal (inside the
else block), call the
There are no shadows anymore, only a basic (directly above) light. If you run the playground now, you should see something similar:
Time to get some real
ambient occlusion now. Ambient means the light does not come from a well defined light source but rather means general background lighting. Occlusion means how much ambient light is blocked. We take the point on the surface where our ray hits and look at what’s around it. If there’s an object anywhere around it, that will block most of the light in the scene, so this is a dark area. If there’s nothing around it, then the area is well lit. For in between situations though, we need to figure out more precisely how much light was occluded. Introducing the cone tracing concept.
The idea of
cone tracing is using a cone in the scene, instead of a ray. If the cone intersects an object, we don’t just have a simple
true/false result. We can find out how much of the cone the object covers at that point. But how do we even trace a cone? We could make a cone using many spheres. Try to imagine several spheres along a line, very small at one end, big at the other end. This is as good a cone approximation we can get here. Here are the steps we want to take:
- Start at the point on the surface
- March out from the surface, along the normal
- For each iteration, determine how much of the sphere is filled by the scene using distance function
- For each iteration, double the distance from the surface, and also double the size of the sphere
Since we are doubling the sphere size at each step, that means we travel out from the surface very fast so we need fewer iterations. That also gives us a nice wide cone. Here is the complete
Let’s go over the code, line by line. First we define the eps variable which is both the cone radius and the distance from the surface. Then, we move away a bit to prevent hitting surface we’re moving away from. Next, we define the occlusion variable which is initially nil (scene is all lit). Then, we enter the loop and at each iteration we get the scene distance, double the radius so we know how much of the cone is occluded, make sure we eliminate negative values for the light, get the amount (ratio) of occlusion scaled by the cone width, set a lower impact for more distant occluders (the iteration count gives us this), preserve the highest occlusion value so far, double the eps value and finally move along the normal by that distance. We then return a value that represents how much light reaches this point.
Now lets have a camera struct. It needs a position. Instead of camera direction we’ll just store a ray. Finally the rayDivergence gives us a factor of how much the ray spreads.
Next, we need to set up the camera. It needs the camera position, a look-at target, the field of view and the view coordinates:
Now we just need to initialize the camera. We’ll have it circling the scene, looking at the center (0,0,0). Add this to the kernel, just after you set up the
Then delete the ray variable, and replace everywhere it was used in the kernel with cam.ray instead. If you run the playground now, you should see something similar:
To see an animated version of this code, use the
Shadertoy embedded player below. Just hover over it and click the play button to watch it in action:
Until next time!