Beer-Lambert Water Extinction in GLSL

This article explains how to implement the Beer-Lambert law in GLSL to simulate wavelength-dependent light extinction through water. You will learn the physical principles governing light absorption and scattering in participating media, how RGB channels approximate continuous light wavelengths, and the exact GLSL code required to calculate realistic underwater color attenuation.

The Beer-Lambert Law Formula

The Beer-Lambert law describes the exponential attenuation of light as it travels through a participating medium. In its fundamental form, the transmitted radiance \(I\) depends on the incident radiance \(I_0\), the distance traveled \(d\), and the medium's extinction coefficient \(\beta\):

\[I = I_0 \cdot e^{-\beta \cdot d}\]

The extinction coefficient \(\beta\) (often denoted as \(\sigma_t\)) is the sum of the absorption coefficient (\(\sigma_a\)) and the scattering coefficient (\(\sigma_s\)):

\[\beta = \sigma_a + \sigma_s\]

Because liquid water absorbs and scatters different wavelengths at varying rates, \(\beta\) varies across the electromagnetic spectrum. Red wavelengths (approx. 650–700 nm) are absorbed rapidly near the surface, while blue and green wavelengths (approx. 400–550 nm) penetrate significantly deeper.

Mapping Wavelengths to RGB in GLSL

Real-world extinction occurs across a continuous spectrum. In standard real-time rendering pipelines, this spectrum is approximated using three discrete extinction coefficients corresponding to the red, green, and blue color primaries:

  • Red: High attenuation (\(\beta_r \approx 0.22 - 0.50 \text{ m}^{-1}\))
  • Green: Moderate attenuation (\(\beta_g \approx 0.05 - 0.10 \text{ m}^{-1}\))
  • Blue: Low attenuation (\(\beta_b \approx 0.01 - 0.04 \text{ m}^{-1}\))

Because GLSL natively supports vector math, the exponential decay for all three channels can be calculated simultaneously.

GLSL Implementation

To implement wavelength-dependent extinction, calculate the transmittance vector across the path length and multiply it by the scene's linear radiance.

// Calculates the transmitted light through water using the Beer-Lambert Law
vec3 applyBeerLambert(vec3 initialColor, float distanceInMeters, vec3 extinctionCoeff) {
    // Compute transmittance per channel: exp(-beta * distance)
    vec3 transmittance = exp(-extinctionCoeff * distanceInMeters);
    
    // Attenuate the initial color
    return initialColor * transmittance;
}

Complete Fragment Shader Example

In a deferred or post-processing water shader, the distance represents the travel length of the primary view ray through the water volume (between the water surface and the submerged scene geometry):

#version 330 core

in vec2 vTexCoord;
out vec4 fragColor;

uniform sampler2D uSceneColor;
uniform sampler2D uSceneDepth;
uniform sampler2D uWaterDepth;

// Typical values for clear oceanic water (in 1/meters)
const vec3 EXTINCTION_COEFFICIENT = vec3(0.35, 0.06, 0.02);

float linearizeDepth(float depth, float near, float far) {
    return (2.0 * near) / (far + near - depth * (far - near));
}

void main() {
    vec3 sceneColor = texture(uSceneColor, vTexCoord).rgb;
    
    // Convert depth values to world/view distance
    float sceneDist = linearizeDepth(texture(uSceneDepth, vTexCoord).r, 0.1, 1000.0);
    float waterDist = linearizeDepth(texture(uWaterDepth, vTexCoord).r, 0.1, 1000.0);
    
    // Determine path length through the water
    float opticalPathLength = max(0.0, sceneDist - waterDist);
    
    // Apply wavelength-dependent extinction
    vec3 attenuatedColor = applyBeerLambert(sceneColor, opticalPathLength, EXTINCTION_COEFFICIENT);
    
    fragColor = vec4(attenuatedColor, 1.0);
}

Practical Considerations

  • Linear Color Space: The exponential math must be evaluated in linear color space. Applying the Beer-Lambert law directly to sRGB textures produces physically inaccurate color shifts and overly aggressive darkening.
  • Scale Calibration: The extinction coefficients depend directly on the scale of your engine's world units. If 1 unit equals 1 meter, standard coefficients work out of the box. If 1 unit equals 1 centimeter or 1 foot, scale the coefficient vector accordingly.
  • In-Scattering: The Beer-Lambert law accounts purely for out-scattering and absorption (loss of light). For fully realistic water, combine this calculation with an in-scattering (ambient water tint/fog) term that increases with distance:
vec3 totalLight = sceneColor * transmittance + waterAmbientColor * (1.0 - transmittance);