It turns out that I was performing a linear filter on my shadow map depths, but I should have been doing the shadow map prefiltering in log space. Whoops.
Actually, there seems to be a lot of confusion about how to filter in log space when outputting linear depth, and why you need to. Let me clear things up.
There are two approaches to exponential shadow mapping:
1. Output exponential light depth map, prefilter linearly.
You can create a depth map for the light by outputting exp(depth) and then perform the typical gaussian/box/tent prefiltering as normal. In this case, when drawing the light, the ESM filtering is done like so:
float occluder = tex2D(shadowMap, texCoords); float lit = occluder / exp(c*reciever);
The advantage of this method is that the prefiltering is less expensive than the next method. Hardware bilinear, anisotropic and mip filtering all automatically filter the shadow map correctly.
The disadvantage of this method is that it is precision hungry, because exp(depth) varies quickly. A 32 bit floating point texture allows a value of c=88 before overflow errors start to occur. 16 bits is not enough precision with this method in my experience as contact light leaking is very problematic.
2. Output linear depth, prefilter in log space.
You can create a depth map for the light by outputting depth as is, and then perform a gaussian/box/tent prefilter in log space. In this case, the when drawing the light, the ESM filtering is done like so:
float occluder = tex2D(shadowMap, texCoords); float lit = exp(c*(occluder - reciever));
The advantages of this method:
- Less precision is required to store depth, which now varies linearly. So we only need 16 bits to store depth!
- The ESM filtering when drawing the light is slightly faster as we can remove a division.
The disadvantages:
- Hardware bilinear and anisotropic filtering will introduce some error, although it is generally close enough — the artifact is just a little bit of shadow overdarkening.
- Prefiltering must be done in log space, which is slower (see below).
- If mipmaps are used, they must be generated using filtering in log space as well.
Overall, these two methods represent a tradeoff between memory and ALU, with method 1 requiring more memory and less ALU overall.
So how and why do we filter in log space?
Consider for example that we wish to average two values in the shadow map (like in a 2×2 separable gaussian or box blur). Then we are averaging the two values exp(d1) and exp(d2).
Using gaussian/box blur weights w1 and w2, we then have:
w1*exp(d1) + w2*exp(d2)
exp(d1) * (w1 + w2*exp(d2 – d1))
exp(d1) * exp(log(w1 + w2*(exp(d2-d1))))
exp(d2 + log(w1 + w2*exp(d2-d1)))
So now the sum of the two exponentials is written as one exponential. Taking the log of the previous statement, we can perform the averaging of the box/gaussian blur by working on the exponentials argument only:
d2 + log(w1 + w2*exp(d2-d1))
So we filter the arguments of the exponentials, and then go back to exponential space when actually drawing the light, using exp(c*(occluder – reciever)) as we saw earlier.
I’ve generalized the above reasoning for two arguments to arbitrarily many arguments in the following code that can perform a box blur in log space, using an HLSL pixel shader. To perform a Gaussian blur instead, just replace the constant sample weights and 1.0 with the appropriate Gaussian weights.
sampler TextureSampler : register(s0);
#define SAMPLE_COUNT 3
float2 Offsets[SAMPLE_COUNT];
float log_space(float w0, float d1, float w1, float d2){
return (d1 + log(w0 + (w1 * exp(d2 - d1))));
}
float4 Blur(float2 texCoord : TEXCOORD0) : COLOR0
{
float v, B, B2;
float w = (1.0/SAMPLE_COUNT);
B = tex2D(TextureSampler, texCoord + Offsets[0]);
B2 = tex2D(TextureSampler, texCoord + Offsets[0]);
v = log_conv(w, B, w, B2);
for(int i = 2; i < SAMPLE_COUNT; i++)
{
B = tex2D(TextureSampler, texCoord + Offsets[i]);
v = log_conv(1.0, v, w, B);
}
return v;
}
So what does all this extra work get us? The error introduced by filtering linearly instead of in log space was so small in my tests, that I couldn’t produce a screenshot that clearly demonstrates it.
Here is the thread that inspired me to try filtering in log space, and also a clear difference between linear and log filtering is demonstrated.
I found that on the Xbox 360, log filtering didn’t cost anything extra, as the prefiltering step was texture bandwidth bound anyway — so I’ll leave it in place for now.
Hi,
B2′s offset should be 1 instead of 0.