Sampling GGX Visible Normals (VNDF) in GLSL
This article provides an overview and an efficient GLSL implementation of sampling the Visible Normal Distribution Function (VNDF) for the GGX microfacet model. Sampling visible normals restricts generated microfacet normals exclusively to those facing the viewer, eliminating wasted samples from back-facing microfacets and significantly reducing Monte Carlo noise in physically based rendering (PBR) pipelines.
The Principle Behind VNDF Sampling
Standard microfacet sampling generates normals across the entire hemisphere defined by the distribution \(D(\mathbf{m})\). However, many of these generated microfacets point away from the incoming view direction \(\mathbf{v}\) and are shadowed. Sampling the VNDF, denoted as \(D_{\mathbf{v}}(\mathbf{m})\), weights the distribution by the geometric visibility term:
\[D_{\mathbf{v}}(\mathbf{m}) = \frac{G_1(\mathbf{v}, \mathbf{m}) \max(0, \mathbf{v} \cdot \mathbf{m}) D(\mathbf{m})}{\mathbf{v} \cdot \mathbf{n}}\]
Eric Heitz presented an exact, bounded algorithm that samples this distribution without rejection sampling by mapping the problem to the projected area of a hemisphere.
The Algorithm Steps
- Transform View Vector: Stretch the tangent-space view vector \(\mathbf{v}\) using the roughness values \(\alpha_x\) and \(\alpha_y\).
- Build an Orthonormal Basis: Construct a coordinate frame aligned with the transformed view vector.
- Sample the Projected Area: Sample a unit disk uniformly and warp it into the visible hemisphere's projected profile.
- Reproject to the Hemisphere: Calculate the unprojected normal in the transformed configuration.
- Unstretch and Normalize: Re-scale the normal coordinates by \(\alpha_x\) and \(\alpha_y\), then normalize to retrieve the final tangent-space normal.
Complete GLSL Implementation
The following function implements Eric Heitz’s bounded VNDF sampling routine for anisotropic GGX distributions in tangent space, where the macro-surface normal is \((0, 0, 1)\).
// Samples the GGX VNDF in tangent space.
// Ve: View vector pointing towards the viewer, in tangent space (Z is up)
// alpha: Roughness parameters vec2(alpha_x, alpha_y)
// u: Two independent uniform random numbers in [0, 1)
vec3 sampleGGXVNDF(vec3 Ve, vec2 alpha, vec2 u) {
// 1. Transform view vector to the stretched hemisphere configuration
vec3 Vh = normalize(vec3(alpha.x * Ve.x, alpha.y * Ve.y, Ve.z));
// 2. Construct orthonormal basis around Vh
float lensq = Vh.x * Vh.x + Vh.y * Vh.y;
vec3 T1 = lensq > 0.0 ? vec3(-Vh.y, Vh.x, 0.0) * inversesqrt(lensq) : vec3(1.0, 0.0, 0.0);
vec3 T2 = cross(Vh, T1);
// 3. Parameterize the projected area via disk sampling
float r = sqrt(u.x);
float phi = 2.0 * 3.14159265358979323846 * u.y;
float t1 = r * cos(phi);
float t2 = r * sin(phi);
float s = 0.5 * (1.0 + Vh.z);
t2 = (1.0 - s) * sqrt(max(0.0, 1.0 - t1 * t1)) + s * t2;
// 4. Reproject onto the hemisphere
vec3 Nh = t1 * T1 + t2 * T2 + sqrt(max(0.0, 1.0 - t1 * t1 - t2 * t2)) * Vh;
// 5. Unstretch and normalize to obtain the final microfacet normal
return normalize(vec3(alpha.x * Nh.x, alpha.y * Nh.y, max(0.0, Nh.z)));
}Using the Sampled Normal for Specular Reflection
Once the microfacet normal \(\mathbf{m}\) is sampled:
- Calculate the reflected incident ray direction: \[\mathbf{l} = -\mathbf{v} + 2(\mathbf{v} \cdot \mathbf{m})\mathbf{m}\]
- Ensure both \(\mathbf{l} \cdot \mathbf{n} > 0\) and \(\mathbf{v} \cdot \mathbf{m} > 0\) to prevent light leaks below the horizon.
- The probability density function (PDF) with respect to the solid angle of the reflected direction \(\mathbf{l}\) is: \[p(\mathbf{l}) = \frac{D_{\mathbf{v}}(\mathbf{m})}{4 (\mathbf{v} \cdot \mathbf{m})} = \frac{G_1(\mathbf{v}) D(\mathbf{m})}{4 (\mathbf{v} \cdot \mathbf{n})}\]
Using this formulation, dividing the specular BRDF evaluation by \(p(\mathbf{l})\) cancels the \(D(\mathbf{m})\) term and reduces geometric shadowing variance, yielding fast convergence in path tracers.