Fix Planetary Z-Fighting with Logarithmic Depth GLSL
In large-scale 3D simulations such as space engines and planetary rendering, standard perspective depth buffers suffer from severe floating-point precision loss at extreme distances, resulting in visible Z-fighting. Implementing a logarithmic depth buffer redistributes depth values evenly across orders of magnitude rather than clustering precision almost entirely at the near clipping plane. This article details the mathematical formulation of a logarithmic depth buffer and demonstrates how to implement it directly inside a GLSL vertex shader to eliminate depth conflict across astronomical distances.
The Planetary Scale Z-Fighting Problem
In a standard perspective projection, the hardware depth buffer maps view-space depth non-linearly using a reciprocal function (\(1/z\)). While this yields sub-millimeter precision close to the near plane, precision drops off exponentially as distance increases.
When rendering planets, the near plane must accommodate a surface observer (e.g., \(0.1\) meters), while the far plane must extend to encompass orbital distances (e.g., \(10^{10}\) meters). Under standard 24-bit floating-point depth, distant terrain meshes, atmosphere hulls, and orbital tracks that are kilometers apart map to identical depth values, producing flickering artifacts known as Z-fighting.
Mathematical Formulation
A logarithmic depth buffer replaces the standard reciprocal distribution with a logarithmic curve, allocating precision uniformly per decade of distance.
The general mapping function scales distance \(z\) between the near and far planes into normalized device coordinates (NDC):
\[z_{\text{log}} = \frac{\ln(C \cdot w + 1.0)}{\ln(C \cdot z_{\text{far}} + 1.0)}\]
Where:
- \(w\) is the camera-space depth
(represented by
gl_Position.wafter standard model-view-projection transformation). - \(z_{\text{far}}\) is the camera's far clipping distance.
- \(C\) is a constant controlling the distribution slope, commonly set between \(1.0\) and small constants depending on near-plane precision requirements.
Because the GPU hardware automatically performs the perspective divide (\(z_{\text{ndc}} = z_{\text{clip}} / w_{\text{clip}}\)) following vertex shading, the vertex shader must output a modified \(z_{\text{clip}}\) value that results in the logarithmic value after this division:
\[z_{\text{clip}} = (2.0 \cdot z_{\text{log}} - 1.0) \cdot w_{\text{clip}}\]
For systems running in OpenGL's default \([-1, 1]\) NDC depth range, the equation transforms to:
\[z_{\text{clip}} = \left( \frac{2.0}{\ln(C \cdot z_{\text{far}} + 1.0)} \cdot \ln(C \cdot w_{\text{clip}} + 1.0) - 1.0 \right) \cdot w_{\text{clip}}\]
To optimize runtime execution, the constant terms can be precomputed on the CPU and passed as a single uniform:
\[F_{\text{coef}} = \frac{2.0}{\ln(C \cdot z_{\text{far}} + 1.0)}\]
This simplifies the vertex-stage formulation to:
\[z_{\text{clip}} = (F_{\text{coef}} \cdot \ln(C \cdot w_{\text{clip}} + 1.0) - 1.0) \cdot w_{\text{clip}}\]
GLSL Vertex Shader Implementation
The following GLSL vertex shader demonstrates the integration of the logarithmic depth formula:
#version 330 core
layout(location = 0) in vec3 aPos;
uniform mat4 u_ModelViewMatrix;
uniform mat4 u_ProjectionMatrix;
// Precomputed uniform: 2.0 / log(C * far + 1.0)
uniform float u_Fcoef;
const float C = 1.0;
void main()
{
// Standard perspective projection
vec4 clipPos = u_ProjectionMatrix * u_ModelViewMatrix * vec4(aPos, 1.0);
// Apply the logarithmic depth formulation to gl_Position.z
clipPos.z = (u_Fcoef * log(C * clipPos.w + 1.0) - 1.0) * clipPos.w;
gl_Position = clipPos;
}Vertex Shader Considerations and Geometry Density
Modifying gl_Position.z exclusively in the vertex shader
relies on the GPU's hardware interpolator across polygon faces. Because
screen-space rasterization interpolates depth linearly rather than
logarithmically, rendering large triangles that cover broad spans of
depth can cause curved surface distortion or clipping artifacts.
For planetary bodies and orbital systems, this formulation works
effectively when geometry is sufficiently tessellated, such as in
quadtree-based terrain meshes, continuous level-of-detail (CDLOD)
spheres, or clipmaps. If low-poly geometry spanning massive depth
variations must be drawn without subdivision, the logarithmic equation
must instead be written to gl_FragDepth inside the fragment
shader.