Oren-Nayar Diffuse Model in GLSL
This article provides an overview of the Oren-Nayar diffuse reflectance model, detailing its mathematical derivation for rough surfaces and providing an optimized implementation in OpenGL Shading Language (GLSL). Unlike ideal Lambertian models, Oren-Nayar accounts for microscopic roughness, making it essential for realistic rendering of materials like concrete, sand, plaster, and cloth.
The Mathematical Formulation
The Oren-Nayar model extends Lambertian reflectance by assuming a surface is composed of microscopic, V-shaped symmetrical facets distributed according to a Gaussian distribution with standard deviation (roughness) \(\sigma\).
The radiance \(L_r\) reflected toward view direction \(V\) from light direction \(L\) on a surface with normal \(N\) is defined as:
\[L_r = \frac{\rho}{\pi} E_0 \cos \theta_i \left( A + B \cdot \max(0, \cos(\phi_i - \phi_r)) \cdot \sin \alpha \tan \beta \right)\]
Where:
- \(\rho\): Diffuse albedo of the surface.
- \(E_0\): Illuminance of the incident light.
- \(\theta_i\): Angle between the light vector \(L\) and normal \(N\) (\(\cos \theta_i = N \cdot L\)).
- \(\theta_r\): Angle between the view vector \(V\) and normal \(N\) (\(\cos \theta_r = N \cdot V\)).
- \(\phi_i - \phi_r\): Difference in azimuthal angles between \(L\) and \(V\) projected on the surface plane.
- \(\alpha = \max(\theta_i, \theta_r)\)
- \(\beta = \min(\theta_i, \theta_r)\)
The constants \(A\) and \(B\) are calculated using the roughness parameter \(\sigma\) (in radians):
\[A = 1.0 - 0.5 \frac{\sigma^2}{\sigma^2 + 0.33}\]
\[B = 0.45 \frac{\sigma^2}{\sigma^2 + 0.09}\]
Vector-Based Optimization
Calculating explicit trigonometric functions (\(\arccos\), \(\sin\), \(\tan\)) in shaders is computationally expensive. Using vector projections, the inter-reflection terms can be simplified:
Azimuthal Difference: \[\cos(\phi_i - \phi_r) \sin \theta_i \sin \theta_r = (L \cdot V) - (N \cdot L)(N \cdot V)\]
Combined Angular Term: Since \(\sin \alpha \sin \beta = \sin \theta_i \sin \theta_r\) and \(\cos \beta = \max(\cos \theta_i, \cos \theta_r)\): \[\cos(\phi_i - \phi_r) \sin \alpha \tan \beta = \frac{(L \cdot V) - (N \cdot L)(N \cdot V)}{\max(N \cdot L, N \cdot V)}\]
This eliminates the need for inverse trigonometric calculations, allowing the model to execute entirely via dot products and basic arithmetic.
GLSL Implementation
Below is a GLSL function that implements the optimized Oren-Nayar formulation:
vec3 orenNayarDiffuse(
vec3 lightDir, // Normalized light direction vector (L)
vec3 viewDir, // Normalized view direction vector (V)
vec3 normal, // Normalized surface normal (N)
float roughness, // Surface roughness (sigma in radians, [0, 1])
vec3 albedo // Surface albedo color (rho)
) {
float NdotL = dot(normal, lightDir);
float NdotV = dot(normal, viewDir);
// If surface faces away from either light or camera, return no light
if (NdotL <= 0.0 || NdotV <= 0.0) {
return vec3(0.0);
}
float LdotV = dot(lightDir, viewDir);
// Calculate roughness coefficients
float sigma2 = roughness * roughness;
float A = 1.0 - 0.5 * (sigma2 / (sigma2 + 0.33));
float B = 0.45 * (sigma2 / (sigma2 + 0.09));
// Vector projection for azimuthal component
float s = LdotV - NdotL * NdotV;
float t = (s > 0.0) ? max(NdotL, NdotV) : 1.0;
// Calculate Oren-Nayar reflectance term
float orenNayar = NdotL * (A + B * max(0.0, s) / t);
return albedo * orenNayar;
}When roughness is set to 0.0, \(B\) evaluates to 0.0 and \(A\) evaluates to 1.0,
seamlessly reducing the output to standard Lambertian reflectance (\(N \cdot L\)).