Charlie Sheen BRDF for Cloth Shading in GLSL

This article provides an overview and technical breakdown of the Charlie sheen distribution, an essential microfacet BRDF model designed to simulate the soft grazing highlights found on cloth, velvet, and fuzzy surfaces. It covers the core mathematical equations for the Normal Distribution Function (NDF) and shadowing-masking (visibility) term, followed by an optimized, drop-in implementation written in GLSL.

Background

Traditional microfacet specular distributions like GGX prioritize sharp specular reflections characteristic of hard surfaces. Cloth and velvet fibers instead catch light at grazing incident angles, yielding an inverted specular response. Introduced by Alejandro Conty Estevez and Christopher Kulla at Sony Pictures Imageworks (2017), the Charlie sheen distribution uses an inverted sinusoidal normal distribution to mimic this behavior.

Mathematical Formulation

The standard microfacet specular formulation is given by:

\[f(\mathbf{l}, \mathbf{v}) = \frac{D(\mathbf{h}) \, V(\mathbf{l}, \mathbf{v}) \, F(\mathbf{v}, \mathbf{h})}{4 (\mathbf{n} \cdot \mathbf{l}) (\mathbf{n} \cdot \mathbf{v})}\]

When defining a height-correlated visibility term \(V(\mathbf{l}, \mathbf{v}) = \frac{G(\mathbf{l}, \mathbf{v}, \mathbf{h})}{4 (\mathbf{n} \cdot \mathbf{l}) (\mathbf{n} \cdot \mathbf{v})}\), the equation simplifies to:

\[f(\mathbf{l}, \mathbf{v}) = D(\mathbf{h}) \, V(\mathbf{l}, \mathbf{v}) \, F(\mathbf{v}, \mathbf{h})\]

Where:

  • \(\mathbf{n}\) is the macroscopic surface normal.
  • \(\mathbf{v}\) is the view direction.
  • \(\mathbf{l}\) is the light direction.
  • \(\mathbf{h} = \frac{\mathbf{v} + \mathbf{l}}{\|\mathbf{v} + \mathbf{l}\|}\) is the half-vector.

1. Normal Distribution Function (NDF)

The Charlie distribution relies on a sine-based microfacet distribution:

\[D_{\text{Charlie}}(\mathbf{h}) = \frac{2 + \frac{1}{\alpha}}{2\pi} \sin(\theta_h)^{\frac{1}{\alpha}}\]

Expressing \(\sin(\theta_h)\) using the dot product \(\mathbf{n} \cdot \mathbf{h}\):

\[\sin(\theta_h) = \sqrt{1 - (\mathbf{n} \cdot \mathbf{h})^2}\]

Substituting this identity gives:

\[D_{\text{Charlie}}(\mathbf{n} \cdot \mathbf{h}, \alpha) = \frac{2 + \frac{1}{\alpha}}{2\pi} \left(1 - (\mathbf{n} \cdot \mathbf{h})^2\right)^{\frac{1}{2\alpha}}\]

Here, \(\alpha \in (0, 1]\) represents the perceptual sheen roughness.

2. Visibility and Shadowing-Masking Term

To satisfy energy conservation, Estevez and Kulla derived a tailored shadowing function \(\Lambda(\nu)\). For real-time applications, two approaches are standard:

Option A: Neubelt and Pettineo Approximation

For high performance, game engines often approximate the visibility term using the Neubelt formulation:

\[V_{\text{Neubelt}}(\mathbf{n} \cdot \mathbf{v}, \mathbf{n} \cdot \mathbf{l}) = \frac{1}{4 \left( (\mathbf{n} \cdot \mathbf{l}) + (\mathbf{n} \cdot \mathbf{v}) - (\mathbf{n} \cdot \mathbf{l})(\mathbf{n} \cdot \mathbf{v}) \right)}\]

Option B: Estevez-Kulla Rational Visibility

The analytical visibility function derived for Charlie uses:

\[V_{\text{Charlie}}(\mathbf{l}, \mathbf{v}) = \frac{1}{\left(1 + \Lambda(\mathbf{n} \cdot \mathbf{v}) + \Lambda(\mathbf{n} \cdot \mathbf{l})\right) (4 (\mathbf{n} \cdot \mathbf{v}) (\mathbf{n} \cdot \mathbf{l}))}\]

Where \(\Lambda(x)\) is approximated numerically:

\[\Lambda(x) \approx \begin{cases} \exp\left(-\frac{0.0054238 x}{1 + 0.35415 x} + \frac{0.14488}{\alpha} - 0.44304\right) & \text{for small } \alpha \\ \text{rational polynomial fit} & \text{otherwise} \end{cases}\]

GLSL Implementation

Below is a complete, production-ready implementation in GLSL combining the Charlie NDF with an efficient visibility approximation.

#define PI 3.14159265359

// Normal Distribution Function: Charlie
float D_Charlie(float NoH, float roughness) {
    // Clamp roughness to prevent numerical instability
    float alpha = max(roughness * roughness, 0.000064);
    float invAlpha = 1.0 / alpha;
    
    // sin^2(theta) = 1 - cos^2(theta)
    float sin2h = max(1.0 - NoH * NoH, 0.0001);
    
    // (2 + 1/alpha) * sin(theta)^(1/alpha) / (2 * PI)
    return (2.0 + invAlpha) * pow(sin2h, invAlpha * 0.5) / (2.0 * PI);
}

// Approximate Sheen Visibility (Neubelt)
float V_Sheen(float NoV, float NoL) {
    return 1.0 / (4.0 * (NoL + NoV - NoL * NoV));
}

// Evaluates the full Charlie Sheen specular component
vec3 EvaluateCharlieSheen(
    vec3 N, 
    vec3 V, 
    vec3 L, 
    float roughness, 
    vec3 sheenColor
) {
    vec3 H = normalize(V + L);

    float NoV = clamp(dot(N, V), 1e-5, 1.0);
    float NoL = clamp(dot(N, L), 1e-5, 1.0);
    float NoH = clamp(dot(N, H), 0.0, 1.0);

    float D = D_Charlie(NoH, roughness);
    float V_term = V_Sheen(NoV, NoL);

    // Sheen is often evaluated with Fresnel = 1.0 (sheenColor handles tint)
    return sheenColor * (D * V_term);
}

Usage Notes

  • Alpha Parameterization: Roughness is mapped as \(\alpha = \text{roughness}^2\) to linearize perceptual control.
  • Energy Balancing: When integrating sheen into a standard PBR model, the base layer (diffuse and specular) should be scaled by \(1.0 - E_{\text{sheen}}\) to preserve energy conservation.