Why does my variance shadow map need an offset?

jbizzler

Newcomer
I decided to forget gradient shadow mapping for variance shadow mapping. Where there are supposed to be shadows, there are definately shadows, but the rest is loaded with acne like when you don't do an offset in regular shadow mapping. Reading NVIDIA's variance shadow mapping whitepaper, it specifically mentions you don't need an offset. And when I try and add an offset anywhere in the process, instead of shadow acne, there are white spots all over. My implementation is pretty similar to the NVIDIA SDK sample, but my light is an orthographic one, so I can't just calculate distLight liek they do, so I set it as the pos.z. I think this may be the problem, but really don't know. Anyone else know?
 
You don't need an offset (bias), although numeric precision issues will still cause a slight problem (which is unavoidable without exact rational arithmetic :)). The easiest solution is to clamp the variance to a small value before calculating Chebyshev's inequality, something like:

Variance = E_x2 - E_x*E_x;
Variance = max(Variance, 0.000001);
p_max = ...

The value that you clamp to is not really related to the scene, and a very small value will usually remove all self-shadowing artifacts without affecting the "real" shadows at all. The value also does not need tweaking: it really can be set once and forgotten about. You can also do some fancier math related to the surface normal (or depth derivatives), but that's probably overkill since clamping the variance works so well in my experience.

Regarding depth metrics: if you're using a directional light, you can use the light-space Z value (BEFORE diving by z!!!) as the depth metric, and that will work well. The easiest way of implementing this is something like:

// Vertex shader
LightSpacePos = mul(Position, LightViewMatrix);
Output.Position = mul(LightSpacePos, LightProjectionMatrix);
Output.Depth = (LightSpacePos.z - NearClipPlane) / (FarClipPlane - NearClipPlane);

Alternatively (if you're using a "normal" orthogonal projection matrix) you can just divide Output.Position by FarClipPlane to save a few instructions, but the above is a bit more clear on what's going on.

For a spot or point light, the best depth metric in my experience is "distance to light", which must actually be calculated by interpolating the light space position (float3) and computing the length in the pixel shader (the NVIDIA demo actually does it incorrectly I believe).
 
Thank you so much. I had to clamp it to 0.00000001. Any higher and the shadow stops showing up completely. Now all I have to do blur it and it'll be as beuatiful as your demo.
 
One more thing. I notice your demo uses 4 32bit components. The NVIDIA demo only uses 2, the depth and its square. What are those other values?
 
The amount that you clamp to will be dependent on your depth metric... the value that I gave works pretty well if you're rescaling to the [0, 1] range. Use whatever is appropriate for your application of course :)

My demo used 4 components to split precision, since summed-area tables eat precision for breakfast! If you're only doing normal variance shadow maps + blur (which is definitely preferable if you don't need per-pixel variable filter widths for soft shadows or something), 2x fp32 is quite enough!
 
Okay, sweet. Thanks so much. I've been looking for a good shadow solution for a long time now, and this is by far the best yet. I'm gonna code a blur tomorrow morning. If I have any questions, I'll let you know here.
 
Okay, I think I just about have the blur working. I just have a small problem. I have the blur vertex shader generate the position and texture coordinate based on the vertex id, like the NVIDIA demo does, so I don't even need a vertex buffer or an input layout. This is how the NVIDIA demo does it, but wehn I do it, I recieve this error:

Code:
D3D10: ERROR: ID3D10Device::Draw: The Vertex Shader expects application provided input data (which is to say data other than hardware auto-generated values such as VertexID or InstanceID). Therefore an Input Assembler object is expected, but none is bound. [ EXECUTION ERROR #349: DEVICE_DRAW_INPUTLAYOUT_NOT_SET ]

So why do I need an input layout and they don't?
 
Back
Top