1.12 Depth Testing

In this section, we'll explore depth testing, a crucial technique for resolving rendering issues such as inverted inside surfaces. Depth testing enables us to determine the visual order of fragments from front to back, ensuring that only the fragments closest to the camera are drawn while discarding those behind them.

Launch Playground - 1_12_depth

In viewport coordinates, the z-axis represents fragment depth. Sorting fragments based on their z-values is essential for accurate rendering.

To achieve this, we introduce an additional buffer known as the depth buffer or z-buffer. When outputting values from the fragment shader, we not only assign colors to fragments but also write the current depth value into the depth buffer. The depth buffer stores only the smallest z-value (frontmost) for each fragment position.

For example, consider a fragment at position (300, 400) in the current framebuffer with a depth of 0.5. If a new fragment at the same location has a depth of 0.6, it's discarded as it's further away. Conversely, a new fragment with a depth of 0.4 is considered closer and replaces the old one. This ensures that only the foremost fragments are displayed.

Depth testing behavior is configurable; we can choose to keep fragments with larger depth values and discard smaller ones. However, preserving the frontmost fragment is typically the preferred behavior.

Let's update our code to enable depth testing. Fortunately, the shader code remains unchanged as writing to the depth buffer is handled automatically when the pipeline is configured correctly.

const pipelineDesc = {
    layout,
    vertex: {
        module: shaderModule,
        entryPoint: 'vs_main',
        buffers: [positionBufferLayoutDesc]
    },
    fragment: {
        module: shaderModule,
        entryPoint: 'fs_main',
        targets: [colorState]
    },
    primitive: {
        topology: 'triangle-strip',
        frontFace: 'ccw',
        cullMode: 'none'
    },
    depthStencil: {
        depthWriteEnabled: true,
        depthCompare: 'less',
        format: 'depth24plus-stencil8'
    }
};

In the pipeline descriptor, we introduce a new depthStencil section. We set depthWriteEnabled to true to write depth values to the depth buffer. The depthCompare is set to less, keeping fragments with lesser depth values and discarding those with greater depth. You can experiment with different behaviors, such as preserving fragments with greater depth.

We specify the format as depth24plus-stencil8, allocating 24 bits for depth values and 8 bits for stencil values. We'll discuss stencil in a future chapter; for now, we'll focus on depth.

To complete our setup, we need to create a depth buffer:

const depthTextureDesc = {
    size: [canvas.width, canvas.height, 1],
    dimension: '2d',
    format: 'depth24plus-stencil8',
    usage: GPUTextureUsage.RENDER_ATTACHMENT 
};

let depthTexture = device.createTexture(depthTextureDesc);
let depthTextureView = depthTexture.createView();

Creating the depth buffer is similar to creating a texture map. We match the depth buffer's size to the canvas, set the dimension to 2d, and use the same depth24plus-stencil8 format. We specify GPUTextureUsage.RENDER_ATTACHMENT as the usage since it serves as a rendering target, even though depth buffers are typically not visible.

const depthAttachment = {
    view: depthTextureView,
    depthClearValue: 1,
    depthLoadOp: 'clear',
    depthStoreOp: 'store',
    stencilClearValue: 0,
    stencilLoadOp: 'clear',
    stencilStoreOp: 'store'
};

const renderPassDesc = {
    colorAttachments: [colorAttachment],
    depthStencilAttachment: depthAttachment
};

Finally, we define the depthAttachment and add it to the renderPass. Similar to the colorAttachment, the depthAttachment specifies behavior for loading and storing depth buffer values. We clear the depth buffer to 1 at load time (depthClearValue), as 1 is the maximum possible depth value in viewport coordinates. This effectively sets the background depth. During storing (depthStoreOp), we simply store the incoming value based on the 'less' behavior. Stencil settings can be ignored for now.

In the renderPass, we now include a depthStencilAttachment. With these updates, our code correctly handles occlusion, ensuring front surfaces occlude back surfaces for more accurate rendering.

Depth Test Fixes the Artifact of the Previous Example
Depth Test Fixes the Artifact of the Previous Example

Leave a Comment on Github