Skip to content
Rendering · Depth & blendingIntermediate

Depth buffer

How the GPU hides what is behind, why depth precision runs out far away, and how effects read depth back.

Live demo coming soon

How it works

Triangles reach the GPU in whatever order the draw calls send them. If each one simply painted over the last, a wall drawn after the character standing in front of it would cover the character. Sorting every triangle by distance doesn’t work either: two triangles can cross each other, each partly in front. The depth buffer solves it one pixel at a time.

The depth test

Next to the color image, the framebuffer keeps a second image of the same size: one depth value per pixel (per sample, with MSAA), cleared to the farthest value at the start of the frame. For every fragment, the depth test compares the fragment’s depth with the stored one. With the usual gl.depthFunc(gl.LESS), a nearer fragment passes: its color is written and its depth replaces the stored one. A farther one fails and changes nothing. Whatever the draw order, each pixel ends up showing the nearest surface.

gl.enable(gl.DEPTH_TEST);
gl.depthFunc(gl.LESS);           // keep the nearer fragment
gl.clearDepth(1.0);              // 1.0 is the far plane
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

The test is one of the per-sample operations, so it is configured from the host, not in the shader. gl.depthMask(false) keeps the test but stops the writes, which is how transparent surfaces are drawn. On a WebGL2 canvas the depth buffer exists unless the context is created with depth: false.

Where the numbers come from

The depth stored is the fragment’s z after the perspective divide and the viewport transform, from 0 at the near plane to 1 at the far plane. You can read it in the fragment shader as gl_FragCoord.z.

That value is not proportional to distance. A perspective projection makes it follow 1 / distance, so most of the range is spent close to the camera. With the near plane at 0.1 and the far plane at 1000, about 90% of the values describe the first meter; everything past 200 meters shares the top 0.04%. Turning it back into a distance, for an OpenGL-style projection:

// window depth (0 near, 1 far) → distance from the camera plane, in scene units
float viewDistance(float depth, float near, float far) {
    float zNdc = depth * 2.0 - 1.0;
    return 2.0 * near * far / (far + near - zNdc * (far - near));
}

Z-fighting

When two surfaces are closer together than the depth precision at that distance, their depths round to the same values and the winner changes from pixel to pixel and frame to frame: a flickering, striped mix called z-fighting. The fixes, from most to least effective:

  • Move the near plane out. Precision depends mostly on the ratio far / near, and the near plane dominates it: 0.1 to 1 multiplies the precision far away by about ten.
  • Don’t build coplanar geometry. A decal or a road on terrain is better lifted slightly than placed exactly on the surface.
  • Offset the depth. gl.polygonOffset() with gl.POLYGON_OFFSET_FILL nudges a draw’s depth toward the camera so it wins ties.
  • Use more bits. A 32-bit float depth buffer (DEPTH_COMPONENT32F) instead of the common 24-bit one helps, most of all combined with a reversed range (1 at the near plane), which some engines use.

Reading depth in an effect

A fragment shader can’t read the depth buffer it is drawing into. Effects that need the scene’s depth (fog, soft particles, outlines from depth edges, depth of field) render the scene first, keep its depth in a texture, and read that texture in a later full-screen pass, converting each sample to a distance as above.

In Godot, that texture is a uniform declared with a hint: uniform sampler2D depth_texture : hint_depth_texture;. Godot’s values use its own convention, a reversed range, so the shader converts them with the inverse projection matrix before treating them as distances. Its render_mode options depth_draw_opaque, depth_draw_always, depth_draw_never and depth_test_disabled control the write and the test.

On this site, a post pass that reads "depth" calls sceneDepth(uv), which already returns the distance from the camera plane in world units, on the web and in Godot alike.

Further reading