Here’s a quick video of something I’ve been working on recently:
This is just an experiment in coming up with a more interesting ray-marched scene to test out graphics techniques. It may be developed into a proper demo at some point, but that probably requires more artistic input than I’m able to give, so we’ll see.
What we have here are two infinite length boxes, with X and Y coordinates rotated around the Z axis depending on Z. Another box is intersected at intervals to break it up a bit. Finally a texture is used to add some small-scale surface detail. So there’s nothing particularly interesting about the base shape.
A while back I had a quick search for soft shadowing techniques for ray-marching rendering, but somehow I didn’t manage to find anything very useful. So I just had a play around and came up with something. Here’s the video from last time again:
Then I did another search and found almost exactly the same technique described here by Iñigo Quilez, apart from his method was a bit simpler and more elegant so I’ve just gone with that. I’ll give a bit of background and explain the method anyway.
Why shadows are soft
If we want to model lighting in the real world we have to understand how it works and why, and then come up with an approximation in code. In the case of shadows, they are caused when the path from a point in the world to a light source is blocked by something else so the light can’t reach it. Which is obvious, but what the shadow looks like depends on the shape of the light source.
The simplest case is when the light is really tiny, so that it’s effectively just a point in space. In this case, each position in the world can either ‘see’ the light source, or it can’t. Each position is either fully lit or fully in shadow, so you get very sharp shadows. Real lights aren’t points though, and have a size. Therefore positions in the world can see either none of the light, all of it or just some of it. This gives ‘soft’ shadows, with a smooth transition from fully lit to fully shadowed. The larger the light, the softer the shadow.
This is a pretty bad diagram but hopefully it shows what I’m talking about. The region in full shadow is called the umbra, and the region in partial shadow is called the penumbra.
Shadows from point lights and area lights
One final thing to note from the diagram is that the size of the penumbra is wider the further away you are from the occluding object. This means you get sharper shadows close to the shadowing object, and blurrier shadows further away.
Larger penumbra further away
Quick intro to ray marching
I’d better give a brief description of how ray-marching renderers work, or the rest of this won’t make a lot of sense.
When using a ray marching renderer, you define your scene by using an equation. What this equation does is it takes a 3D point and tells you how far that point is from a surface in the scene. To render the scene you start at the eye position and for every pixel on the screen, get a direction vector that goes from the start position through that pixel. Then you iteratively step through the scene along this vector.
Marching along a ray until it hits the floor
At each step, you plug the current position into the equation and it tells you how far you are from a surface. You then know that you can safely move that distance along the vector and not hit anything. As you approach a surface the distance of each step will get smaller and smaller, and at some threshold you stop and say that you’ve hit it. There is a more detailed explanation in this great presentation.
Simulating soft shadows
We now know where soft shadows come from and how the ray-marching renderer works, so we can combine them.
The simplest shadowing method is hard shadows. For this we simply need to fire a ray from the position of the pixel we’re rendering, to the sun. If it hits anything, it’s in shadow. If it doesn’t (i.e. the distance function gets sufficiently large) then there is nothing in the way and it’s lit.
But, at each step we know how far the ray is from something in the scene. The closer the ray comes to an object, the more shadowed it’s going to be. Also, the further away from the rendered pixel the object is, the more shadowed it’s going to be (as objects further away case larger penumbra). Therefore, the amount of light at each step is proportional to:
distanceFromObject / distanceFromPixel
Furthermore, the penumbra size is proportional to the light source size, so we can extend it to:
Take the minimum of this at every step and clamp the amount of light to 1 (fully lit), and you’ll get nice soft shadows.
This shadow method doesn’t give fully correct shadows though. The reason is that it only gives the outer half of the penumbra, meaning that there is actually more shadow than there should be (i.e. light energy is lost). Hopefully this illustrates the reason:
Inner half of penumbra is fully shadowed
Rays that intersect with an object never make it to the light source, they just converge on the object itself. Therefore it is impossible for a single ray to measure the inner half of the penumbra. The central red line is from a pixel that will be drawn as fully shadowed, instead of half shadowed as it should be. However, it still looks nice and it’s a really simple shadowing method, so we can live with it being a little wrong.
Finally, here is the full shadow code if you’re interested:
In my last post I mentioned that even the simplest scene can look photo-realistic if lit well, before going on to talk about really simple lighting methods that really don’t. Obviously you have to start with the basics, but I was playing around with the code to see what could be done and I got it looking fairly nice.
Confession time – I was talking about polygonal rendering in my last post but the screenshots were actually from a ray-marching renderer (I’ll talk about it more on here one day), which is much easier to do realistic lighting with. But for the simple lighting examples the results are exactly the same.
The video show a method of doing soft shadows, where the softness correctly increases the further the pixel is from the occluder. The sun size can be adjusted to change the degree of blurring. After that there is some basic ambient occlusion (nothing fancy, just brute force to get it looking nice). Finally there is some postprocessing – tone mapping, bloom, antialiasing and lens flare.
The video looks pretty terrible. I suspect that the almost flat colours don’t compress well, hence all the banding. When it’s running there aren’t any banding issues, as you can hopefully see in the screenshot.