# Note

The theoretical parts of this article require some knowledge of optics and electromagnetism, however the conclusion and final result (a practical implementation of single-layer thin film interference in the context of a BRDF) do not. You may therefore wish to skip the theoretical sections.# Introduction

Wave interference of light has been neglected for a long time in computer graphics, for multiple reasons. Firstly, it is often insignificant and can be cheaply approximated, or even ignored completely. Secondly, it is harder to understand as it requires interpreting light as waves instead of particles (photons). However, interference crops up almost everywhere in daily life, and has recently gained popularity in rendering applications. Examples of wave interference of light are soap bubbles, gasoline rainbow patterns, lens flares, basically everything that looks cool and/or involves multicolor patterns. For instance, in computer graphics, soap bubbles were in the past approximated with more or less realistic multicolor textures slightly panned with view angle. But it turns out that they are not that computationally difficult to accurately render. We will learn how. This article will focus on one particular form of interference, namely thin film interference. This occurs when one or more very thin transparent coatings ("films") are placed on top of a material. The films are so thin that when a light wave comes into contact with these film layers, it reflects and refracts multiple times inside the layer system, and interferes with itself in the process.*only one film*is present (single-layer) and conclude on how to solve the general case with arbitrarily many layers. The single-layer case is sufficient to render most real life occurrences of thin-film interference, however using more layers enables many more advanced effects. The cost of calculating reflection and transmission coefficients is linear in the number of layers.

# Derivation

Consider a light wave incident to a thin layer of depth \(\delta\) and real refractive index \(n_1\). The external medium has refractive index \(n_0\) and the internal medium has refractive index \(n_2\). The incident angle made by the incident light wave and the film's surface normal is \(\theta_0\), the angle inside the layer is \(\theta_1\) and the refracted angle (inside the internal medium) is denoted \(\theta_2\). We will also give numbers to each of the three media: medium 0 is the external medium, medium 1 is the layer, and medium 2 is the internal medium. Also, naturally, medium 0 has to have a different refractive index than medium 1, and the same goes for medium 1 and medium 2. Media 0 and 2 can be the same, of course.## General Case

The derivation shown above is quite naive, and does not generalize well at all to multiple layers, though it is the simplest method to see what is happening at a low level. If you wish to implement n-layer thin film interference, the method of choice is the Transfer-matrix method, which simplifies the problem down to a series of matrix multiplications and can be derived using powerful electromagnetism techniques.# Implementation

