How to Use SV_Depth in DirectX Pixel Shaders

This article provides an overview of how the SV_Depth system-value semantic operates within DirectX High-Level Shader Language (HLSL) to override hardware-calculated depth values. By binding this semantic to a pixel shader output, developers can programmatically alter the depth buffer on a per-pixel basis, allowing for custom occlusion, volumetric effects, and geometric depth manipulation beyond standard rasterizer interpolation.

The Role of SV_Depth in HLSL

In a standard DirectX rendering pipeline, depth is calculated automatically during rasterization. The vertex shader outputs the position in clip space, and the hardware interpolates the \(z\) and \(w\) coordinates across the primitive to compute a depth value (\(z/w\)) for each pixel. This default value is then tested against and written to the depth-stencil buffer.

The SV_Depth semantic allows a pixel shader to intervene in this process by explicitly writing a custom floating-point value to the depth buffer instead of using the hardware-generated value.

Implementation in the Pixel Shader

To use SV_Depth, you define it as an output attribute within the pixel shader's return structure or as an out parameter.

struct PixelOutput
{
    float4 Color : SV_Target;
    float  Depth : SV_Depth;
};

PixelOutput main(PixelInput input)
{
    PixelOutput output;
    output.Color = float4(1.0, 0.0, 0.0, 1.0);
    
    // Explicitly modify the depth value (must be in the range [0.0, 1.0])
    output.Depth = input.CustomDepth; 

    return output;
}

The value written to SV_Depth must fall within the normalized viewport depth range, typically \([0.0, 1.0]\) in DirectX (representing the near and far clipping planes). Any value written outside this range is clamped or can cause clipping artifacts depending on the depth-stencil state configuration.

Performance Impact: Early-Z Disabling

Under normal conditions, modern GPUs utilize an optimization known as Early-Z, running the depth test before the pixel shader executes. If a pixel is occluded by previously rendered geometry, the GPU discards it immediately, saving the computational cost of running the pixel shader.

When a pixel shader outputs to SV_Depth, the hardware can no longer predict the final depth value prior to execution. As a result:

  1. Early-Z is disabled: The GPU must execute the pixel shader for every rasterized fragment to discover its final depth.
  2. Fill rate decreases: Scenes with high overdraw will experience a performance penalty because occluded pixels are fully shaded before depth rejection can occur.

Preserving Optimizations with Conservative Depth

To mitigate the performance loss associated with disabling Early-Z, DirectX 11 and later introduced conservative depth semantics. These semantics allow the developer to guarantee directional bounds on the modified depth, allowing the GPU to keep certain Early-Z optimizations active:

  • SV_DepthGreaterEqual: Asserts that the shader will only output a depth value greater than or equal to the hardware-generated rasterizer depth. If the original depth already fails the depth test, the pixel can be discarded early.
  • SV_DepthLessEqual: Asserts that the shader will only output a depth value less than or equal to the hardware-generated rasterizer depth.

Practical Use Cases

Explicit depth modification via SV_Depth is commonly employed in advanced graphics techniques:

  • Raymarching and Signed Distance Fields (SDFs): When rendering procedural surfaces inside a bounding proxy box, SV_Depth is used to write the exact intersection point calculated by the raymarcher into the depth buffer, ensuring correct interaction with standard rasterized objects.
  • Impostors and Billboards: Volumetric particles or spherical impostors can write reconstructed analytical depths so they clip and blend realistically against surrounding geometry.
  • Decals and Intersecting Geometry: Shaders can smooth out seams or adjust depth discrepancies dynamically to eliminate Z-fighting.