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:

  1. Azimuthal Difference: \[\cos(\phi_i - \phi_r) \sin \theta_i \sin \theta_r = (L \cdot V) - (N \cdot L)(N \cdot V)\]

  2. 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\)).