SourceEngine : CSM + Soft Shadows
I have been working on Source Engine from past few months with a wonderful team at Crowbar Collective on a project called BlackMesa. It is a remake of one of the best games of all time – Half-Life which was originally released in 1988. One of the things I have been working on from past couple of months is cascaded shadow maps.
Shadows are one of the most important aspects of making a virtual scene look realistic and making games more immersive. They provide key details of object placements in the virtual world and can be pretty crucial to gameplay as well.
SourceEngine (at least the version of the engine we have) doesn’t have a high-quality shadows system that works flawlessly on both static and dynamic objects. Even the static shadows from the VRAD aren’t that great unless we use some crazy high resolutions for light maps which greatly increases both the compile times and the map size. And shadows on models just doesn’t work properly since they are all vertex lit. So we needed some sort of dynamic and a very high-quality shadow system, which is something very common nowadays in a real-time rendering application or game.
One of the most popular ways of implementing shadows is through shadow mapping algorithm. CSM or Cascade Shadow Maps is the further extension of the algorithm to generate high-quality shadows avoiding aliasing artifacts and other limitations of vanilla shadow mapping. For more details check this.
Here’s a screenshot of one of the levels from the upcoming content update –
Well, it wasn’t easy but it all worked out in the end and results are pleasing. It really improves the visual quality of the game. While compiling maps for CSM we need to use -cascadeshadows parameter for VRAD. I have also added a tricky way to make CSM work without baking CSM data into light maps. It’s handy for quick testing and also a workaround for dynamic lights / light styles issues.
We took the deferred approach for implementing shadows and here’s how CSM works in our implementation right now ( things maybe change a bit in future ) –
- Generate CSM buffer by rendering scene into 4 cascades.
- We do this via depth only shadow render pass.
- Render Scene into DepthBuffer.
- Generate a fullscreen ShadowBuffer in a deferred way.
- Render scene normally and use the ShadowBuffer from the previous pass to fetch shadow values per pixel.
- We also have uniform soft shadows in VeryHigh shadow quality mode.
More Screenshots –
Video demo of one of the multiplayer levels – dm_crossfire
Here are some new Convars that have been implemented to control various aspects of CSM. Some of these are also defined as properties of cascade entity in hammer –
- cl_csm_qualitymode (0-4): Quality setting for CSM. 0 – VeryLow, 1- Low, 2 – Medium, 3 – High, 4 – VeryHigh.
- cl_csm_max_shadow_dist: Controls the max distance for CSM shadows.
- cl_csm_lightmapblend_mode (0-1): There are two blend modes that have slightly different maths to calculate shadow color. Mode 0 will give you grayish shadows and Mode 1 will make shadow color look closer to the color of the directional light source. This convar will only affect the brushes. Here are screenshots of CSM with lightmap blend mode 0 and 1.
- cl_csm_skip_lightmap (0-1): Useful for quick testing and a workaround for light styles . This version or code path of CSM doesn’t rely on the CSM data in the lightmaps. To use this method you don’t have to compile maps with -cascadeshadows option.This also comes in two flavours cl_csm_lightmapblend_mode 0 & 1. There are just some different equations trying to generate shadows that look as close to cl_csm_skip_lightmap = 0 mode as possible. Here are screenshots with lightmap blend mode 0 & 1 – .
Video Demo –
- cl_csm_shadow_color – Shadow color RGB (0-255) controls the color of shadow. This effects only lightmapped generic material when cl_csm_skip_lightmap is turned on. This doesn’t effect prop shadows.Video Demo –
- cl_csm_shadow_color_intensity – A float multiplier that is multiplied with the shadow color. Controls the darkness of the shadow. In lightblendmode 0 increasing intensity increases the shadow darkness and in lightblendmode 1 decreasing intensity will increase the shadow darkness.
Also, this effects only lightmapped generic material when cl_csm_skip_lightmap is turned on. This doesn’t effect prop shadows.
- cl_csm_light_radius1 – If soft shadows mode is active this controls the softness of shadow in the first cascade.
- cl_csm_light_radius2 – If soft shadows mode is active this controls the softness of shadow in 2nd cascade.
- cl_csm_light_radius3 – If soft shadows mode is active this controls the softness of shadow in 3rd cascade.
Some known limitations / Issues –
- CSM is limited to only one light at the moment.
- CSM doesn’t work on many translucent surfaces (many translucent detail sprites, glass, etc). They don’t cast or receive shadows.
- CSM is disabled on reflection passes / views for now.
It wasn’t easy there were a lot of challenges and it took longer than I expected but things worked out in the end and we are happy with the results so far. First of all the Shader explosion was a big hurdle for us. According to our estimates, if we would have done things in traditional forward rendering way it would have taken 20+ days to compile one set of the shaders (on cloud with 24 core system and 128 gigs of ram) and in case you encounter a bug after that again the same time. So to complete this feature in one lifetime I took a deferred approach where I separated the shadow sampling part from the core shaders. Now all we have in code shaders is a single texture fetch instruction. Apart from reducing the shader complexity in core shaders it bought down the instructions count significantly, which will allow us to explore some more high quality shadow sampling like uniform soft shadows, physically based soft shadows via pcss, etc.
My tag team partner on this task Mark Abent (he’s a wizard with source engine) removed all the ancient code and also reduced the shader combinations significantly. Everything below dx9 was removed, a lot of unused shader code paths were removed, others were optimized to bring down the compile time and there were code changes for CSM. So many major changes broke a lot of materials and it took a lot of time to test, identify and fix all the broken materials. Debugging materials & shader bugs is not an easy task on the source engine.Even after removing so much stuff still the number of shader combinations was too high for 32bit shader compiler which was then ported to 64 bit by Mark.
The main challenge for me was dealing the source engine itself and old DX9 API. Figuring out good ways to pass data along the pipeline without breaking stuff dealing with the limitations and bugs, etc. Fun fact all the objects goes through 3 different coordinate systems and/or matrix conversions before being rendered on the screen. I spent a lot of time debugging code base finding out many of these fun facts.
I remember that to fix final two bugs for the first version of CSM I had to pull an all night long debugging session which turned out to be bugs related to proper texture/samplers were not being assigned and some state data wasn’t being passed through the pipeline properly. It sounded so obvious or minor thing upon fixing it but figuring this stuff out in source isn’t easy.
Even implementing deferred version of CSM turned out to be a little challenging. I had a hard coded version up &running quite early during the development as a proof of concept. But implementing things properly presented it’s own set of problems to deal with. Figuring out the internal pipeline details took a lot of effort.Implementing a proper depth buffer had its own challenges. Writing proper depth values, figuring out what to render in depth and what to avoid etc., took some time to figure it all out.
Before this I have never coded any shader from scratch in source neither I was very familiar with shader related source code. I just rolled with what I knew and learned as I implemented stuff. Following is a brief overview of this journey while implementing deferred shadow buffer pass –
- Here’s the first shader I implemented from scratch in the source engine. A blue screen space quad. –
- Starting to get depth values and world position here –
- Finally, something that looks like a shadow buffer –
- Final version with everything fixed –
This is one of the challenging and the most exciting thing I have ever worked on. After months of hard work & facing so many hurdles I am happy we could add this to our game. And after play testing some of the levels with csm enabled I would say IT WAS TOTALLY WORTH IT.And as I said (to fellow team members), before picking up this task, what’s the point of spending so much time & effort learning all this game development stuff, if I can’t even use it to improve Half-Life. The game that inspired me to get into game development.
On the final note I would say this – It’s not perfect and it’s not even completely done yet. There’s still some stuff left to do and some polishing is still needed. But it’s still good enough to be shipped in our upcoming game content update.