Implementing Minnaert Limb Darkening in GLSL
This article provides an implementation guide for the Minnaert limb-darkening reflectance model used to simulate astronomical surfaces in real-time computer graphics. It covers the mathematical formulation of the Minnaert equation, explains the physical significance of its parameters for planetary bodies, and presents an optimized OpenGL Shading Language (GLSL) fragment shader implementation designed to avoid common rendering artifacts.
Understanding the Minnaert Model
Standard Lambertian reflectance assumes a completely diffuse surface that scatters incoming light equally in all directions, yielding uniform brightness across a sphere regardless of viewing angle. Astronomical surfaces rarely behave as ideal Lambertian reflectors. Rocky airless bodies (such as the Moon) exhibit limb brightening or retro-reflection, while gas giants (such as Jupiter or Uranus) and stars exhibit limb darkening due to atmospheric absorption and scattering depth.
Marcel Minnaert introduced a phenomenological reflectance model that adapts the Lambertian cosine law to account for these photometric effects. The standard Minnaert equation is defined as:
\[R = \rho \cdot (N \cdot L)^k \cdot (N \cdot V)^{k - 1}\]
Where:
- \(N\) is the normalized surface normal vector.
- \(L\) is the normalized direction vector pointing toward the light source.
- \(V\) is the normalized direction vector pointing toward the observer/camera.
- \(\rho\) is the surface albedo.
- \(k\) is the Minnaert limb
parameter:
- When \(k = 1.0\), the model reduces to pure Lambertian reflectance (\(N \cdot L\)).
- When \(k < 1.0\), the surface exhibits limb brightening, common on rough, porous regolith like that of the Moon or asteroids (typically \(k \approx 0.5\) to \(0.7\)).
- When \(k > 1.0\), the surface exhibits limb darkening, typical for dense atmospheres and gas giants (typically \(k \approx 1.2\) to \(2.0\)).
Numerical Stability Considerations in Shaders
Directly translating \((N \cdot V)^{k -
1}\) into GLSL can cause floating-point errors. When \(k < 1.0\), the exponent \((k - 1.0)\) becomes negative. As the
viewing angle approaches the silhouette edge of the planet, \(N \cdot V\) approaches zero, leading to
division by zero or pow(0.0, negative_value), which
produces NaN or Inf artifacts on the geometry
edges.
To ensure numerical stability, clamp dot products using a small
positive epsilon (e.g., 0.001) and ensure that back-facing
lighting (\(N \cdot L \le 0\)) drops to
zero smoothly.
GLSL Implementation
Below is a complete GLSL function implementing the Minnaert reflectance model:
/**
* Calculates Minnaert reflectance for astronomical bodies.
*
* @param normal Normalized surface normal (N)
* @param lightDir Normalized vector toward light source (L)
* @param viewDir Normalized vector toward viewer (V)
* @param k Minnaert limb parameter (k < 1: brightening, k > 1: darkening)
* @return Reflectance intensity scalar
*/
float calculateMinnaert(vec3 normal, vec3 lightDir, vec3 viewDir, float k) {
float nDotL = dot(normal, lightDir);
// Discard unlit surfaces to prevent light leaking
if (nDotL <= 0.0) {
return 0.0;
}
// Prevent division by zero or NaN at glancing angles
const float EPSILON = 0.001;
float nDotV = max(dot(normal, viewDir), EPSILON);
// Minnaert formulation: (N.L)^k * (N.V)^(k - 1)
float minnaertFactor = pow(nDotL, k) * pow(nDotV, k - 1.0);
return minnaertFactor;
}Integration in a Fragment Shader
To use this function in an astronomical rendering pipeline, combine the Minnaert intensity with the planetary surface texture and light source color:
#version 330 core
in vec3 vNormal;
in vec3 vPosition;
in vec2 vTexCoord;
out vec4 fragColor;
uniform sampler2D uAlbedoMap;
uniform vec3 uLightPosition;
uniform vec3 uCameraPosition;
uniform vec3 uLightColor;
uniform float uMinnaertK; // e.g., 0.65 for the Moon, 1.4 for Jupiter
void main() {
vec3 N = normalize(vNormal);
vec3 L = normalize(uLightPosition - vPosition);
vec3 V = normalize(uCameraPosition - vPosition);
vec4 albedo = texture(uAlbedoMap, vTexCoord);
float intensity = calculateMinnaert(N, L, V, uMinnaertK);
vec3 finalColor = albedo.rgb * uLightColor * intensity;
fragColor = vec4(finalColor, albedo.a);
}This shader allows dynamic tuning of the parameter
uMinnaertK, enabling a single shader to accurately depict
both rough, rocky terrestrial moons and smooth, scattering planetary
atmospheres.