Implementing Ward Anisotropic Specular in GLSL

This article provides the mathematical formula and practical GLSL implementation for the Ward anisotropic specular reflection model. By evaluating surface roughness across two independent orthogonal tangent axes, the Ward model reproduces the elongated, directional specular highlights characteristic of brushed metals, hair, and woven textiles.

The Ward Anisotropic Formula

The Ward anisotropic specular distribution calculates highlight intensity using the surface normal (\(N\)), the unit tangent (\(T\)), the unit bitangent (\(B\)), the view direction (\(V\)), the light direction (\(L\)), and the half-vector (\(H = \frac{L + V}{\|L + V\|}\)). Surface roughness is defined independently along the tangent and bitangent axes by \(\alpha_x\) and \(\alpha_y\).

The specular reflectance term \(f_r\) is defined as:

\[f_r = \frac{1}{4 \pi \alpha_x \alpha_y \sqrt{(N \cdot L)(N \cdot V)}} \exp\left( - \frac{\left( \frac{H \cdot T}{\alpha_x} \right)^2 + \left( \frac{H \cdot B}{\alpha_y} \right)^2}{(H \cdot N)^2} \right)\]

Where:

  • \(N \cdot L\) and \(N \cdot V\) are the clamped dot products of the normal with the light and view vectors.
  • \(H \cdot N\) is the dot product of the half-vector and normal.
  • \(H \cdot T\) and \(H \cdot B\) represent the projection of the half-vector onto the surface's tangent and bitangent axes.
  • \(\alpha_x\) and \(\alpha_y\) are the standard deviations of the surface slope along \(T\) and \(B\), controlling highlight spread.

GLSL Implementation

Below is the GLSL function implementing the Ward anisotropic specular model:

float wardAnisotropicSpecular(
    vec3 N,         // Normalized surface normal
    vec3 T,         // Normalized tangent vector
    vec3 B,         // Normalized bitangent vector
    vec3 L,         // Normalized vector pointing toward the light
    vec3 V,         // Normalized vector pointing toward the viewer
    float alphaX,   // Roughness along tangent (must be > 0.0)
    float alphaY    // Roughness along bitangent (must be > 0.0)
) {
    vec3 H = normalize(L + V);

    float NdotL = max(dot(N, L), 0.0001);
    float NdotV = max(dot(N, V), 0.0001);
    float NdotH = max(dot(N, H), 0.0001);

    float HdotT = dot(H, T);
    float HdotB = dot(H, B);

    // Compute the exponent term
    float exponentNumerator = pow(HdotT / alphaX, 2.0) + pow(HdotB / alphaY, 2.0);
    float exponent = -exponentNumerator / (NdotH * NdotH);

    // Compute the normalization denominator
    const float PI = 3.14159265359;
    float denominator = 4.0 * PI * alphaX * alphaY * sqrt(NdotL * NdotV);

    return exp(exponent) / denominator;
}

The function clamps dot products to a minimal threshold (0.0001) to prevent division by zero at grazing angles. The calculated scalar output can then be multiplied by light intensity, specular albedo, and \(N \cdot L\) to produce the final lit specular component.