Skip to main content

Screen Space Shadows

Screen space shadows add the small contact shadows that shadow maps miss: grass on the ground, pebbles, cracks, folds in cloth, distant tree detail. XFSDK Light's version is based on the technique Bend Studio presented at SIGGRAPH 2023 and shipped in Days Gone, and follows their public reference code.

xl_sss_enable=1

How it works​

For each pixel, a short ray is marched through the depth buffer towards the light. If the ray passes behind something on screen, the pixel is in shadow. Bend Studio's key idea makes this cheap: pixels whose rays point at the same spot share their depth samples, so one GPU wave of 64 threads walks a single line of pixels towards the light. Each depth sample is read once and reused by every pixel on that line.

  • A pixel is treated as a slab with a thickness (xl_sss_surface_thickness). A ray only counts as blocked when it passes behind a surface by roughly 0.25 to 1.75 times that thickness. Too thin and long shadows stop early; too thick and objects cast shadows onto things far behind them.
  • The shadow length is a number of pixels (xl_sss_sample_count, 60 by default), not a world distance.
  • The last samples fade out, so shadows end softly.

XFSDK Light applies the shadow to the scene color before depth of field, with a 3×3 depth-aware blur. Shadows fade out between xl_sss_fade_ratio × xl_sss_max_distance and xl_sss_max_distance (30 m to 50 m by default).

Compared to the original​

Bend StudioXFSDK Light
Where the shadow goesInto the sun's shadow term, before lightingApplied to scene color after lighting (post process)
ThicknessOne global valueGlobal, plus an optional per-material thickness
Depth biasStored in the GBuffer, added to the depth buffer for all systemsStored in Opacity, used by these shadows only
LightsSunMost important light, or up to 4 lights

Because it runs after lighting, mode 0 darkens the whole pixel, including sky light and GI. Mode 1 is closer to Bend Studio's result: it removes only the sun's estimated direct light. See Composite.

Settings​

Console variableDefaultDescription
xl_sss_enable0Turns the effect on.
xl_sss_surface_thickness0.05Thickness of each pixel, as a fraction of its depth. Roughly 5% of the camera distance: 50 cm at 10 m. Change it in steps of ×2.
xl_sss_sample_count60Shadow length in pixels (16–120). Cost grows with it.
xl_sss_shadow_strength0.50 no effect, 1 full black.
xl_sss_shadow_contrast4.0Sharpness of the shadow edge (1–8).
xl_sss_bilinear_threshold0.02Depth difference treated as an edge, where samples are not blended. Scale it with the thickness.
xl_sss_ignore_edge_pixels01: edge pixels don't cast. Helps on flat surfaces at grazing angles, thins foliage shadows.
xl_sss_min_depth_delta0.05Casters closer than this fraction of a thickness window don't shadow. Removes striping in tiny crevices.
xl_sss_max_distance5000Distance (cm) where shadows have fully faded.
xl_sss_fade_ratio0.6Fraction of the max distance where the fade starts.
xl_sss_multi_light00 most important light only, 1 up to 4 lights (more expensive).
xl_sss_use_early_out1Skips sky and out-of-range pixels.

Excluding objects​

Objects with Render CustomDepth Pass on and a Custom Depth Stencil Value with bit 1 set (for example 1) neither cast nor receive these shadows. Use it for first-person weapons, UI meshes or characters you shade differently. This needs custom depth with stencil enabled (r.CustomDepth=3).

Runtime Bend SSS API​

Bend Studio Screen-Space Shadows can be controlled at runtime from Blueprint or C++.

The public API validates its inputs and reports unavailable console variables through the XFSDK Light diagnostics system. These are non-fatal failures.

Blueprint functions​

The Blueprint functions are available under XFSDK Light → Bend SSS.

