Shadows
Shadows are a natural (if relatively-expensive-to-compute) part of light propagation. But shadows in games serve a number of more important purposes than providing a more accurate simulation of reality. Indeed, accurately simulating reality is seldom the goal of a game; reality does a good enough job of simulating itself.
Shadows can help players to understand the structure of an environment, the relation of characters to that environment, and can serve explicit gameplay purposes ("stay in the shadows to hide", "lure the monster into the light").
This Mini-lesson will introduce four shadowing ideas. It is my intent to briefly discuss a few high points of each technique to get you started. There are a ton of existing resources out there to continue with. I will provide pointers in the lesson text, but web search is your friend here.
Blob Shadows
One very important function of shadows in games with 3D environments is to act as an indicator of whether objects are floating or sitting on the ground. This particular use case is important enough in 3D platformers that the game will often include a special shadow exclusively to show where the player will land if they are jumping.
One way of making these "hovering-or-standing?" shadows is to cast a ray downward from each character and explicitly draw a transparent, dark "blob" where this ray hits the ground below the character. These are called "blob shadows."
Blob shadows are very cheap to implement and render because they require no special new shadow data structures and position the whole shadow using only a single ray-cast. (Though you can fancy them up a bit by drawing the blob with "decal rendering" -- web search that if you want to know more -- but you don't need to. Blob shadows will still generally look fine if they are rendered as a transparent texture on a quad which is just slightly offset from the ground.)
You even see blob shadows used in 2D/isometric games, since it is often quite straightforward to draw a simple oval-shaped shadow sprite in addition to a player sprite.
Contact Shadows
In addition to serving the very important gameplay purpose of understanding the ground-relative position of characters, blob shadows also simulate a phenomena known as "contact shadows" or "ambient occlusion".
The general idea of ambient occlusion is that parts of objects that are in folds or creases (or, e.g., right next to a character's foot) can see less of the environment around them, so receive less [direct and indirect] light. Ambient occlusion code simulates this effect by adding additional shadowing to areas that have a lot of nearby geometry.
You could imagine precomputing this information (see precomputed radiance transfer for a particularly fancy way of doing that), but typical modern games instead use dynamic, real-time screen-space ambient occlusion approaches. Broadly, these approaches use a series of depth buffer lookups at each pixel to estimate whether local geometry is shadowing the current fragment.
Screen-space ambient occlusion produces nice contact shadows and (generally) accentuates the shapes of objects in 3D scenes, which can really help your game's geometric detail "pop".
Cast Shadows
Finally, we come to what you probably think of when you think of shadows: cast shadows. I'll discuss two techniques here, but be aware that I'm mostly talking about shadow volumes / stencil buffer shadows because I really like the technique and the crisp shadow edges it produces, not because it's something people use in modern engines.
Also be aware that as accelerated raytracing becomes more prevalent, the possibility of using raytracing to compute shadowing becomes more realistic (though, as of this writing, it's still not really practical for direct lighting visibility queries).
Stencil-Buffer Shadows
Stencil-buffer shadows (or "shadow volumes") involve using a vertex shader to build special shadow geometry, rendering this geometry against the scene (with depth and color writes disabled) to produce a mask in the stencil buffer which indicates which pixels are in shadow, and then rendering lighting in another pass that reads the stencil buffer.
Shadow-volume shadows are perfect shadows in the sense that they are exactly computing intersections between shadow volumes and scene geometry at the per-pixel level. This is also their greatest downfall, because shadow volume shadows always have perfect hard edges (which correspond to perfect point lights, rather than lights with spatial extent, which is what we expect in the real world). And, given that shadow-volume shadows are computed by rendering geometry, it is very computationally expensive to take multiple samples to make them softer.
Further, because shadow volumes are perfectly testing against the depth buffer, they tend to reveal other fakery in play; for example, detail added with normal maps or parallax bump maps will not have any influence on stencil shadows.
Finally, stencil-based shadows require multi-pass (or, possibly, deferred) light rendering, and so have all of the framebuffer memory bandwidth problems associated with those techniques.
Further reading:
- Stencil-buffer shadows in javascript: http://graphics.cs.cmu.edu/courses/15-466-f20/notes/stencil-toy.html (the source code is reasonably readable, though I recall that it contains a few errors that cancel each-other out)
- A game I used the above in: https://ixchow.github.io/gridwords/?0
- Doom 3, famously, has very nice stencil-buffer-based shadows. Worth looking at gameplay footage. Also, worth doing a web search to learn about "Carmack's Reverse" -- both the technique and the patent kerfuffle.
- GPUGems chapter with some nice pictures: https://developer.download.nvidia.com/books/HTML/gpugems/gpugems_ch09.html
Shadow-Map Shadows
By far the most common method of computing visibility to dynamic lights is to render the scene's depth from the point of view of the light into a texture, then to read this texture from the shader doing the lighting in order to check if the current fragment is visible from the light. This technique is known as "shadow mapping" and the texture containing the depth data is called a "shadow map".
Shadow maps are so common that there are special texture lookup modes on GPUs to support shadow mapping. Two to be specifically aware of are projective texture sampling operations and comparison texture fetches.
Projective texture lookups (e.g. textureProj
in GLSL) are a texture lookup with a built in homogenous-coordinate divide;
this means you can use perspective projection in a world-to-light-space matrix without needing to write texture(coord.xy/coord.w)
explicitly.
Further, at least on older hardware, this will likely use a separate division unit to compute the normalization as part of the texture fetch pipeline, enabling faster overall performance.
Comparison texture fetches (see OpenGL wiki) allows a shader to perform depth comparisons before texture filtering, effectively requiring 4x fewer texture()
calls (worth taking a moment to think about why) when doing soft shadows.
Though, again, on newer hardware this might not actually result in any performance improvement, it still makes the code a bit cleaner.
See also:
- Nvidia's "soft shadows" sample code
- Large outdoor scenes used to be considered a weakness of shadow maps. "Cascaded shadow maps" solve this problem by using different map resolutions at different distances, and are widely used in modern engines.
- The base code for 'Shady Business' (game3 in f18) has a shadow map implementation that you could probably forward-port to our current base code: https://github.com/ixchow/15-466-f18-base3
- Shadow maps details from "The Witness": http://www.ludicon.com/castano/blog/articles/shadow-mapping-summary-part-1/
Conclusion
This has been a whirlwind tour through different shadow rendering techniques. There are lots of documents out there (just web search!) about various shadowing technqiues if you want to learn more.
And, as you do learn more, remember that shadows have gameplay consequences. Think of shadowing as a tool to further the gameplay you want to encourage. Don't let imitation of reality get in the way of making sure your 3D platformer has an (unrealistic but essential) landing shadow!