Skip to content

Fresnel reflectance at an angle, live

The reflectance of unpolarised light arriving from vacuum at a surface of complex refractive index n + ik, at an angle θ from the normal: the mean of the s and p reflectances of the full Fresnel equations, not Schlick's approximation. The library's law writes them in real arithmetic. Below, the angle is swept from the normal to grazing, a degree a step, fifteen steps a second, and at each the library's law (lusine.properties.fresnel) gives the reflectance of the library's copper in blue, red and green light and of its soda-lime glass, on your GPU as you read. Beside it the same program works the textbook's complex equations (Born and Wolf, Principles of Optics, 14.2) out in complex arithmetic, each complex number as its real and imaginary parts: r_s = (cos θ − N cos θ_t) / (cos θ + N cos θ_t), r_p = (N cos θ − cos θ_t) / (N cos θ + cos θ_t), cos θ_t = √(1 − sin²θ / N²), R = (|r_s|² + |r_p|²) / 2.

This page runs the program on your GPU through WebGPU, so it needs a browser with WebGPU (a recent Chrome, Edge, Safari or Firefox). Without it the program does not run and nothing is drawn below.

What it shows

  • The strip above the controls is green while every check holds, and turns red for good once one fails.
  • The angle: the chart draws each surface's reflectance against the angle from the normal in degrees, a point a step over one whole sweep: copper reflects most of the red and less of the blue, which is its colour, and every surface reflects everything at grazing. Below it, the angle, and copper's red and the glass's reflectance beside the complex equations'.
  • Brewster's angle: a glass of n = 1.5 at tan θ = n, where it reflects no p-polarised light, so its reflectance is half its s reflectance, 0.0739.
  • The checks, each held at every step: the complex equations, every surface's reflectance within 2 × 10⁻⁵ of theirs at every angle (the real form is exact; the difference is binary32's); normal and grazing, at the normal the library's F0 (lusine.properties.fresnel_f0) within 0.0002% and at grazing exactly 1; Brewster's angle, the glass within 10⁻⁶ of half its s reflectance; rises toward grazing, the soda-lime glass's reflectance never falling as the angle grows.

Every number here is computed by the program on the GPU; the page only reads the values back and draws them. The library's rows, the Brewster glass and the sweep are the program's settings, which the host may write; this page offers no control to change them yet.

The program

The sweep, the law's answers, the complex equations and the checks, the panels the page draws, and the one procedure, main, run each step: the sweep moves on, the law and the complex equations are asked for each surface, and the checks are taken.

// The Fresnel reflectance's live page: the angle of incidence swept from the normal to grazing for the library's copper
// in red, green and blue and its soda-lime glass, the library's reflectance at each beside the textbook's complex
// Fresnel equations worked out here in complex arithmetic, with the page's checks computed here on the GPU and shown by
// the panels the program declares.

namespace fresnel;

lusine.version version = { { number = 1 } };

// The reader's settings: the library's copper at 450, 650 and 550 nm and its soda-lime glass at the sodium D line; a
// glass of n = 1.5 for Brewster's angle; and how far each step moves the angle, in degrees, from the normal.
host struct {
  relation<lusine.materials.refractive_index> copper_blue
    = lusine.materials.refractive_index[lusine.materials.substances["copper"], "solid", 450.0 nm];
  relation<lusine.materials.refractive_index> copper_red
    = lusine.materials.refractive_index[lusine.materials.substances["copper"], "solid", 650.0 nm];
  relation<lusine.materials.refractive_index> copper_green
    = lusine.materials.refractive_index[lusine.materials.substances["copper"], "solid", 550.0 nm];
  relation<lusine.materials.refractive_index> glass
    = lusine.materials.refractive_index[lusine.materials.substances["soda_lime_glass"], "solid", 589.3 nm];
  float brewster_glass = 1.5; float stride = 1.0;
} settings;

