Skip to content

Reflectance at normal incidence, live

A surface's reflectance at normal incidence from vacuum, F0 = ((n − 1)² + k²) / ((n + 1)² + k²), from its complex refractive index n + ik at one wavelength; a dielectric is k = 0, ((n − 1) / (n + 1))². A metal's colour is its F0 at the three wavelengths a renderer's channels sample. Below, the index n is swept from 0 to 4, 0.025 a step, twenty steps a second, for a dielectric and for absorbers of k = 1 and k = 3, and at each the library's law (lusine.properties.fresnel_f0) gives F0 on your GPU as you read. Beside it the same program divides the complex numbers N − 1 by N + 1 as complex numbers are divided and takes the square of the quotient's modulus, the textbook's form of the law. The library's own copper and iron, from their measured indices, are held to measured colours.

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 index: the chart draws F0 against n for the three, a point a step over one whole sweep: the dielectric's falls to nothing at n = 1, where the surface is vacuum's own index, and rises either side; an absorber never reaches zero. Below it, n, and F0 at k = 1 beside the complex quotient's.
  • The library: copper's and iron's F0 in red, green and blue (650, 550 and 450 nm), beside Hoffman's measured F0 in linear RGB ("Physics and Math of Shading", SIGGRAPH 2015 course notes); soda-lime glass and water at the sodium D line.
  • The checks, each held at every step: the complex quotient, F0 within 0.0001% of |(N − 1) / (N + 1)|² at every index and absorber; glass and water, glass of n = 1.5 reflecting 4% to a rounding, the library's soda-lime glass 4.30% and water 2.03%; copper and iron, copper at 0.935, 0.624 and 0.538, each within 0.001, within 3% of Hoffman's copper in every channel, and iron within 15% of his iron (his is integrated over each channel's response, these sample one wavelength a channel, and the library's iron is an evaporated film's); vacuum and a perfect conductor, n = 1 with k = 0 reflecting exactly nothing, and k of a million reflecting everything but a millionth.

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 measured colours 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 and the checks, the panels the page draws, and the one procedure, main, run each step: the sweep moves on, the law and the quotient are asked at the index, the library's phases are taken, and the checks are taken.

// The normal-incidence reflectance's live page: the refractive index swept for a dielectric and two absorbers, the
// library's F0 at each beside the complex quotient |(N - 1) / (N + 1)|^2 worked out in complex arithmetic, and the
// library's copper and iron against measured colours, with the page's checks computed here on the GPU and shown by the
// panels the program declares.

namespace fresnel_f0;

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

// The reader's settings: the library's copper and iron at 650, 550 and 450 nm (red, green, blue), and its soda-lime
// glass and liquid water at the sodium D line; Hoffman's measured F0 of copper and iron in linear RGB ("Physics and
// Math of Shading", SIGGRAPH 2015 course notes); the two absorbers' extinction coefficients; and how far each step
// moves the index, from none.
host struct {
  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> copper_blue
    = lusine.materials.refractive_index[lusine.materials.substances["copper"], "solid", 450.0 nm];
  relation<lusine.materials.refractive_index> iron_red
    = lusine.materials.refractive_index[lusine.materials.substances["iron"], "solid", 650.0 nm];
  relation<lusine.materials.refractive_index> iron_green
    = lusine.materials.refractive_index[lusine.materials.substances["iron"], "solid", 550.0 nm];
  relation<lusine.materials.refractive_index> iron_blue
    = lusine.materials.refractive_index[lusine.materials.substances["iron"], "solid", 450.0 nm];
  relation<lusine.materials.refractive_index> glass
    = lusine.materials.refractive_index[lusine.materials.substances["soda_lime_glass"], "solid", 589.3 nm];
  relation<lusine.materials.refractive_index> water
    = lusine.materials.refractive_index[lusine.materials.substances["water"], "liquid", 589.3 nm];
  float copper_red_measured = 0.955; float copper_green_measured = 0.638; float copper_blue_measured = 0.538;
  float iron_red_measured = 0.562; float iron_green_measured = 0.565; float iron_blue_measured = 0.578;
  float weak = 1.0; float strong = 3.0; float stride = 0.025;
} settings;

// The sweep: the step it is at, from 0 to 160 and round again, and the refractive index there.
struct { integer step = -1; float index; } sweep;

// F0 at the index for k = 0, 1 and 3, and the complex quotient's for each; the library's copper and iron in red, green
// and blue, and Hoffman's; its glass and water at the sodium D line.
struct {
  float dielectric; float weak; float strong; float dielectric_quotient; float weak_quotient; float strong_quotient;
  vector<float, 3> copper; vector<float, 3> iron; vector<float, 3> copper_measured; vector<float, 3> iron_measured;
  float copper_red; float copper_green; float copper_blue; float iron_red; float iron_green; float iron_blue;
  float glass; float water;
} law;

// The checks: F0 the complex quotient within 0.0001% at every index and absorber; glass of n = 1.5 reflecting 4% to a
// rounding; the library's glass at 4.30% and water at 2.03%, each within 0.01%; copper at 0.935, 0.624 and 0.538, each
// within 0.001, and within 3% of Hoffman's measured copper, iron within 15% of his (a film's index, one wavelength a
// channel); vacuum against vacuum reflecting nothing, and an extinction without bound everything.
struct { bool quotient = true; bool dielectrics = true; bool metals = true; bool limits = 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);
}