Blueprint nodeC++ functionDescription
Set Bend SSS EnabledXFSDK_BendSSS_SetEnabledEnables or disables Bend SSS.
Set All Significant Lights EnabledXFSDK_BendSSS_SetAllSignificantLightsEnabledEnables shadows for the significant point and spot lights selected by the renderer, up to four lights.
Set Shadow ModeXFSDK_BendSSS_SetShadowModeSelects the shadow-composition mode.
Set Shadow StrengthXFSDK_BendSSS_SetShadowStrengthSets shadow strength in the inclusive range 0.0–1.0.
Set Shadow ContrastXFSDK_BendSSS_SetShadowContrastSets shadow contrast in the inclusive range 1.0–8.0.
Set Shadow DesaturationXFSDK_BendSSS_SetShadowDesaturationSets shadow desaturation in the inclusive range 0.0–1.0.

All setter functions return true when the setting was applied and false when the input is invalid or the required console variable is unavailable.

Shadow modes​

enum class EXFSDKBendSSSShadowMode : uint8
{
Darken,
RemoveDirectLight
};
ModeDescription
DarkenComposites the screen-space shadow by darkening the scene color. Shadow desaturation applies in this mode.
RemoveDirectLightRemoves the estimated direct-light contribution in shadowed regions. If no suitable directional-light contribution is available, the renderer falls back to darkening.

Significant lights​

When All Significant Lights is enabled, Bend SSS processes the significant point and spot lights selected by the renderer, up to four total lights.

When it is disabled, only the best selected local light is used.

C++ setup​

Add XFSDK_System to the consuming module:

PrivateDependencyModuleNames.AddRange(
new string[]
{
"XFSDK_System"
});

Include the API header:

#include "XFSDK_BendSSS_API.h"

Enable Bend SSS:

UXFSDKBendSSSAPI::XFSDK_BendSSS_SetEnabled(true);

Enable all selected significant lights:

UXFSDKBendSSSAPI::XFSDK_BendSSS_SetAllSignificantLightsEnabled(true);

Configure the shadow appearance:

UXFSDKBendSSSAPI::XFSDK_BendSSS_SetShadowMode(EXFSDKBendSSSShadowMode::Darken);

UXFSDKBendSSSAPI::XFSDK_BendSSS_SetShadowStrength(0.8f);
UXFSDKBendSSSAPI::XFSDK_BendSSS_SetShadowContrast(2.0f);
UXFSDKBendSSSAPI::XFSDK_BendSSS_SetShadowDesaturation(0.25f);

Disable Bend SSS:

UXFSDKBendSSSAPI::XFSDK_BendSSS_SetEnabled(false);

Composite​

xl_sss_shadow_modeResult
0Darken (default). Multiplies the pixel. xl_sss_shadow_desaturation (0–1) pulls the shadow towards grey instead of only darkening it, which reads less like a colored overlay.
1Remove direct light. Subtracts the directional light's estimated diffuse light from the pixel, never going below the sky ambient. Shadowed areas keep their ambient and GI. Falls back to mode 0 without a directional light.

Per-material data​

Shadow thickness and depth bias can be set per material. Unreal has no free GBuffer channel for this, so XFSDK Light reuses the Preintegrated Skin shading model:

  • A small engine shader patch makes Preintegrated Skin render exactly like Default Lit.
  • Its Subsurface Color and Opacity inputs are then free, and carry the shadow data.

1. Patch the engine shaders​

  1. In the level editor toolbar, open the XFSDK Light menu and choose Patch Engine Shaders.
  2. Restart the editor. All shaders recompile.

The patch edits five files in the engine's Shaders folder: GBufferHelpers.ush, DeferredShadingCommon.ush, ShadingModels.ush, BasePassPixelShader.usf and Lumen/LumenCardBasePass.ush. It needs stock UE 5.8 with Substrate off. Revert Engine Shader Patch in the same menu restores the files. Engine updates overwrite the patch; apply it again after updating.

note

If you already patched with an earlier 3.0 build, the menu shows the patch as partial. Click Patch Engine Shaders again: it only adds the missing file.

