Supporting transparencies with traditional shadow mapping is straight forward and allows for nice effects but as with anything related to rendering transparents with rasterization, there are corner cases.
The implementation is really simple once you have implemented shadow mapping for opaque objects. After the opaque shadow pass, we must render the transparents into a color buffer, but reject samples which would be occluded by opaques, so using a depth read-only depth stencil state. The transparents should be blended multiplicatively. Sorting does not matter with a multiply blend state. In bullet points:
- Render opaque objects into depth stencil texture from light’s point of view
- Bind render target for shadow color filter: R8G8B8A8_UNORM works good
- Clear render target to 1,1,1,0 (RGBA) color
- Apply depth stencil state with depth read, but no write
- Apply multiplicative blend state eg:
- SrcBlend = BLEND_DEST_COLOR
- DestBlend = BLEND_ZERO
- BlendOp = BLEND_OP_ADD
- Render transparents in arbitrary order
When reading shadow maps in shading passes, we only need to multiply the lighting value with the transparent shadow map color filter if the pixel is inside the light. There is a slight problem with this approach, that you will notice immediately. Transparent objects now receive their own colored self shadow too. The simplest fix is to just disable the colored part of the shadow calculation for transparent objects. We can already produce nice effects with this, this is not a huge price to pay.
See it in action, transparents are rendered without colored self-shadows:
But they receive shadows from opaque objects just fine:
There is a technique which would allow us to render colored shadows on top of transparents too. This involves keeping an additional shadow depth map for transparencies. The flow of this technique is like this (from a Blizzard presentation):
- Render opaque shadow map
- Render transparent shadow map
- To a separate depth texture!
- Depth writes ON
- Clear shadow color filter texture (like in the simple approach)
- Render transparent again to color filter render target
- But use the opaque shadow map’s depth stencil
- depth read ON
- depth write OFF
And in the shading step, now there will be two shadow map checks, one for the opaque, one for the transparent shadow maps. Only multiply the light with the shadow filter color texture when the transparent shadow check fails.
This will eliminate false self shadows from transparent objects. But unfortunately now when a transparent receives a colored shadow, its own transparent shadow color will also contribute to itself.
What’s more interesting, are the additional effects we can achieve with transparent shadow maps:
Textured shadows, which can be used as a projector for example, just put a transparent textured geometry in front of a light:
And underwater refraction caustics:
The caustics are implemented as adding additional light to the surface, so even if the surface below the water would be in the water’s shadow it would add in the caustic effect. For this I am using the transparent color filter render target’s alpha channel with an additive blend mode. The water shadow shader renders the slope of the water normal map into the alpha channel, eg. this:
float4 outputColor.a = 1 – saturate(dot(normalMap.rgb * 2 – 1, float3(0, 0, 1)));
Which could probably use a more sophisticated method, which I didn’t bother to find yet. So in the light evaluation shaders, I add the transparent color filter alpha channel to the pixelIsInShadow value multiplied by some causticStrength modulator to render the caustics. Caustics are added because there can be brighter caustics than the light brightness, because in reality they are generated when multiple rays (photons) arrive at the same location from different sources.
That’s it, I think this is a worthwhile technique to include in a game engine. Even the simplest implementation can be used for many interesting effects. Thank you for reading!