Three scenes lit only with an ambient term, including occlusion.
A couple of posts ago I mentioned I would try my hand at implementing some more general purpose low frequency ambient occlusion. The article here suggests a blurry overhead shadowmap to darken areas under trees and such. With this as inspiration, I came up with a technique I’m fairly pleased with.
My general principle is the same: I render several overhead shadow maps from various sun angles for a region. I create lightmaps of these, as if they were applied to near-ground geometry. I blend them all together, blur them, and then store the result in a texture for lookup by my ambient lighting shader.
Step by step
Here’s a view of an area of my game world with a brick wall, a canyon cliff, and some trees:
I create an orthographic projection of a 32×32 area looking down, generate a shadowmap with the “sun” roughly overhead, and then cast that onto a model of the terrain that is just slightly above the actual terrain (so that the actual terrain doesn’t cause self-shadowing everywhere). I don’t need to use any kind of fancy shadow mapping here, just a basic single tap compare will do. The resulting light map looks something like this:
That’s very crisp, let’s combine more light maps with the sun at various positions in a hemisphere above the ground:
That’s around 6 samples, and looks a little better. Let’s try 25 samples, evenly distributed around the hemisphere (incidentally see here for information on how to choose uniform sampling points on a hemisphere, using cosine weighting). Yes, this is a lot of work, and isn’t suitable for realtime. So the resulting textures are meant to be generated offline and used in realtime.
That’s looking a lot better, we can even begin to see darkness along the canyon walls. The occlusion on the ground near the brick wall is much smoother. Let’s blur it.
Now, what I do with all this data? Remember, this is all for a 32 x 32 chunk of my 1024 x 1024 map (because I’m blurring things, I actually render large enough for 34 x 34 chunk, and just pluck out the center 32 x 32 piece). I only want 32 x 32 pieces of data, so I shrink the image down to that size, and here is the result (magnified).
The above is a very coarse representation of the ambient occlusion at ground level. I can sample from this during my lighting pass and include it in the ambient lighting term.
It’s not good enough as is though, since most objects are above the ground, not right at the ground. The above image is measuring shadow at the ground. The area with the brick wall, for instance, is very dark. That would mean I would render the top of my brick wall very dark, since it’s in the dark region – even though it basically isn’t occluded at all. The same goes for my trees and such.
So what I want to do is to measure the occlusion at points higher above the ground too. So we’re basically developing not just a 2d grid of occlusion, but multiple layers of grids. My game is basically 2.5d though – there isn’t much verticality. I don’t need a full 3d cube of occlusion. Just a few layers will do.
I end up rendering a total of 3 layers (I’ll describe why I chose 3). Here’s another section of my game world, and 3 “layers” of terrain (onto to which shadows are projected) visualized in magenta:
I use fixed layer heights roughly based on how tall I expect objects to be in my game.
Why 3 layers? Well it fits nicely in a single texture. Remember, I am using this in my ambient lighting shader. In that shader I have access to the world position of the pixel being rendered. So I can sample from the appropriate location in my 1024 x 1024 occlusion texture. However, I also need the reference ground height at this location. I could sample from the heightmap that I have available, but that would be an extra texture sampling operation. So instead I stash the reference ground height in the alpha component, and the occlusion terms for the 3 layers in the R, G and B channels. Once I figure out how high I am above the ground, I lerp between the three occlusion channels to get the value I need.
The total cost in the shader is one texture sample and 15 instructions. The per-frame costs appears to be under 0.1ms on my GeForce GT 240.
float OcclusionStrength; // I'm using 0.8 right now.
// These define the thicknesses of the occlusion bands/layers.
#define BAND_HEIGHT 2.0
#define HEIGHT_BAND_UPPER 4.0
#define HEIGHT_BAND_LOWER 2.0
float GetOcclusionAtPoint(float3 worldPosition)
// We don't need the DX9 half texel offset. The center of each texel represents the point in between two grid vertices,
// not an actual grid vertex.
float2 texCoord = worldPosition.xz * OneOverWorldSize;
float4 sample = tex2D(OcclusionAndHeightSampler, texCoord);
float terrainReferenceHeight = sample.a * YDisplacementScale; // YDisplacementScale converts [0-1] into actual world values.
float heightAboveTerrain = worldPosition.y - terrainReferenceHeight;
// heightAboveTerrain should guide us as to the balance of the RGB channels. We'll end up lerping between two of them.
bands.x = heightAboveTerrain < HEIGHT_BAND_LOWER;
bands.y = (heightAboveTerrain >= HEIGHT_BAND_LOWER) && (heightAboveTerrain < HEIGHT_BAND_UPPER);
bands.z = heightAboveTerrain >= HEIGHT_BAND_UPPER;
float3 sampleA = float3(sample.gb, 1);
float3 sampleB = sample.rgb;
float3 lerpAmount = saturate(float3(HEIGHT_BAND_LOWER - heightAboveTerrain, HEIGHT_BAND_UPPER - heightAboveTerrain, HEIGHT_BAND_UPPER + BAND_HEIGHT - heightAboveTerrain) / BAND_HEIGHT);
float occlusion = lerp(dot(bands, sampleA), dot(bands, sampleB), dot(bands, lerpAmount));
// We could optimize this by storing the inverse in the texture to avoid some instructions.
return 1 - (1 - occlusion) * OcclusionStrength;
What does it look like? Here’s the previous scene rendered with only the occlusion term:
You can see the obvious darkening at the base of the brick wall (but not at the top, because we have multiple occlusion layers!), on the lower parts of the trees, and on the steep cliff faces.
Now, with out any direct sunlight, the scene would be pretty muddled (no sunlight means there is just an overhead ambient term representing light from a cloudy sky):
In the above image, the tree and plant already have some high frequency AO baked into them (which is using in the ambient lighting equation), so they look a little better (or rather different) than the surroundings. Now we include the occlusion term:
The old way
To be fair, I did have a similar but more limited mechanism functioning previously. I ran an offline ray-caster on my terrain and baked some occlusion into the terrain itself, so I did have some darkening in corners and cliff faces. But the ray-casting only included other terrain, not all the static world objects like this method described in this article does.
To make up for that, I had an overly complex system that, for a limited set of world objects (trees and walls, but nothing else), would draw some appropriate occlusion terms into a texture (e.g. a round blurry disc below plants and trees). This would be picked up by the terrain shader and included in the AO already baked into the terrain. So it allows some world objects to affect the terrain only.
However, the new way I’m doing things lets all (static) objects occlude all others (and it’s simpler, really).
I might keep the old way for dynamic objects causing some occlusion against terrain only.
Some more images
In my next post, I hope to talk more about other lighting changes I’ve been doing in the hopes of improving my lighting overall. For now though, here are some more images related to this post.
Here are some cylinder “probes” to help see the effect of the occlusion at various heights above the ground, next to various objects and terrain features:
Here’s a moving object placed under a tree and out in the open. There are no directional light sources in this image at all, just the overhead ambient skylight. You can see the man becomes darker under the cover of trees.
The next two images below show the subtle effect this gives. In each case, the bottom portion is the one with the occlusion term.
Note the darkening on the cliff below the conifer trees, for instance.
Here’s the AO term only:
There is sunlight in this picture, but the shaded canyon is a little darker due to less ambient light:
Doing work like this really makes you appreciate well-factored code. While I’m in the world editor, I want to be able to push a button and have the occlusion calculated for the entire world map. This requires setting up another rendering path, camera, loading all parts of the world as needed, and so on… – without affecting the “current” version of the world that is loaded. Because I’m lazy, I still had several places in my code where I was using static/global variables. I had to clean up all those code paths and remove almost all global state to get this working cleanly. Which just reinforces the belief that you will always regret having global state. There will always end up being a reason to remove it.
A pleasant surprise though, was how easy it was to get the entity component system working in this version of the world. I currently have around 15 systems operating on the entities, and there is a fair amount of code to set them all up. I was about to refactor that code to make it accessible by the occlusion-calculating code, when I realized all I needed were the RenderingSystem and ChildTransformSystem. So I just instantiate my entity component framework and add only those two systems. It was just a handful of lines of code, and a small modification to the RenderingSystem to only render objects marked as “static”.