// |q|^2 of the complex quotient q = (N - 1) / (N + 1), N = n + ik, divided as complex numbers are: by multiplying
// through by the conjugate of N + 1.
float quotient(float n, float k)
{
  let d = (n + 1.0) * (n + 1.0) + k * k;
  let re = ((n - 1.0) * (n + 1.0) + k * k) / d;
  let im = (k * (n + 1.0) - (n - 1.0) * k) / d;
  return re * re + im * im;
}

float f0(relation<lusine.materials.refractive_index> index)
{
  return lusine.properties.fresnel_f0(n = index.n, k = index.k);
}

vector<float, 3> colour(relation<lusine.materials.refractive_index> red,
                        relation<lusine.materials.refractive_index> green,
                        relation<lusine.materials.refractive_index> blue)
{
  return vector<float, 3>(f0(index = red), f0(index = green), f0(index = blue));
}

bool matches(float got, float want) { return abs(got - want) <= 0.000001 * want + 0.000000001; }

bool near_all(vector<float, 3> got, vector<float, 3> want, float share)
{
  return abs(got.x / want.x - 1.0) < share && abs(got.y / want.y - 1.0) < share && abs(got.z / want.z - 1.0) < share;
}

output table<lusine.widget> panels = {
  lusine.series { { panel = "the index", chart = "F0", quantity = law.dielectric, steps = 161, against = sweep.index,
                    label = "k = 0" } },
  lusine.series { { panel = "the index", chart = "F0", quantity = law.weak, steps = 161, against = sweep.index,
                    label = "k = 1" } },
  lusine.series { { panel = "the index", chart = "F0", quantity = law.strong, steps = 161, against = sweep.index,
                    label = "k = 3" } },
  lusine.value { { panel = "the index", quantity = sweep.index, label = "n" } },
  lusine.value { { panel = "the index", quantity = law.weak, label = "F0 at k = 1" } },
  lusine.value { { panel = "the index", quantity = law.weak_quotient, label = "|(N - 1) / (N + 1)|^2" } },
  lusine.value { { panel = "the library", quantity = law.copper_red, label = "copper, red" } },
  lusine.value { { panel = "the library", quantity = settings.copper_red_measured, label = "Hoffman's" } },
  lusine.value { { panel = "the library", quantity = law.copper_green, label = "copper, green" } },
  lusine.value { { panel = "the library", quantity = settings.copper_green_measured, label = "Hoffman's" } },
  lusine.value { { panel = "the library", quantity = law.copper_blue, label = "copper, blue" } },
  lusine.value { { panel = "the library", quantity = settings.copper_blue_measured, label = "Hoffman's" } },
  lusine.value { { panel = "the library", quantity = law.iron_red, label = "iron, red" } },
  lusine.value { { panel = "the library", quantity = law.iron_green, label = "iron, green" } },
  lusine.value { { panel = "the library", quantity = law.iron_blue, label = "iron, blue" } },
  lusine.value { { panel = "the library", quantity = law.glass, label = "soda-lime glass" } },
  lusine.value { { panel = "the library", quantity = law.water, label = "water" } },
  lusine.value { { panel = "the checks", quantity = checks.quotient, label = "the complex quotient" } },
  lusine.value { { panel = "the checks", quantity = checks.dielectrics, label = "glass and water" } },
  lusine.value { { panel = "the checks", quantity = checks.metals, label = "copper and iron" } },
  lusine.value { { panel = "the checks", quantity = checks.limits, label = "vacuum and a perfect conductor" } },
};

