Implementing Walter Rough Transmission in GLSL
This article outlines how to implement the Walter et al. (2007) microfacet transmission model for rough dielectrics in GLSL. Microfacet refraction extends standard reflection models by accounting for light scattering as it passes through rough surface interfaces. Below, the core mathematical equations for the Generalized Walter transmission BSDF are broken down into standard GGX components, followed by a complete, optimized GLSL shader function.
The Microfacet Refraction Formulation
Unlike reflection, where the half-vector is simply the normalized sum of the incident and outgoing vectors, rough transmission requires a generalized half-vector \(H_t\) that depends on the indices of refraction (IOR) of the two media:
\[H_t = -(\eta_i V + \eta_t L)\]
Where:
- \(V\) is the incoming view direction (pointing away from the surface).
- \(L\) is the refracted transmission direction (pointing away from the surface, into the medium).
- \(\eta_i\) and \(\eta_t\) are the refractive indices of the incident and transmitting media.
The rough dielectric transmission BTDF \(f_t(V, L)\) is defined as:
\[f_t(V, L) = \frac{|V \cdot H_t| |L \cdot H_t|}{|N \cdot V| |N \cdot L|} \frac{\eta_t^2 (1 - F) D(H_t) G(V, L)}{(\eta_i (V \cdot H_t) + \eta_t (L \cdot H_t))^2}\]
- \(D(H_t)\): GGX/Trowbridge-Reitz microfacet normal distribution function.
- \(G(V, L)\): Smith height-correlated or separable masking-shadowing function.
- \(F\): Dielectric Fresnel transmittance term evaluated at \(V \cdot H_t\).
- Denominator term: The Jacobian determinant accounting for the non-linear change of variables during refraction.
GLSL Helper Functions
The standard GGX normal distribution function and the Smith geometry term are computed using the surface normal \(N\), roughness \(\alpha\), and half-vector \(H\).
// GGX / Trowbridge-Reitz Microfacet Distribution
float distributionGGX(vec3 N, vec3 H, float roughness) {
float a = roughness * roughness;
float a2 = a * a;
float NdotH = max(dot(N, H), 0.0);
float NdotH2 = NdotH * NdotH;
float denom = (NdotH2 * (a2 - 1.0) + 1.0);
return a2 / (3.14159265359 * denom * denom);
}
// Smith GGX Masking-Shadowing Function (Separable)
float geometrySchlickGGX(float NdotV, float roughness) {
float r = roughness + 1.0;
float k = (r * r) / 8.0;
return NdotV / (NdotV * (1.0 - k) + k);
}
float geometrySmith(vec3 N, vec3 V, vec3 L, float roughness) {
float NdotV = abs(dot(N, V));
float NdotL = abs(dot(N, L));
return geometrySchlickGGX(NdotV, roughness) * geometrySchlickGGX(NdotL, roughness);
}
// Unpolarized Dielectric Fresnel Reflectance
float fresnelDielectric(float cosThetaI, float etaI, float etaT) {
float sinThetaT2 = (etaI / etaT) * (etaI / etaT) * (1.0 - cosThetaI * cosThetaI);
// Total internal reflection
if (sinThetaT2 > 1.0) return 1.0;
float cosThetaT = sqrt(max(1.0 - sinThetaT2, 0.0));
float rParl = ((etaT * cosThetaI) - (etaI * cosThetaT)) / ((etaT * cosThetaI) + (etaI * cosThetaT));
float rPerp = ((etaI * cosThetaI) - (etaT * cosThetaT)) / ((etaI * cosThetaI) + (etaT * cosThetaT));
return 0.5 * (rParl * rParl + rPerp * rPerp);
}Full Walter Transmission Implementation
In the following function, \(V\) and \(L\) point away from the shading point. Because transmission travels across the interface, \(N \cdot V\) and \(N \cdot L\) must have opposite signs.
vec3 evalWalterTransmission(
vec3 N, // Geometric surface normal
vec3 V, // Direction toward incoming ray/camera (pointing away from surface)
vec3 L, // Direction of refracted light (pointing away from surface)
float etaI, // Index of refraction of the incident medium (e.g., 1.0 for air)
float etaT, // Index of refraction of the transmitting medium (e.g., 1.5 for glass)
float roughness, // Linear roughness parameter
vec3 transmissionColor // Base material tint
) {
float cosThetaV = dot(N, V);
float cosThetaL = dot(N, L);
// V and L must reside on opposite sides of the surface for transmission
if (cosThetaV * cosThetaL >= 0.0) {
return vec3(0.0);
}
// Compute the Walter refraction half-vector
vec3 H = -(etaI * V + etaT * L);
if (dot(H, H) < 1e-6) return vec3(0.0);
H = normalize(H);
// Align half-vector normal to the side of the incoming ray
if (dot(N, H) < 0.0) {
H = -H;
}
float VdotH = abs(dot(V, H));
float LdotH = abs(dot(L, H));
float NdotV = abs(cosThetaV);
float NdotL = abs(cosThetaL);
// Evaluate microfacet components
float D = distributionGGX(N, H, roughness);
float G = geometrySmith(N, V, L, roughness);
float F = fresnelDielectric(VdotH, etaI, etaT);
// Transmitted energy fraction
float transmittance = 1.0 - F;
// Evaluate transmission denominator (Jacobian determinant)
float sqrtDenom = etaI * dot(V, H) + etaT * dot(L, H);
float denom = sqrtDenom * sqrtDenom;
if (denom < 1e-6) return vec3(0.0);
// Walter BSDF transmission evaluation
float factor = (VdotH * LdotH) / (NdotV * NdotL);
float btdf = factor * ((etaT * etaT) * transmittance * D * G) / denom;
return transmissionColor * btdf;
}Integration Notes
- Total Internal Reflection (TIR): The function
handles TIR automatically within
fresnelDielectric. If total internal reflection occurs, \(F = 1.0\), which nullifies the transmission factor \((1 - F)\). - Roughness Mapping: Linear roughness is mapped to
perceptual roughness (\(\alpha =
\text{roughness}^2\)) in
distributionGGXto match standard Disney and glTF PBR workflows. - Radiance Scaling: If using this model inside a bidirectional path tracer rather than a direct transmission evaluator, the term \(\frac{\eta_t^2}{\eta_i^2}\) must be adjusted to account for non-symmetric radiance transport across refractive boundaries.