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 Studio | XFSDK Light | |
|---|---|---|
| Where the shadow goes | Into the sun's shadow term, before lighting | Applied to scene color after lighting (post process) |
| Thickness | One global value | Global, plus an optional per-material thickness |
| Depth bias | Stored in the GBuffer, added to the depth buffer for all systems | Stored in Opacity, used by these shadows only |
| Lights | Sun | Most 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 variable | Default | Description |
|---|---|---|
xl_sss_enable | 0 | Turns the effect on. |
xl_sss_surface_thickness | 0.05 | Thickness 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_count | 60 | Shadow length in pixels (16–120). Cost grows with it. |
xl_sss_shadow_strength | 0.5 | 0 no effect, 1 full black. |
xl_sss_shadow_contrast | 4.0 | Sharpness of the shadow edge (1–8). |
xl_sss_bilinear_threshold | 0.02 | Depth difference treated as an edge, where samples are not blended. Scale it with the thickness. |
xl_sss_ignore_edge_pixels | 0 | 1: edge pixels don't cast. Helps on flat surfaces at grazing angles, thins foliage shadows. |
xl_sss_min_depth_delta | 0.05 | Casters closer than this fraction of a thickness window don't shadow. Removes striping in tiny crevices. |
xl_sss_max_distance | 5000 | Distance (cm) where shadows have fully faded. |
xl_sss_fade_ratio | 0.6 | Fraction of the max distance where the fade starts. |
xl_sss_multi_light | 0 | 0 most important light only, 1 up to 4 lights (more expensive). |
xl_sss_use_early_out | 1 | Skips 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 node | C++ function | Description |
|---|---|---|
| Set Bend SSS Enabled | XFSDK_BendSSS_SetEnabled | Enables or disables Bend SSS. |
| Set All Significant Lights Enabled | XFSDK_BendSSS_SetAllSignificantLightsEnabled | Enables shadows for the significant point and spot lights selected by the renderer, up to four lights. |
| Set Shadow Mode | XFSDK_BendSSS_SetShadowMode | Selects the shadow-composition mode. |
| Set Shadow Strength | XFSDK_BendSSS_SetShadowStrength | Sets shadow strength in the inclusive range 0.0–1.0. |
| Set Shadow Contrast | XFSDK_BendSSS_SetShadowContrast | Sets shadow contrast in the inclusive range 1.0–8.0. |
| Set Shadow Desaturation | XFSDK_BendSSS_SetShadowDesaturation | Sets 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
};
| Mode | Description |
|---|---|
Darken | Composites the screen-space shadow by darkening the scene color. Shadow desaturation applies in this mode. |
RemoveDirectLight | Removes 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_mode | Result |
|---|---|
0 | Darken (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. |
1 | Remove 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
- In the level editor toolbar, open the XFSDK Light menu and choose Patch Engine Shaders.
- 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.
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
- Set the material's Shading Model to Preintegrated Skin.
- Add the XFSDK Light material function node.
- Connect its SubsurfaceColor output to Subsurface Color and its Opacity output to Opacity.
| Node input | Default | Meaning |
|---|---|---|
| Shadow Thickness | 0.5 | 0.5 = the global thickness. 0 and 1 = 16× thinner and 16× thicker (xl_sss_material_thickness_range, in stops). |
| Behaviour | 1 | 1 = casts shadows, 0 = only receives. |
| Depth Bias (Height Map) | 1 | Height 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 Thickness | Multiplier | At 10 m (default global 0.05) | Good for |
|---|---|---|---|
0.25 | ×¼ | ~12 cm | Flat ground with depth bias |
0.375 | ×½ | ~25 cm | Rocks, debris |
0.5 | ×1 | ~50 cm | Most props |
0.625 | ×2 | ~1 m | Grass and bush clumps |
0.75 | ×4 | ~2 m | Very 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_rangeto 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:
- For every layer, add a SetMaterialAttributes node with an Opacity pin between the layer function and the
blend: layer
Result→MaterialAttributes, layerHeightmap→Opacity, output →Layer <Name>. Keep theHeight <Name>inputs as they are. - After the blend, BreakMaterialAttributes ›
Opacity→ XFSDK Light Depth Bias. - XFSDK Light
OpacityandSubsurfaceColor→ MakeMaterialAttributes. - 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_mode | Shows |
|---|---|
1 | Edge mask. Use it to tune xl_sss_bilinear_threshold. |
2 | Shadow mask only (grey scale). The right view for judging shadow shape. |
3 | Material thickness. 0.5 grey = default, black = does not cast. |
4 | Depth 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_deltato 0.1–0.2, or lowerxl_sss_shadow_contrastto 2. - A very low sun exaggerates everything. Test at 30–45° sun elevation too.
References
- G. Aldridge. Screen Space Shadows. Sony Bend Studio, SIGGRAPH 2023. Code and slides: bendstudio.com/siggraph