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:
- 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\).
- 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;
}