Local 2D Lighting & Shadowing Part 2

In Game Demonstration

In Editor Demonstration

HLSL Raymarch code

HLSL Raymarch code

Local 2D Lighting & Shadowing Part 2

Please see Part 1 for information on GI, AO and 2D Light Assets

For shadows things get a bit more tricky. A lot of examples of 2D shadows solutions in games involve tracing outward from the source to find surfaces, then tracing to the corners of that surface and finally constructing a triangle out of the three points. All of these triangles come together to form the mask used by the light. This could in theory be done from blueprints, but would involve a heavy number of traces on the CPU. Doing this on tick for a purely visual element of the scene did not seem responsible. Possibly we could precalculate all of this in the construction script and bake out a mask or mesh, but we have a dynamic scene with characters running around so this was out as well.
However we don't have to throw the whole idea out. What if we can achieve the same general idea of tracing from the light source to various points to generate a mask, but do so purely on the GPU? To do this we need an input of the scene that the GPU can iterate over. Taking a render target capture of the scene depth ends up being perfect for this. With an RT capture component hooked up to the above lighting asset we now have a real time representation we can pass to our material. From here we implement a pretty straight forward ray-marching algorithm via a custom HLSL node to trace outward from the light source in local space. If anywhere along that loop we sample the depth and if it falls within a pre-defined bounds we mark the point as 0 (black). If it never hits this check we assume its 1 (white). Now we have a our shadow mask that can be applied directly over the top of the existing lighting assets. All done right? Nope.
Rarely is their a silver bullet for these sorts of problems. In this case doing some initial PIX captures on our target platforms showed that while in theory this path could be cheaper than a traditional 3D shadow casting light it was by no means free. Thankfully there were a number of avenues available for optimizations.

The first was the capture component itself. By default they capture ever frame, but in reality if there is nothing moving in the scope of the light space then we don't need to update. Capturing creation and then only capturing when a character actor is overlapping is a great start. We can also filter pretty much every render flag other than static and skeletal meshes to prevent the render target from drawing things like lighting, fog and translucency which saves time. We can also scale the RT render size relative to the bounds of the light itself to optimize on total texel sizes as long as we make sure to put appropriate clamping on either end. Since render targets do not Mip there is no need to maintain a power of 2 resolution. Finally Epic recent exposed a flag for pushing scene depth captures into the main renderer which mitigates much of the overhead of a separate scene capture in PIX.

The second big overhead is the custom raymarching HLSL code in the material. This is almost as expensive as the capture was because for each pixel we are looping N number of times as we step away from the light source and sample the depth texture. Lowing the number of steps too far results in unacceptable shadow banding. However we can make some assumptions that will allow us to early out or skip these loops all together for some pixels. Specifically we've already calculated an outer mask for the light source to approximate its shape (sphere, cone or rect). Since there will always be some number of pixels that are never lit we can also assume that they will never need to be shadowed. Using an if statement we can completly bypass the ray-march loop for all pixels that have a mask below some value close to 0. We can also optimize within the loop by exiting early as soon as we hit a sample within the depth band we are checking since we can assume that all checks beyond that point are irrelevant.

The final optimization is simply that for our lowest platforms we will opt not to capture shadowing for dynamic objects. This allows us to capture once on load (usually during a black screen) and then never again. This reduces the dynamism of the scene, but does maintain the overall look and feel that art direction was striving for and seems like an acceptable trade off to reach a larger market.

These optimizations have a big impact on reducing perf, but are no substitution for good process and guidelines. Beyond this work it is important to use measurements via frame analysis to create clear bounds for artists to work within. For most cases it seemed reasonable to keep the number of shadow casting lights on screen limited to 2-4 and to minimize overlap not just for performance, but because the overall visuals quickly became muddier and reduced overall visual clarity of the style.

Creative Direction - Duncan Drummond
Art Direction - Jeramy Cooke
Level Art/Design/Props - Nick Bizzozero