Shader Story
October 1, 2025 · View on GitHub
Lighting Models : Minnaert
The Minnaert Model was originally designed to replicate the way light scatters off the surface of the moon. That is why its also called the Moon Shader.
Unlike Lambertian lighting, which assumes light scatters evenly in all directions, Minnaert introduces a view-dependent component. This makes it ideal for simulating materials where light tends to backscatter, such as:
- Porous surfaces: dust, chalk
- Fibrous materials: velvet, velour, suede, carpets
- Soft, rough surfaces: cloth, unpolished stone
The result is a shading effect where surfaces facing the viewer look slightly darker or lighter depending on the chosen roughness parameter. It produces a soft, velvety look that’s very close to the more complex Oren-Nayar model, but cheaper to compute.
Visual demo
// Pseudo-code (Complete per-vertex and per-pixel implementations, with detailed comments, are available in the related Patreon post)
// Link: https://www.patreon.com/posts/shader-story-139479113
// World-space directions
float3 viewDirection = normalize(_WorldSpaceCameraPos.xyz - i.posWorld.xyz);
// Lambertian cosine falloff
float NdotL = max(0.0, dot(normalDirection, lightDirection));
float NdotV = max(0.0, dot(normalDirection, viewDirection));
// Minnaert factor (view-dependent diffuse)
float minnaertFactor = saturate(NdotL pow(NdotL NdotV, _Roughness));
// Apply diffuse color
float3 lightingModel = minnaertFactor * diffuseColor;
// Light attenuation and final output
float attenuation = LIGHT_ATTENUATION(i);
float3 attenColor = attenuation * _LightColor0.rgb;
half4 finalColor = half4(lightingModel * attenColor, 1.0);
Complete per-vertex and per-pixel implementations, with detailed comments, are available on Patreon.
Basic idea
Light travels from the source, hits the surface, and reaches the viewer.
- Diffuse (Lambertian): Compute the cosine falloff (dot(N, L)) to measure how directly light hits the surface.
- View Dependency: Compute dot(N, V) to measure how directly the viewer looks at the surface.
- Minnaert Adjustment: Multiply dot(N, L) by (dot(N, L) x dot(N, V))^k to darken grazing angles based on the chosen roughness k.
- Final Shading: Multiply the Minnaert factor by the surface diffuse color, light color, and attenuation to get the final shaded result.
Minnaert Equation
d = Σ (from i=0 to n) [ I_i x atten_i x diffuseFactor x clamp( (max(0.0, dot(N, L_i)))^(1.0 + k) x (max(0.0, dot(N, V)))^k , 0.0, 1.0 ) ]
Where:
I_i - per-light intensity & color atten_i - per-light attenuation (distance/shadows) N - surface normal L_i - light direction V - view direction k - Minnaert exponent (roughness):
- k=0 - plain Lambertian
- k>0 - darkens edges
🔗 Related Functions
❤️ Support Shader Story
If this article helped you, consider supporting the project on Patreon - you'll get access to the related source files, reference cheat-sheets, and other exclusive resources:
Your support helps keep this library open, growing, and free for everyone.