2. Turn on the data you use​

xl_sss_material_thickness=1
xl_sss_depth_bias=1

3. Set up the material​

  1. Set the material's Shading Model to Preintegrated Skin.
  2. Add the XFSDK Light material function node.
  3. Connect its SubsurfaceColor output to Subsurface Color and its Opacity output to Opacity.
Node inputDefaultMeaning
Shadow Thickness0.50.5 = the global thickness. 0 and 1 = 16× thinner and 16× thicker (xl_sss_material_thickness_range, in stops).
Behaviour11 = casts shadows, 0 = only receives.
Depth Bias (Height Map)1Height for depth bias. 1 = the rendered surface, 0 = xl_sss_depth_bias_range behind it.

Choosing a thickness​

The thickness window is roughly thickness × camera distance:

Shadow ThicknessMultiplierAt 10 m (default global 0.05)Good for
0.25×¼~12 cmFlat ground with depth bias
0.375×½~25 cmRocks, debris
0.5×1~50 cmMost props
0.625×2~1 mGrass and bush clumps
0.75×4~2 mVery dense foliage

If shadows stop abruptly behind an object, it is too thin: the ray passes "behind" it. If objects cast blobby shadows onto things far behind them, it is too thick. If shadows end at the same on-screen length no matter the thickness, raise xl_sss_sample_count.

Depth bias​

Depth bias lets flat surfaces with a height map cast micro shadows: stones on a ground texture, bark, bricks. The height pushes each pixel's depth back for the shadow trace only, so both the receiver and its neighbors see the bumps. This is the "Depth Bias" idea from Bend Studio's talk, where it was used on Days Gone's ground materials.

  • Feed a height map, white = high into Depth Bias. White stays at the rendered surface, black goes xl_sss_depth_bias_range (8 cm by default) behind it.
  • Set xl_sss_depth_bias_range to the real relief of the texture: 3–8 cm for pebbles and dirt. Large values turn every dark patch into a deep hole with long shadows.
  • Use a low Shadow Thickness (0.2–0.4) on these materials. A bump only casts when it is at least about a quarter of the thickness window, so the global thickness hides small relief at distance.
  • Use a blurred height: sample the height texture a second time with a mip bias of +1 to +2 for Depth Bias only. Sharp per-pixel height makes stripes along the light direction. Keep the fine detail in the normal map.
  • Nodes that leave Depth Bias unconnected get the default 1, so they keep their real depth.

Landscape​

The Landscape Layer Blend node blends material attributes, so each layer's height can ride along in Opacity:

  1. For every layer, add a SetMaterialAttributes node with an Opacity pin between the layer function and the blend: layer Result → MaterialAttributes, layer Heightmap → Opacity, output → Layer <Name>. Keep the Height <Name> inputs as they are.
  2. After the blend, BreakMaterialAttributes › Opacity → XFSDK Light Depth Bias.
  3. XFSDK Light Opacity and SubsurfaceColor → MakeMaterialAttributes.
  4. For water or puddle layers, feed a constant 1 into Opacity so they stay flat.

The blended height uses exactly the same weights as the layer colors.

Debug views​

xl_sss_debug_modeShows
1Edge mask. Use it to tune xl_sss_bilinear_threshold.
2Shadow mask only (grey scale). The right view for judging shadow shape.
3Material thickness. 0.5 grey = default, black = does not cast.
4Depth bias. White = rendered surface, black = full range behind it.

Tips​

  • Judge shadows in the lit view, not only in debug mode 2. Bend Studio's own notes say striping can look strong in the raw mask and disappear in the lit image.
  • Stripes always run along the screen-space light direction. They come from noisy depth: lower the depth bias range, blur the height, raise xl_sss_min_depth_delta to 0.1–0.2, or lower xl_sss_shadow_contrast to 2.
  • A very low sun exaggerates everything. Test at 30–45° sun elevation too.

References​