// The sweep: the step it is at, from 0 to 90 and round again, the angle from the normal in degrees, and its cosine.
struct { integer step = -1; float degrees; float cosine; } sweep;

// The reflectance of each surface at the angle; the complex equations' for each; the glass's at the last step; the
// glass of n = 1.5 at Brewster's angle, and half its s reflectance there.
struct {
  float blue; float red; float green; float glass; float blue_complex; float red_complex; float green_complex;
  float glass_complex; float before; float brewster; float half_s;
} law;

// The checks: the reflectance the complex equations' within 2 x 10^-5 at every angle for every surface; at the normal
// the library's F0 within 0.0002%, and at grazing exactly 1; a dielectric at Brewster's angle reflecting no p light,
// so half its s reflectance, within 10^-6; and the glass's reflectance never falling as the angle grows.
struct { bool complex = true; bool limits = true; bool brewster = true; bool rises = true; } checks;

// A strip of lamps the page draws, lit green while every check holds and red once one fails.
struct lamp { vector<float, 4> colour; };
lattice<lamp, 2> lamps(spacing = 1 m, boundary = wall);

vector<float, 4> lamp_colour(bool holds)
{
  return holds ? vector<float, 4>(0.15, 0.6, 0.3, 1.0) : vector<float, 4>(0.75, 0.15, 0.15, 1.0);
}

// The textbook's complex Fresnel equations (Born and Wolf, 14.2), each complex number as its real and imaginary
// parts: N = n + ik, cos_t = sqrt(1 - sin^2 / N^2) on the principal branch, r_s = (cos - N cos_t) / (cos + N cos_t),
// r_p = (N cos - cos_t) / (N cos + cos_t), R = (|r_s|^2 + |r_p|^2) / 2.
float complex_fresnel(float n, float k, float c)
{
  let s2 = 1.0 - c * c;
  let nr = n * n - k * k;
  let ni = 2.0 * n * k;
  let m = nr * nr + ni * ni;
  let zr = 1.0 - s2 * nr / m;
  let zi = s2 * ni / m;
  let r = sqrt(zr * zr + zi * zi);
  let tr = sqrt(0.5 * (r + zr));
  let ti0 = sqrt(0.5 * (r - zr));
  let ti = zi < 0.0 ? -ti0 : ti0;
  let ar = n * tr - k * ti;
  let ai = n * ti + k * tr;
  let rs = ((c - ar) * (c - ar) + ai * ai) / ((c + ar) * (c + ar) + ai * ai);
  let pr = n * c;
  let pi = k * c;
  let rp = ((pr - tr) * (pr - tr) + (pi - ti) * (pi - ti)) / ((pr + tr) * (pr + tr) + (pi + ti) * (pi + ti));
  return 0.5 * (rs + rp);
}

float reflectance(relation<lusine.materials.refractive_index> index, float cosine)
{
  return lusine.properties.fresnel(n = index.n, k = index.k, cos_theta = cosine);
}

float textbook(relation<lusine.materials.refractive_index> index, float cosine)
{
  return complex_fresnel(n = index.n, k = index.k, c = cosine);
}

// At the normal the library's F0, within 16 roundings (the two sum in different orders); at grazing exactly 1.
bool ends(relation<lusine.materials.refractive_index> index)
{
  let f0 = lusine.properties.fresnel_f0(n = index.n, k = index.k);
  return abs(reflectance(index, cosine = 1.0) - f0) <= 0.000002 * f0 && reflectance(index, cosine = 0.0) == 1.0;
}

// Brewster's angle of a dielectric, tan = n: its cosine 1 / sqrt(1 + n^2).
float brewster_cosine(float n) { return 1.0 / sqrt(1.0 + n * n); }

// Half the s reflectance there, from Snell's law: cos_t = sqrt(1 - sin^2 / n^2).
float half_s(float n)
{
  let c = brewster_cosine(n);
  let t = sqrt(1.0 - (1.0 - c * c) / (n * n));
  let rs = (c - n * t) / (c + n * t);
  return 0.5 * rs * rs;
}

