Thin-Film Interference in GLSL Using Optical Path Difference
Thin-film optical interference creates dynamic, iridescence-driven color shifts on surfaces such as soap bubbles, oil slicks, and coated optics. In real-time computer graphics, shaders implement this wave-optics phenomenon by computing the Optical Path Difference (OPD) between light rays reflecting off the upper and lower boundaries of a thin layer. By determining the phase displacement across visible wavelengths and converting spectral intensity variations into RGB color values, OpenGL Shading Language (GLSL) shaders can render physically plausible thin-film iridescence efficiently.
Physical Principles and Optical Path Difference (OPD)
When light encounters a thin dielectric film of thickness \(d\) and refractive index \(n_2\) surrounded by a medium of refractive index \(n_1\), a portion reflects off the outer boundary while the rest refracts into the film, reflects off the inner boundary, and emerges back out.
The geometric difference in travel distance between these two paths defines the Optical Path Difference. Using Snell's law (\(n_1 \sin\theta_i = n_2 \sin\theta_t\)), where \(\theta_i\) is the angle of incidence and \(\theta_t\) is the angle of refraction inside the film, the OPD is defined as:
\[\text{OPD} = 2 \cdot n_2 \cdot d \cdot \cos(\theta_t)\]
Using the trigonometric identity \(\cos(\theta_t) = \sqrt{1 - \sin^2(\theta_t)}\), the term \(\cos(\theta_t)\) can be derived directly from the surface normal \(\mathbf{N}\) and incident view vector \(\mathbf{V}\):
\[\cos(\theta_t) = \sqrt{1.0 - \left(\frac{n_1}{n_2}\right)^2 (1.0 - (\mathbf{N} \cdot \mathbf{V})^2)}\]
Phase Shifts and Interference Conditions
When light reflects from a medium with a lower refractive index to one with a higher refractive index, it undergoes a half-wave (\(\pi\)) phase shift. If the film sits on a substrate with refractive index \(n_3\), the boundary conditions determine net phase changes:
- If \(n_1 < n_2 > n_3\) or \(n_1 > n_2 < n_3\): Only one boundary undergoes a \(\pi\) phase shift, adding \(\frac{\lambda}{2}\) to the path difference.
- If \(n_1 < n_2 < n_3\) or \(n_1 > n_2 > n_3\): Both or neither boundary undergoes a \(\pi\) phase shift, resulting in zero net shift.
The total phase difference \(\delta\) for a specific wavelength \(\lambda\) is:
\[\delta(\lambda) = \frac{2\pi}{\lambda} \cdot \text{OPD} + \Delta\phi\]
Constructive interference occurs when waves align in phase (\(\delta = 2\pi m\)), amplifying that wavelength. Destructive interference occurs when waves are out of phase (\(\delta = (2m + 1)\pi\)), canceling the wavelength.
Approximating Spectral Interference to RGB in GLSL
Full spectral rendering evaluates continuous wavelengths across the visible spectrum (380 nm to 780 nm) and integrates them with CIE color matching functions. In real-time GLSL, a fast and visually convincing approach samples representative wavelengths for the primary color channels:
- Red: \(\lambda_R \approx 650\text{ nm}\)
- Green: \(\lambda_G \approx 510\text{ nm}\)
- Blue: \(\lambda_B \approx 475\text{ nm}\)
Using the two-beam interference equation, the reflected intensity \(I\) for each wavelength is:
\[I(\lambda) = \cos^2\left(\frac{\pi \cdot \text{OPD}}{\lambda} + \frac{\Delta\phi}{2}\right)\]
Complete GLSL Implementation
The following GLSL fragment shader function computes interference color based on incident angle, film thickness, and refractive indices:
// Computes RGB interference reflection based on thin-film OPD
vec3 calculateThinFilmInterference(
vec3 normal,
vec3 viewDir,
float thicknessNm,
float n1, // Surrounding medium (e.g., Air = 1.0)
float n2, // Thin film (e.g., Soap = 1.33, Oil = 1.5)
float n3 // Substrate (e.g., Water = 1.33)
) {
float cosThetaI = clamp(dot(normal, viewDir), 0.0, 1.0);
// Snell's Law: compute cos(theta_t) inside the film
float sinThetaISq = 1.0 - cosThetaI * cosThetaI;
float eta = n1 / n2;
float sinThetaTSq = eta * eta * sinThetaISq;
// Total internal reflection check
if (sinThetaTSq > 1.0) {
return vec3(1.0);
}
float cosThetaT = sqrt(1.0 - sinThetaTSq);
// Compute Optical Path Difference (OPD) in nanometers
float opd = 2.0 * n2 * thicknessNm * cosThetaT;
// Determine boundary phase shift (Delta Phi)
// Shift by pi adds a half-cycle (0.5) to the wave ratio
float phaseShift = 0.0;
bool shift1 = (n1 < n2);
bool shift2 = (n2 < n3);
if (shift1 != shift2) {
phaseShift = 0.5;
}
// Target wavelengths in nanometers (R, G, B)
vec3 wavelengths = vec3(650.0, 510.0, 475.0);
// Calculate phase cycle and resultant intensity
vec3 phase = (opd / wavelengths) + phaseShift;
// Interference intensity equation: cos^2(pi * phase)
const float PI = 3.14159265359;
vec3 intensity = cos(PI * phase);
intensity = intensity * intensity;
return intensity;
}Integration into Lighting Models
To blend the result with standard physically based rendering (PBR):
- Fresnel Modulation: Multiply the computed interference intensity by the angle-dependent Fresnel reflectance to ensure edge reflections transition smoothly toward pure reflection.
- Thickness Variation: Supply
thicknessNmusing a procedural noise function or a texture map to model uneven layers, reproducing the swirling color bands seen on physical films.