Implementing Distance and Altitude Fog in GLSL

This article explains how to compute realistic aerial perspective fog in GLSL as a coupled function of distance and altitude. By modeling an atmosphere where particle density decays exponentially with height, you can analytically integrate atmospheric extinction along any view ray without expensive numerical raymarching. The following sections break down the underlying mathematical formulation, provide an optimized GLSL implementation, and detail how to integrate the resulting transmittance into your lighting pipeline.

The Mathematical Model

Standard distance fog assumes constant atmospheric density, yielding an extinction factor determined solely by distance (\(d\)):

\[\tau = \sigma \cdot d\]

Real-world atmospheres, however, decrease in density as altitude increases. Assuming an exponential distribution, atmospheric density \(\rho\) at height \(y\) is defined as:

\[\rho(y) = \rho_0 \cdot e^{-b \cdot y}\]

where:

  • \(\rho_0\) is the base density at sea level (\(y = 0\)).
  • \(b\) is the height falloff coefficient (controlling how rapidly density drops with altitude).

To evaluate fog between the camera position \(\mathbf{C} = (C_x, C_y, C_z)\) and a surface point \(\mathbf{P} = (P_x, P_y, P_z)\), calculate the optical depth \(\tau\) along the ray \(\mathbf{r}(t) = \mathbf{C} + t\mathbf{v}\), where \(\mathbf{v} = \mathbf{P} - \mathbf{C}\) and \(t \in [0, 1]\):

\[\tau = \int_0^1 \rho(C_y + t \cdot v_y) \cdot \|\mathbf{v}\| \, dt = \|\mathbf{v}\| \cdot \rho_0 \cdot e^{-b \cdot C_y} \int_0^1 e^{-b \cdot v_y \cdot t} \, dt\]

Evaluating this integral yields the closed-form equation:

\[\tau = \|\mathbf{v}\| \cdot \rho_0 \cdot e^{-b \cdot C_y} \cdot \left( \frac{1 - e^{-b \cdot v_y}}{b \cdot v_y} \right)\]

When the ray is completely horizontal (\(v_y \approx 0\)), the fraction approaches \(1\), simplifying the optical depth to:

\[\tau = \|\mathbf{v}\| \cdot \rho_0 \cdot e^{-b \cdot C_y}\]

Once \(\tau\) is determined, the transmittance \(T\) (the fraction of light reaching the camera from the surface) follows the Beer-Lambert law:

\[T = e^{-\tau}\]

The fog blend factor is then \(1.0 - T\).

GLSL Implementation

Below is a GLSL function that computes the fog transmittance and inscattering blend factor given the camera position, world-space fragment position, and atmospheric parameters.

// Computes aerial perspective fog factor
// Returns a value between 0.0 (no fog) and 1.0 (full fog)
float evaluateFog(
    vec3 cameraPos,
    vec3 fragPos,
    float fogDensity,     // Base density at y = 0
    float heightFalloff   // Controls exponential decrease with height
) {
    vec3 viewDir = fragPos - cameraPos;
    float dist = length(viewDir);
    float deltaY = viewDir.y;

    // Optical depth base evaluation
    float cameraDensity = fogDensity * exp(-heightFalloff * cameraPos.y);
    float opticalDepth;

    // Handle near-zero vertical delta to avoid division by zero
    if (abs(deltaY) < 0.0001) {
        opticalDepth = dist * cameraDensity;
    } else {
        float heightFactor = (1.0 - exp(-heightFalloff * deltaY)) / (heightFalloff * deltaY);
        opticalDepth = dist * cameraDensity * heightFactor;
    }

    // Transmittance via Beer-Lambert law
    float transmittance = exp(-opticalDepth);

    return clamp(1.0 - transmittance, 0.0, 1.0);
}

Applying Fog in the Fragment Shader

In your fragment shader, apply the evaluated fog factor by blending the shaded geometry color with the ambient atmospheric color:

void main() {
    vec3 surfaceColor = computeLighting(); // Standard PBR/Lambertian shading
    vec3 fogColor = vec3(0.6, 0.7, 0.85);   // Atmospheric haze color

    // Fog configuration
    float fogBaseDensity = 0.005;
    float fogHeightFalloff = 0.05;

    float fogFactor = evaluateFog(u_CameraPosition, v_WorldPosition, fogBaseDensity, fogHeightFalloff);

    vec3 finalColor = mix(surfaceColor, fogColor, fogFactor);
    fragColor = vec4(finalColor, 1.0);
}

Directional Inscattering (Sun Glow)

To account for directional sunlight scattering (Mie scattering), modify fogColor by taking the dot product between the normalized view vector and the directional light vector:

vec3 viewDirNorm = normalize(v_WorldPosition - u_CameraPosition);
float sunDot = max(dot(viewDirNorm, u_SunDirection), 0.0);
vec3 sunGlow = u_SunColor * pow(sunDot, 8.0) * 0.5;

vec3 finalFogColor = fogColor + sunGlow;
vec3 finalColor = mix(surfaceColor, finalFogColor, fogFactor);

This method produces deep valleys shrouded in heavy ground fog, clear mountain peaks at higher altitudes, and realistic optical attenuation across long viewing distances without sampling textures or executing iterative loops.