void main()
{
  sweep.step = sweep.step >= 160 ? 0 : sweep.step + 1;
  sweep.index = float(sweep.step) * settings.stride;
  law.dielectric = lusine.properties.fresnel_f0(n = sweep.index, k = 0.0);
  law.weak = lusine.properties.fresnel_f0(n = sweep.index, k = settings.weak);
  law.strong = lusine.properties.fresnel_f0(n = sweep.index, k = settings.strong);
  law.dielectric_quotient = quotient(n = sweep.index, k = 0.0);
  law.weak_quotient = quotient(n = sweep.index, k = settings.weak);
  law.strong_quotient = quotient(n = sweep.index, k = settings.strong);
  law.copper = colour(red = settings.copper_red, green = settings.copper_green, blue = settings.copper_blue);
  law.iron = colour(red = settings.iron_red, green = settings.iron_green, blue = settings.iron_blue);
  law.copper_red = law.copper.x;
  law.copper_green = law.copper.y;
  law.copper_blue = law.copper.z;
  law.iron_red = law.iron.x;
  law.iron_green = law.iron.y;
  law.iron_blue = law.iron.z;
  law.copper_measured = vector<float, 3>(settings.copper_red_measured, settings.copper_green_measured,
                                         settings.copper_blue_measured);
  law.iron_measured = vector<float, 3>(settings.iron_red_measured, settings.iron_green_measured,
                                       settings.iron_blue_measured);
  law.glass = f0(index = settings.glass);
  law.water = f0(index = settings.water);
  checks.quotient = checks.quotient && matches(got = law.dielectric, want = law.dielectric_quotient)
                    && matches(got = law.weak, want = law.weak_quotient)
                    && matches(got = law.strong, want = law.strong_quotient);
  checks.dielectrics = checks.dielectrics && abs(lusine.properties.fresnel_f0(n = 1.5, k = 0.0) - 0.04) <= 0.00000001
                       && abs(law.glass - 0.043) < 0.0001 && abs(law.water - 0.0203) < 0.0001;
  checks.metals = checks.metals && abs(law.copper.x - 0.935) < 0.001 && abs(law.copper.y - 0.624) < 0.001
                  && abs(law.copper.z - 0.538) < 0.001
                  && near_all(got = law.copper, want = law.copper_measured, share = 0.03)
                  && near_all(got = law.iron, want = law.iron_measured, share = 0.15);
  checks.limits = checks.limits && lusine.properties.fresnel_f0(n = 1.0, k = 0.0) == 0.0
                  && lusine.properties.fresnel_f0(n = 1.0, k = 1000000.0) > 0.999999;
  lamps.colour = lamp_colour(holds = checks.quotient && checks.dielectrics && checks.metals && checks.limits);
}

The law's other proof (every index in the library against the complex form in double precision) is on its proof page.