bool agrees(float got, float want) { return abs(got - want) <= 0.00002; }

output table<lusine.widget> panels = {
  lusine.series { { panel = "the angle", chart = "reflectance", quantity = law.blue, steps = 91,
                    against = sweep.degrees, label = "copper, 450 nm" } },
  lusine.series { { panel = "the angle", chart = "reflectance", quantity = law.red, steps = 91,
                    against = sweep.degrees, label = "copper, 650 nm" } },
  lusine.series { { panel = "the angle", chart = "reflectance", quantity = law.green, steps = 91,
                    against = sweep.degrees, label = "copper, 550 nm" } },
  lusine.series { { panel = "the angle", chart = "reflectance", quantity = law.glass, steps = 91,
                    against = sweep.degrees, label = "soda-lime glass" } },
  lusine.value { { panel = "the angle", quantity = sweep.degrees, label = "degrees from the normal" } },
  lusine.value { { panel = "the angle", quantity = law.red, label = "copper, 650 nm" } },
  lusine.value { { panel = "the angle", quantity = law.red_complex, label = "the complex equations" } },
  lusine.value { { panel = "the angle", quantity = law.glass, label = "glass" } },
  lusine.value { { panel = "the angle", quantity = law.glass_complex, label = "the complex equations" } },
  lusine.value { { panel = "Brewster's angle", quantity = law.brewster, label = "n = 1.5 there" } },
  lusine.value { { panel = "Brewster's angle", quantity = law.half_s, label = "half R_s" } },
  lusine.value { { panel = "the checks", quantity = checks.complex, label = "the complex equations" } },
  lusine.value { { panel = "the checks", quantity = checks.limits, label = "normal and grazing" } },
  lusine.value { { panel = "the checks", quantity = checks.brewster, label = "Brewster's angle" } },
  lusine.value { { panel = "the checks", quantity = checks.rises, label = "rises toward grazing" } },
};

void main()
{
  sweep.step = sweep.step >= 90 ? 0 : sweep.step + 1;
  sweep.degrees = float(sweep.step) * settings.stride;
  sweep.cosine = cos;
  law.before = law.glass;
  law.blue = reflectance(index = settings.copper_blue, sweep.cosine);
  law.red = reflectance(index = settings.copper_red, sweep.cosine);
  law.green = reflectance(index = settings.copper_green, sweep.cosine);
  law.glass = reflectance(index = settings.glass, sweep.cosine);
  law.blue_complex = textbook(index = settings.copper_blue, sweep.cosine);
  law.red_complex = textbook(index = settings.copper_red, sweep.cosine);
  law.green_complex = textbook(index = settings.copper_green, sweep.cosine);
  law.glass_complex = textbook(index = settings.glass, sweep.cosine);
  law.brewster = lusine.properties.fresnel(n = settings.brewster_glass, k = 0.0,
                                           cos_theta = brewster_cosine(n = settings.brewster_glass));
  law.half_s = half_s(n = settings.brewster_glass);
  checks.complex = checks.complex && agrees(got = law.blue, want = law.blue_complex)
                   && agrees(got = law.red, want = law.red_complex) && agrees(got = law.green, want = law.green_complex)
                   && agrees(got = law.glass, want = law.glass_complex);
  checks.limits = checks.limits && ends(index = settings.copper_blue) && ends(index = settings.copper_red)
                  && ends(index = settings.copper_green) && ends(index = settings.glass);
  checks.brewster = checks.brewster && abs(law.brewster - law.half_s) <= 0.000001;
  checks.rises = checks.rises && (sweep.step == 0 || law.glass >= law.before - 0.000001);
  lamps.colour = lamp_colour(holds = checks.complex && checks.limits && checks.brewster && checks.rises);
}

The law's other proof (every index in the library at 91 angles, and copper beside Schlick's approximation) is on its proof page.