Depth testing
Since Coordinate Systems a depth buffer has been quietly keeping the far side of every cube behind its near side. We created it, cleared it every frame and otherwise ignored it. This chapter looks at what it actually stores, how the test that uses it works, and why the numbers in it are less evenly spread than you might expect.
The
// A depth buffer the size of the window: 24 bits of depth, 8 of stencil.
depth_desc: D3D11_TEXTURE2D_DESC = {
.Width = cast(scr_width, UINT),
.Height = cast(scr_height, UINT),
.MipLevels = 1,
.ArraySize = 1,
.Format = DXGI_FORMAT.DXGI_FORMAT_D24_UNORM_S8_UINT,
.SampleDesc = { .Count = 1 },
.BindFlags = D3D11_BIND_DEPTH_STENCIL,
};
depth_texture: *ID3D11Texture2D = null;
depth_dsv: *ID3D11DepthStencilView = null;
ID3D11Device_CreateTexture2D(device, depth_desc&, null, depth_texture&);
ID3D11Device_CreateDepthStencilView(device, cast(depth_texture, *ID3D11Resource), null, depth_dsv&);
In OpenGL the windowing library makes this buffer for you; in Direct3D it is an ordinary texture that we create ourselves. DXGI_FORMAT_D24_UNORM_S8_UINT packs 24 bits of depth, stored as a fixed-point fraction from 0 to 1, with 8 bits of stencil that the next chapter uses. 24 bits is the common choice; DXGI_FORMAT_D32_FLOAT gives a 32-bit float instead. To be drawn into, the texture gets a
ID3D11DeviceContext_OMSetRenderTargets(context, 1, backbuffer_rtv&, depth_dsv);
ID3D11DeviceContext_RSSetViewports(context, 1, viewport&);
ID3D11DeviceContext_ClearRenderTargetView(context, backbuffer_rtv, clear_color[0]&);
ID3D11DeviceContext_ClearDepthStencilView(context, depth_dsv, D3D11_CLEAR_DEPTH | D3D11_CLEAR_STENCIL, 1.0, 0);
Forgetting the clear has the effect you would guess: last frame's depths stay, and this frame's pixels are tested against surfaces that are no longer there.
With a depth buffer bound, the
SV_Depth turns it off, and one that discards pixels with
Unlike OpenGL, where depth testing is off until glEnable(GL_DEPTH_TEST), Direct3D has it on by default. What controls it is a
A depth-stencil state is made from a D3D11_DEPTH_STENCIL_DESC. Its depth part has three fields, here as the stencil lesson sets them:
.DepthEnable = depth,
.DepthWriteMask = D3D11_DEPTH_WRITE_MASK.D3D11_DEPTH_WRITE_MASK_ALL,
.DepthFunc = D3D11_COMPARISON_FUNC.D3D11_COMPARISON_LESS,
DepthEnable turns the test on or off. DepthWriteMask says whether passing pixels update the buffer: D3D11_DEPTH_WRITE_MASK_ZERO keeps testing but stops writing, a read-only depth buffer, which is what glDepthMask(GL_FALSE) does. That is useful for things like particles, which should be hidden by walls without hiding each other. DepthFunc is the comparison.
Depth test function
The comparison decides which pixels survive. It is written "new depth function stored depth", and Direct3D offers the same eight as OpenGL's
| Function | The pixel passes when |
|---|---|
| D3D11_COMPARISON_NEVER | never |
| D3D11_COMPARISON_LESS | its depth is less than the stored depth (the default) |
| D3D11_COMPARISON_EQUAL | its depth equals the stored depth |
| D3D11_COMPARISON_LESS_EQUAL | its depth is less than or equal to the stored depth |
| D3D11_COMPARISON_GREATER | its depth is greater than the stored depth |
| D3D11_COMPARISON_NOT_EQUAL | its depth differs from the stored depth |
| D3D11_COMPARISON_GREATER_EQUAL | its depth is greater than or equal to the stored depth |
| D3D11_COMPARISON_ALWAYS | always |
LESS is "keep whatever is nearer", which is what we want nearly all the time. To see what the test buys us, imagine a state with ALWAYS:
desc.DepthEnable = 1;
desc.DepthWriteMask = D3D11_DEPTH_WRITE_MASK_ALL;
desc.DepthFunc = D3D11_COMPARISON_ALWAYS; // every pixel passes
Every pixel passes, so whatever is drawn last ends up on top, which is the same as having no depth test. Our scene draws the floor first and then the two cubes, so with ALWAYS the cubes would still cover the floor. Within each cube, though, the faces would cover each other in the order they appear in the vertex array rather than by distance, and some far faces would show through near ones. Try it in the exercises.
Depth value precision
The depth buffer holds values from 0 to 1, but the distances in our scene run from the near plane to the far plane, here 0.1 to 100. Something maps one onto the other. The obvious mapping would be linear:
\[ F_{depth} = \frac{z - near}{far - near} \]where \(z\) is the distance from the camera. Equal steps in distance would then be equal steps in depth, as in LearnOpenGL's graph (drawn for near = 1 and far = 50):
Depth buffers practically never work that way. A perspective projection divides by distance (that is what makes far things small), and the depth it produces is divided too. What ends up in the buffer is proportional to \(1/z\):
\[ F_{depth} = \frac{1/z - 1/near}{1/far - 1/near} \]
This is exactly the depth Direct3D stores. We can check it against our projection matrix in includes/math.rin:
[10] = z_far / (z_near - z_far),
[11] = z_near * z_far / (z_near - z_far),
[14] = -1.0,
For a point at distance \(z\) in front of the camera (view-space z equal to \(-z\), since the camera looks down -z), row 2 gives clip-space z and row 3 gives w:
\[ z_{clip} = \frac{far\,(z - near)}{far - near}, \qquad w = z, \qquad depth = \frac{z_{clip}}{w} = \frac{far}{far - near}\left(1 - \frac{near}{z}\right) \]which is 0 at the near plane, 1 at the far plane, and, with a little algebra, the same as the formula above. OpenGL's projection produces the same curve over -1 to 1 and then maps it to 0 to 1 when writing the buffer; Direct3D's lands in 0 to 1 directly, which is one reason the D3D version of the formulas below is slightly simpler.
LearnOpenGL's graph shows how lopsided this is. With our near of 0.1 and far of 100 it is more extreme still. Half of the whole range, 0 to 0.5, is used up by the first tenth of a unit past the near plane (depth reaches 0.5 at a distance of about 0.2). One unit away the depth is already 0.90, two units away 0.95, ten units away 0.99. Everything from ten units to a hundred shares the last one percent.
That sounds wasteful, but it is roughly what we want: things close to the camera are big on screen and need fine depth distinctions, while far things are small and need fewer. The catch is that the precision far away can run out, which we come back to with z-fighting.
Visualizing the depth buffer
The lesson shows the depth directly. In a pixel shader, the SV_Position input is not a clip-space position any more; it is the pixel itself: x and y in pixels from the top-left corner (OpenGL's gl_FragCoord counts from the bottom-left), and z the depth about to be tested. So the shader can return that z as a grey:
float4 ps_main(vs_out p) : SV_Target
{
// In a pixel shader SV_Position is the pixel: x and y in pixels, z the
// depth about to be tested. GL calls it gl_FragCoord.
float depth = p.position.z;
if (mode == 1) return float4(depth.xxx, 1.0);
if (mode == 2) return float4((linear_depth(depth) / 10.0).xxx, 1.0);
return albedo.Sample(linear_wrap, p.uv);
}
The mode comes from a small constant buffer in b1, together with the near and far planes:
cbuffer view_mode : register(b1)
{
int mode;
float near_plane;
float far_plane;
};
view_constants: struct = {
mode: i32; // 0 textured, 1 raw depth, 2 linear depth
near_plane: f32;
far_plane: f32;
pad: f32;
}
The padding makes the struct 16 bytes, one HLSL register, as constant buffers require. Tab steps through the three modes; the program starts in mode 2, the linear view, and Tab goes on to the textured scene and then the raw depth:
if (key_pressed(VK_TAB)) { mode = (mode + 1) % 3; }
Here is the raw depth, mode 1, beside the ordinary scene:


Nearly everything is white. Given the numbers in the previous section, that is no surprise: the camera starts about five units from the cubes, where the depth is around 0.98. Only the bit of floor right in front of the camera is a shade darker. Fly very close to a cube and it will turn grey and then black, within a fraction of a unit. (The background is the clear colour, since nothing was drawn there.)
To see real distances we undo the projection. Solving the depth formula for \(z\):
\[ z = \frac{near \cdot far}{far - depth\,(far - near)} \]which in HLSL is
// The eye-space distance a 0..1 depth came from: the projection, undone.
float linear_depth(float depth)
{
return near_plane * far_plane / (far_plane - depth * (far_plane - near_plane));
}
LearnOpenGL's version first turns the depth back into a -1 to 1 value, z * 2.0 - 1.0, because that is the range OpenGL's projection produced. Ours skips that step, since Direct3D's projection already produced 0 to 1. Copying the GLSL formula as it is would give wrong distances, a typical small trap when porting.
The result is a distance in world units, from 0.1 to 100. The shader divides it by 10 to fit a range that suits this small scene: black at the camera, white from ten units on. LearnOpenGL divides by the far plane, which leaves its picture nearly black.
Now the floor fades evenly from dark near the camera to white in the distance, and the cubes, about five units away, are a middle grey. Move around and the greys change in proportion to how far you move. The full program is here, and its shader here.
Z-fighting
When two surfaces lie in the same plane, or so close that the depth buffer cannot tell them apart, neither one consistently wins the depth test. Which one shows flips from pixel to pixel and from frame to frame as the camera moves, in jagged stripes. This is called
Our scene has a case of it built in. The cubes are 1 unit tall and centred at y = 0, so their bottoms are at y = -0.5, exactly the height of the floor. From outside you never see a cube's bottom face, but you can from inside. The Face culling lesson starts out removing the cubes' front faces, which opens them up, and its first frame shows the fight on their floors:
The stripes at the bottom of each cube are the metal floor and the cube's own bottom face taking turns. Z-fighting gets worse with distance, because, as we saw, precision drops off the farther a surface is.
Prevent z-fighting
It cannot be removed entirely, but a few habits keep it away:
- Do not put surfaces on top of each other. Lifting our cubes by a hundredth of a unit would end the fight here and nobody would see the gap. This is the most effective fix, but it has to be done object by object.
- Push the near plane out. Precision is concentrated just past the near plane, so moving it from 0.1 to, say, 0.5 spreads much more of it over the rest of the scene. Too far and nearby objects get clipped, so it is a matter of tuning per scene.
- Use more bits. DXGI_FORMAT_D32_FLOAT (or D32_FLOAT_S8X24_UINT with stencil) has more precision than a 24-bit buffer, at some cost in memory and bandwidth.
glClipControl, since its -1 to 1 range spoils the trick.
Additional resources
- Configuring depth-stencil functionality: Microsoft's guide to depth-stencil textures, views and states.
- Depth Precision Visualized: Nathan Reed's illustrated explanation of depth precision and reversed-Z.
- OpenGL Projection Matrix: Song Ho Ahn's derivation of the projection, including where the \(1/z\) comes from (for OpenGL's -1 to 1 range).
Exercises
- Create a depth-stencil state with DepthFunc set to D3D11_COMPARISON_ALWAYS, bind it with
OMSetDepthStencilState before drawing, and look at the cubes from different sides. Which faces show through, and why those? - Change the near plane in
m4_perspective (and in near_plane) from 0.1 to 1.0, then look at the raw depth view again. How close can you get to a cube before it is clipped? - Get rid of the z-fighting in the face culling lesson by lifting the cubes slightly. How small a lift still works when you fly far away?