Clear Coat Specular Over Rough Metal in GLSL

This article details the mathematical formulation and GLSL implementation for rendering a dual-layer material consisting of an uncolored, smooth clear coat layer over a rough metallic base. It covers the underlying microfacet BRDF mechanics, Fresnel transmission attenuation for physical energy conservation, and an optimized shader implementation tailored for modern real-time rendering pipelines.


Layering Theory and Energy Conservation

A clear coat model simulates a thin, transparent dielectric layer (such as automotive lacquer or varnish) applied over an underlying substrate. When applied over a rough metal, the system comprises two distinct specular interactions:

  1. Clear Coat Layer (Top): A smooth dielectric interface with an index of refraction typically around \(1.5\), yielding an uncolored normal-incidence reflectance \(F_0 \approx 0.04\).
  2. Base Metal Layer (Bottom): A rough conductive interface characterized by complex refractive indices, yielding a chromatic base reflectance \(F_0\) (derived from the base color) and high microfacet roughness.

When light enters this medium, a portion is reflected specularly by the clear coat interface according to the Fresnel equations. The remaining fraction \((1 - F_c)\) penetrates the clear coat, strikes the base metal, reflects, and passes back out through the clear coat interface. Because the clear coat is assumed to be thin and non-absorbing, absorption within the layer is neglected.

The composite specular BRDF \(f_r(\mathbf{v}, \mathbf{l})\) is defined as:

\[f_r(\mathbf{v}, \mathbf{l}) = f_{\text{base}}(\mathbf{v}, \mathbf{l}) \cdot (1 - F_c) \cdot \text{clearCoat} + f_{\text{cc}}(\mathbf{v}, \mathbf{l}) \cdot \text{clearCoat}\]

Where:

  • \(f_{\text{cc}}\) is the microfacet BRDF of the clear coat.
  • \(f_{\text{base}}\) is the microfacet BRDF of the rough metal.
  • \(F_c\) is the Fresnel reflection term of the top interface, usually evaluated as \(F_c = F_{\text{Schlick}}(0.04, 1.0, \mathbf{v} \cdot \mathbf{h})\).
  • \(\text{clearCoat}\) is a scalar \([0, 1]\) controlling the layer's strength.

Component Formulations

Both the base and the clear coat layers are evaluated using the standard Cook-Torrance microfacet specular formulation:

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

where \(V(\mathbf{v}, \mathbf{l}) = \frac{G(\mathbf{v}, \mathbf{l}, \mathbf{h})}{4 (\mathbf{n} \cdot \mathbf{v}) (\mathbf{n} \cdot \mathbf{l})}\) is the visibility function.

1. Clear Coat Term (\(f_{\text{cc}}\))

  • Normal Distribution (\(D_{cc}\)): Commonly modeled using standard GGX or GGX with a fixed low roughness. Disney and Filament frequently use the GTR1 (Berry) formulation for clear coat, but anisotropic/standard GGX with roughness clamped to \([0.001, 0.1]\) is widely used: \[D_{cc}(\mathbf{h}) = \frac{\alpha_{cc}^2}{\pi \left( (\mathbf{n} \cdot \mathbf{h})^2 (\alpha_{cc}^2 - 1) + 1 \right)^2}\]
  • Fresnel (\(F_{cc}\)): Evaluated with a fixed dielectric \(F_0 = 0.04\): \[F_{cc} = 0.04 + (1.0 - 0.04)(1.0 - (\mathbf{v} \cdot \mathbf{h}))^5\]
  • Visibility (\(V_{cc}\)): To minimize computation, Kelemen-Szirmay-Kalos visibility is often used for the clear coat: \[V_{cc} = \frac{1}{4 (\mathbf{v} \cdot \mathbf{h})^2}\]