So we now know just how much light is reflected from the layer. How can we implement this in the context of a BRDF? It's quite simple: this reflected term simply replaces the ordinary Fresnel term, accounting for thin film interference effects. This means you can trivially include thin-film interference effects in any BRDF as long as it has a Fresnel term. The function below computes the reflection coefficient for a given wavelength and incident angle.```
// cosI is the cosine of the incident angle, that is, cos0 = dot(view angle, normal)
// lambda is the wavelength of the incident light (e.g. lambda = 510 for green)
float ThinFilmReflectance(float cos0, float lambda)
{
const float thickness; // the thin film thickness
const float n0, n1, n2; // the refractive indices
// compute the phase change term (constant)
const float d10 = (n1 > n0) ? 0 : PI;
const float d12 = (n1 > n2) ? 0 : PI;
const float delta = d10 + d12;
// now, compute cos1, the cosine of the reflected angle
float sin1 = pow(n0 / n1, 2) * (1 - pow(cos0, 2));
if (sin1 > 1) return 1.0f; // total internal reflection
float cos1 = sqrt(1 - sin1);
// compute cos2, the cosine of the final transmitted angle, i.e. cos(theta_2)
// we need this angle for the Fresnel terms at the bottom interface
float sin2 = pow(n0 / n2, 2) * (1 - pow(cos0, 2));
if (sin2 > 1) return 1.0f; // total internal reflection
float cos2 = sqrt(1 - sin2);
// get the reflection transmission amplitude Fresnel coefficients
float alpha_s = rs(n1, n0, cos1, cos0) * rs(n1, n2, cos1, cos2); // rho_10 * rho_12 (s-polarized)
float alpha_p = rp(n1, n0, cos1, cos0) * rp(n1, n2, cos1, cos2); // rho_10 * rho_12 (p-polarized)
float beta_s = ts(n0, n1, cos0, cos1) * ts(n1, n2, cos1, cos2); // tau_01 * tau_12 (s-polarized)
float beta_p = tp(n0, n1, cos0, cos1) * tp(n1, n2, cos1, cos2); // tau_01 * tau_12 (p-polarized)
// compute the phase term (phi)
float phi = (2 * PI / lambda) * (2 * n1 * thickness * cos1) + delta;
// finally, evaluate the transmitted intensity for the two possible polarizations
float ts = pow(beta_s) / (pow(alpha_s, 2) - 2 * alpha_s * cos(phi) + 1);
float tp = pow(beta_p) / (pow(alpha_p, 2) - 2 * alpha_p * cos(phi) + 1);
// we need to take into account conservation of energy for transmission
float beamRatio = (n2 * cos2) / (n0 * cos0);
// calculate the average transmitted intensity (if you know the polarization distribution of your
// light source, you should specify it here. if you don't, a 50%/50% average is generally used)
float t = beamRatio * (ts + tp) / 2;
// and finally, derive the reflected intensity
return 1 - t;
}
```

We can now sample this function at red, green, and blue wavelengths (650, 510, 475 nanometers, respectively) and substitute the RGB reflectance obtained into the Fresnel term of the BRDF. Or, if you are rendering spectrally, just give the wavelength directly. That's it.
One word on polarization - in general, in computer graphics, we assume light contains an equal amount of s-polarized and p-polarized light waves. Then the Fresnel reflection coefficient is simply an average between the s-polarized and p-polarized light reflection coefficients, as the comment indicates. If you have more information on how much s-polarized light is emitted by your light source, then the average should reflect that.
## BRDF Explorer Sample

The following shader script implements the BRDF in the Disney BRDF Explorer tool, using the stock Blinn-Phong shader with the default microfacet distribution. Note how we just implemented the code separately and multiplied the BRDF by the modified "thin film" Fresnel term.```
analytic
# Blinn Phong based on halfway-vector with single-layer thin
# film wave interference effects via a Fresnel film coating.
::begin parameters
float thickness 0 3000 250 # Thin film thickness (in nm)
float externalIOR 0.2 3 1 # External (air) refractive index
float thinfilmIOR 0.2 3 1.5 # Layer (thin film) refractive index
float internalIOR 0.2 3 1.25 # Internal (object) refractive index
float n 1 1000 100 # Blinn-Phong microfacet exponent
::end parameters
::begin shader
const float PI = 3.14159265f;
/* Amplitude reflection coefficient (s-polarized) */
float rs(float n1, float n2, float cosI, float cosT)
{
return (n1 * cosI - n2 * cosT) / (n1 * cosI + n2 * cosT);
}
/* Amplitude reflection coefficient (p-polarized) */
float rp(float n1, float n2, float cosI, float cosT)
{
return (n2 * cosI - n1 * cosT) / (n1 * cosT + n2 * cosI);
}
/* Amplitude transmission coefficient (s-polarized) */
float ts(float n1, float n2, float cosI, float cosT)
{
return 2 * n1 * cosI / (n1 * cosI + n2 * cosT);
}
/* Amplitude transmission coefficient (p-polarized) */
float tp(float n1, float n2, float cosI, float cosT)
{
return 2 * n1 * cosI / (n1 * cosT + n2 * cosI);
}
/* Pass the incident cosine. */
vec3 FresnelCoating(float cos0)
{
/* Precompute the reflection phase changes (depends on IOR) */
float delta10 = (thinfilmIOR < externalIOR) ? PI : 0.0f;
float delta12 = (thinfilmIOR < internalIOR) ? PI : 0.0f;
float delta = delta10 + delta12;
/* Calculate the thin film layer (and transmitted) angle cosines. */
float sin1 = pow(externalIOR / thinfilmIOR, 2) * (1 - pow(cos0, 2));
float sin2 = pow(externalIOR / internalIOR, 2) * (1 - pow(cos0, 2));
if ((sin1 > 1) || (sin2 > 1)) return vec3(1); /* Account for TIR. */
float cos1 = sqrt(1 - sin1), cos2 = sqrt(1 - sin2);
/* Calculate the interference phase change. */
vec3 phi = vec3(2 * thinfilmIOR * thickness * cos1);
phi *= 2 * PI / vec3(650, 510, 475);
phi += delta;
/* Obtain the various Fresnel amplitude coefficients. */
float alpha_s = rs(thinfilmIOR, externalIOR, cos1, cos0)
* rs(thinfilmIOR, internalIOR, cos1, cos2);
float alpha_p = rp(thinfilmIOR, externalIOR, cos1, cos0)
* rp(thinfilmIOR, internalIOR, cos1, cos2);
float beta_s = ts(externalIOR, thinfilmIOR, cos0, cos1)
* ts(thinfilmIOR, internalIOR, cos1, cos2);
float beta_p = tp(externalIOR, thinfilmIOR, cos0, cos1)
* tp(thinfilmIOR, internalIOR, cos1, cos2);
/* Calculate the s- and p-polarized intensity transmission coefficient. */
vec3 ts = pow(beta_s, 2) / (pow(alpha_s, 2) - 2 * alpha_s * cos(phi) + 1);
vec3 tp = pow(beta_p, 2) / (pow(alpha_p, 2) - 2 * alpha_p * cos(phi) + 1);
/* Calculate the transmitted power ratio for medium change. */
float beamRatio = (internalIOR * cos2) / (externalIOR * cos0);
/* Calculate the average reflectance. */
return 1 - beamRatio * (ts + tp) * 0.5f;
}
vec3 BRDF(vec3 L, vec3 V, vec3 N, vec3 X, vec3 Y)
{
vec3 H = normalize(L + V);
float val = pow(max(0, dot(N, H)), n);
return vec3(val) * FresnelCoating(dot(V, H));
}
::end shader
```

It is worth noting that this is a reference implementation meant to be readable, and can be thoroughly optimized. In particular, the Fresnel calculations are the most expensive, but there are numerous ways of reducing the amount of computations. For instance, we can use the reciprocity properties of s-polarized light, and also recycle many intermediate calculations. If you are not interested in perfect physical accuracy, you can also skip the polarization calculations and directly use intensity Fresnel coefficients, though because amplitudes are signed and intensities are not, you will need to calculate the proper sign to use for the cosine term somehow (or just ignore it altogether and have incorrect but plausible thin film interference).
If you are really desperate about runtime performance, you can still retain the nice colorful patterns while trading physical accuracy by approximating the final formula however you see fit, the only fundamental requirement is that the \(\cos{\varphi}\) term be in there somewhere.
This is a screenshot of the above BRDF's polar plot at incidence 45 degrees and illustrates its wavelength-dependent nature: