Skip to content

Depth Bias for Shadow Mapping

depth_bias.png

This example shows how to use depth bias (also called "polygon offset") to solve z-fighting and shadow acne artifacts that occur when rendering shadows or coplanar geometry. Depth bias applies a small offset to fragment depth values during rasterization, ensuring consistent depth test results without modifying vertex positions. This is essential for shadow mapping, decals, and overlaid geometry.

The example uses the KDGpuExample helper API for simplified setup.

Overview

What this example demonstrates:

  • Configuring depth bias in pipeline rasterization state
  • Using biasConstantFactor for constant depth offset
  • Using biasSlopeFactor for slope-dependent offset
  • Eliminating z-fighting between coplanar surfaces
  • Common depth bias values for shadow mapping

Use cases:

  • Shadow mapping (eliminate shadow acne)
  • Decals on surfaces (prevent z-fighting)
  • Overlaid geometry (coplanar triangles)
  • CSG operations (constructive solid geometry)

Vulkan Requirements

  • Vulkan Version: 1.0+
  • Extensions: None (depth bias is core)
  • Features: depthBias (enabled by default on all devices)

Key Concepts

Z-Fighting:

When two surfaces occupy the same depth, floating-point precision errors cause flickering as the GPU can't consistently determine which is in front:

1
2
3
Surface A at z=5.0000001
Surface B at z=5.0000002
 Depth buffer precision ~0.0001  flickering!

This is "z-fighting" and is especially common with:

  • Shadow maps (shadow receiver and occluder at same depth)
  • Decals projected onto surfaces
  • Coplanar geometry (overlapping triangles)

Depth Bias:

Depth bias adds an offset to fragment depth during rasterization, before depth testing:

1
2
finalDepth = fragmentDepth + bias
bias = constantFactor + (slopeFactor × maxDepthSlope)
  • constantFactor: Fixed offset in depth units
  • slopeFactor: Offset proportional to surface slope (steeper = more bias)
  • clamp: Maximum bias magnitude (prevents excessive offset)

Spec: https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/VkPipelineRasterizationStateCreateInfo.html

Shadow Acne:

In shadow mapping, shadow acne appears as incorrect self-shadowing:

1
2
3
1. Render depth from light perspective  shadow map
2. Render scene, compare fragment depth to shadow map
3. Precision errors cause surface to shadow itself  acne!

Depth bias pushes shadow map depth slightly away from light, preventing false self-shadowing.

Choosing Bias Values:

Too little bias → Shadow acne remains Too much bias → "Peter Panning" (shadows detach from objects)

Typical values:

  • constantFactor: 1.0 to 4.0 (depth units)
  • slopeFactor: 1.0 to 2.0 (slope-dependent)
  • clamp: 0.0 (disabled) or small positive value

Slope factor is crucial for surfaces at grazing angles to light.

Implementation

Pipeline with Depth Bias:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
    // Create pipelines for front and back triangles
    m_pipelineFront = m_device.createGraphicsPipeline(GraphicsPipelineOptions{
            .label = "TriangleFront",
            .shaderStages = {
                    { .shaderModule = vertexShader, .stage = ShaderStageFlagBits::VertexBit },
                    { .shaderModule = fragmentShader, .stage = ShaderStageFlagBits::FragmentBit },
            },
            .layout = m_pipelineLayout,
            .vertex = {
                    .buffers = {
                            { .binding = 0, .stride = sizeof(Vertex) },
                    },
                    .attributes = {
                            { .location = 0, .binding = 0, .format = Format::R32G32B32_SFLOAT }, // Position
                            { .location = 1, .binding = 0, .format = Format::R32G32B32_SFLOAT, .offset = sizeof(glm::vec3) } // Color
                    },
            },
            .renderTargets = { { .format = m_swapchainFormat } },
            .depthStencil = {
                    .format = m_depthFormat,
                    .depthWritesEnabled = true,
                    .depthCompareOperation = CompareOperation::LessOrEqual,
            },
    });
    m_pipelineBack = m_device.createGraphicsPipeline(GraphicsPipelineOptions{
            .label = "TriangleBack",
            .shaderStages = {
                    { .shaderModule = vertexShader, .stage = ShaderStageFlagBits::VertexBit },
                    { .shaderModule = fragmentShader, .stage = ShaderStageFlagBits::FragmentBit },
            },
            .layout = m_pipelineLayout,
            .vertex = {
                    .buffers = {
                            { .binding = 0, .stride = sizeof(Vertex) },
                    },
                    .attributes = {
                            { .location = 0, .binding = 0, .format = Format::R32G32B32_SFLOAT }, // Position
                            { .location = 1, .binding = 0, .format = Format::R32G32B32_SFLOAT, .offset = sizeof(glm::vec3) } // Color
                    },
            },
            .renderTargets = { { .format = m_swapchainFormat } },
            .depthStencil = {
                    .format = m_depthFormat,
                    .depthWritesEnabled = true,
                    .depthCompareOperation = CompareOperation::LessOrEqual,
            },
            .primitive = {
                    .depthBias{
                            .enabled = true,
                            .biasConstantFactor = 1.0f,
                    },
            },
    });

Filename: depth_bias/depth_bias.cpp

Key configuration:

  • depthBiasEnable = true: Enables depth bias
  • biasConstantFactor: Constant depth offset
  • biasSlopeFactor: Slope-dependent offset (0 if not needed)
  • biasClamp: Maximum bias magnitude

The biased pipeline ensures consistent depth ordering.

Rendering Coplanar Geometry:

In this example:

  1. Triangle 1 rendered with no depth bias (standard pipeline)
  2. Triangle 2 rendered with depth bias (pushed forward/back)

Result: No z-fighting, consistent depth ordering.

For Shadow Mapping:

1
2
3
4
5
6
7
8
// When rendering shadow map:
 .primitive = {
        .depthBias{
                .enabled = true,
                .biasConstantFactor = 1.5f,
                .biasSlopFactor = 1.75f,
        },
},

This pushes shadow map depth away from light, preventing self-shadowing.

Performance Notes

Cost:

  • Negligible GPU overhead (computed during rasterization)
  • No extra memory or bandwidth
  • Fully hardware-accelerated

Depth Precision:

  • Depth bias doesn't reduce precision
  • Careful tuning needed for wide depth ranges
  • Consider reverse-Z for better precision distribution

Dynamic Bias:

  • Can't change bias per-draw without pipeline switch
  • Alternative: Use dynamic state extension (VK_EXT_extended_dynamic_state2)
  • Or pre-create pipelines with different bias values

See Also

Further Reading


Updated on 2026-03-31 at 00:02:07 +0000