2. Base Metal Term (\(f_{\text{base}}\))

  • Normal Distribution (\(D_{\text{base}}\)): Standard GGX using the material's surface roughness parameter \(\alpha = \text{roughness}^2\).
  • Fresnel (\(F_{\text{base}}\)): Colored metallic Fresnel using the base color as \(F_0\): \[F_{\text{base}} = F_0 + (1.0 - F_0)(1.0 - (\mathbf{v} \cdot \mathbf{h}))^5\]
  • Visibility (\(V_{\text{base}}\)): The height-correlated Smith visibility function: \[V_{\text{base}} = \frac{0.5}{(\mathbf{n} \cdot \mathbf{l}) \sqrt{(\mathbf{n} \cdot \mathbf{v})^2(1 - \alpha^2) + \alpha^2} + (\mathbf{n} \cdot \mathbf{v}) \sqrt{(\mathbf{n} \cdot \mathbf{l})^2(1 - \alpha^2) + \alpha^2}}\]

GLSL Implementation

#version 450 core

const float PI = 3.141592653589793;

// Normal Distribution Function: GGX / Trowbridge-Reitz
float D_GGX(float NoH, float roughness) {
    float a = roughness * roughness;
    float a2 = a * a;
    float denom = (NoH * NoH * (a2 - 1.0) + 1.0);
    return a2 / (PI * denom * denom);
}

// Height-Correlated Smith Visibility Function
float V_SmithGGXCorrelated(float NoV, float NoL, float roughness) {
    float a2 = roughness * roughness;
    float lambdaV = NoL * sqrt(NoV * NoV * (1.0 - a2) + a2);
    float lambdaL = NoV * sqrt(NoL * NoL * (1.0 - a2) + a2);
    return 0.5 / max(lambdaV + lambdaL, 1e-5);
}

// Kelemen Visibility approximation for clear coat
float V_Kelemen(float VoH) {
    return 0.25 / max(VoH * VoH, 1e-5);
}

// Fresnel-Schlick approximation
vec3 F_Schlick(vec3 f0, float VoH) {
    return f0 + (vec3(1.0) - f0) * pow(clamp(1.0 - VoH, 0.0, 1.0), 5.0);
}

float F_SchlickDielectric(float f0, float VoH) {
    return f0 + (1.0 - f0) * pow(clamp(1.0 - VoH, 0.0, 1.0), 5.0);
}

// Evaluates clear coat layer over rough metallic base
vec3 evaluateClearCoatMetalBRDF(
    vec3 N,             // Surface normal
    vec3 V,             // View vector
    vec3 L,             // Light direction vector
    vec3 baseColor,     // Metallic reflectance (F0)
    float baseRoughness,// Base metal roughness [0..1]
    float clearCoat,    // Clear coat strength [0..1]
    float ccRoughness   // Clear coat roughness [0..1]
) {
    vec3 H = normalize(V + L);

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

    if (NoL <= 0.0) {
        return vec3(0.0);
    }

    // --- Base Metal Layer ---
    float linearRoughness = baseRoughness * baseRoughness;
    float D_base = D_GGX(NoH, linearRoughness);
    float V_base = V_SmithGGXCorrelated(NoV, NoL, linearRoughness);
    vec3 F_base  = F_Schlick(baseColor, VoH);
    vec3 specBase = (D_base * V_base) * F_base;

    // --- Clear Coat Layer ---
    // Clamping clear coat roughness to avoid singularities
    float ccLinearRoughness = clamp(ccRoughness * ccRoughness, 0.001, 0.1);
    float D_cc = D_GGX(NoH, ccLinearRoughness);
    float V_cc = V_Kelemen(VoH);
    float F_cc = F_SchlickDielectric(0.04, VoH) * clearCoat;
    vec3 specClearCoat = vec3(D_cc * V_cc * F_cc);

    // --- Layer Combination ---
    // The clear coat absorbs/reflects energy, attenuating the base layer
    vec3 combinedColor = specBase * (1.0 - F_cc) + specClearCoat;

    return combinedColor * NoL;
}