Fast Subsurface Scattering with DirectX Compute
Subsurface scattering (SSS) is a critical lighting phenomenon that simulates how light penetrates translucent materials, scatters internally, and exits at different surface points. This article breaks down the physical fundamentals of subsurface scattering, explains why brute-force evaluation is prohibitive in real-time rendering, and outlines an efficient implementation strategy using DirectX compute shaders. By leveraging screen-space approximations, separable diffusion kernels, and optimized compute passes, developers can achieve high-fidelity rendering of skin, wax, and marble at real-time frame rates.
Understanding Subsurface Scattering
Standard surface rendering uses a Bidirectional Reflectance Distribution Function (BRDF), which assumes that light enters and exits a surface at the exact same point. While this approximation works well for opaque materials like metals or plastics, it fails completely for translucent materials such as human skin, jade, foliage, and liquids.
Subsurface scattering requires a Bidirectional Scattering-Surface Reflectance Distribution Function (BSSRDF). The BSSRDF models light entering at point \(x_i\), undergoing multiple scattering events within the medium, and exiting at a distinct location \(x_o\). Without SSS, objects like human faces appear hard, dry, and plastic-like because the light does not soften across shadow boundaries or produce the subtle warm glows characteristic of internal diffusion.
The Real-Time Challenge: Screen-Space SSS
Full volumetric simulation using path tracing accurately resolves the BSSRDF by tracing millions of random light paths through a volume. However, this is too computationally expensive for real-time graphics.
To overcome this, modern real-time engines utilize Screen-Space Subsurface Scattering (SSSS). This technique approximates subsurface scattering as a post-processing filter applied to the screen-space diffuse lighting buffer. The core realization is that the diffusion profile—the spatial distribution of scattered light—can be modeled mathematically using a sum of Gaussians or a normalized diffusion profile.
Structuring the DirectX Compute Pipeline
Compute shaders in DirectX (Direct3D 11 and Direct3D 12) provide the
ideal framework for SSSS because they grant fine-grained control over
thread scheduling, texture sampling, and fast on-chip memory access via
groupshared memory.
An efficient compute-based SSS pipeline is structured into three primary stages:
1. Lighting Separation
Before scattering, the scene must be rendered with specular and diffuse lighting separated:
- Specular Component: Specular reflection occurs strictly at the surface interface (Fresnel reflection) and must not be blurred.
- Diffuse Component: Diffuse light penetrates the material and is written to a dedicated offscreen render target to be scattered.
- Linear Depth & Normals: Rendered to a G-Buffer to guide the bilateral filtering and prevent bleeding across geometric edges.
2. Separable Blur via Two Compute Passes
A 2D radial blur kernel is computationally expensive, requiring \(O(N^2)\) texture reads per pixel. Because diffusion profiles can be decomposed into a sum of separable 1D Gaussians, the 2D filter can be split into two 1D passes:
- Pass A (Horizontal Compute Pass): Reads the diffuse buffer, computes horizontal scattering, and writes to an intermediate texture.
- Pass B (Vertical Compute Pass): Reads the intermediate texture, computes vertical scattering, and writes the final scattered diffuse result.
This reduces complexity from \(O(N^2)\) to \(O(2N)\) samples per pixel.
3. Depth-Aware (Bilateral) Filtering
To ensure that light does not bleed across disocclusions or between disconnected objects, the compute shader must evaluate depth differences between the center pixel and surrounding sample points:
float depthDifference = abs(centerLinearDepth - sampleLinearDepth);
float depthWeight = saturate(1.0 - (depthDifference * depthSensitivity));
The computed depthWeight scales the diffusion kernel
weight for that sample, discarding samples that belong to the background
or separate surfaces.
DirectX Compute Optimization Techniques
To maximize throughput across compute passes on modern graphics hardware, several critical optimizations should be integrated:
Utilizing groupshared
Memory
Rather than having every thread issue high-latency texture reads to
global VRAM, threads within a compute thread group can cooperatively
load a tile of pixels—including a border region (apron) to accommodate
the filter radius—into groupshared memory.
Once the GroupMemoryBarrierWithGroupSync() instruction
is executed, all subsequent filter taps are read directly from fast
on-chip shared memory, drastically reducing memory bandwidth
pressure.
Wave Intrinsics (DirectX 12 / SM6.0+)
If target hardware supports Shader Model 6.0 or higher, lane-swapping
intrinsics like WaveReadLaneAt can be used to share data
across threads within a SIMD wave without routing through
groupshared memory. This eliminates barrier synchronization
stalls and optimizes register allocation.
Half-Resolution Execution with Bilateral Upsampling
Because SSS functions as a low-pass filter, the horizontal and vertical compute passes can be executed at half resolution (\(1/4\) the total pixel count). After the vertical scattering pass completes, a bilateral upsampling compute pass reconstructs the full-resolution image using high-resolution linear depth and normal buffers to preserve crisp geometric silhouettes.
Material Masking and Early Exits
Not all pixels on the screen require subsurface scattering. In Direct3D 12, developers can generate an indirect dispatch buffer containing only the tiles or dispatch counts that contain SSS-flagged materials. Alternatively, the compute shader can sample a material ID buffer at the tile or wave level to immediately early-exit threads covering non-scattering geometry.
Final Recombination
Once the horizontal and vertical compute passes finish scattering the diffuse light, a final recombine pass blends the scattered diffuse color, the untouched specular reflections, ambient occlusion, and emissive elements. The result is a physically plausible, soft material appearance calculated in less than a single millisecond of GPU frame time.