Prism Dispersion
Open the prism experiment. Drag the canvas or use arrow keys to rotate the equilateral prism. Use the settings drawer to adjust rotation angle and Cauchy dispersion strength, or reset to default parameters.
Geometric Optics Model
The simulation traces a white-light beam through an equilateral prism using 2D geometric optics. At each intersection with a polygon edge, refraction and total internal reflection are calculated using the vector form of Snell's law.
Snell's Law in Vector Formulation
Given an incident unit direction , outward surface normal , and refractive index ratio :
- Orient the normal opposite to the incoming ray such that .
- Compute the transmission discriminant:
- Total Internal Reflection (TIR): If , light cannot transmit into the second medium and reflects completely:
- Refraction: If , the refracted transmission vector is:
export function refract(direction, outwardNormal, fromIndex, toIndex) {
const normal = dot(direction, outwardNormal) > 0 ? outwardNormal.map(x => -x) : outwardNormal;
const cosine = -dot(direction, normal);
const ratio = fromIndex / toIndex;
const discriminant = 1 - ratio * ratio * Math.max(0, 1 - cosine * cosine);
if (discriminant < 0) return null; // Total internal reflection
return unit(direction.map((x, i) => ratio * x + (ratio * cosine - Math.sqrt(discriminant)) * normal[i]));
}
Cauchy Dispersion Model
To separate polychromatic white light into a rainbow spectrum, the refractive index varies as a function of wavelength via an empirical Cauchy relation:
where controls dispersion strength and the refractive index is fixed at for every value of . The simulation uniformly samples 101 wavelengths from 400 nm (violet) to 700 nm (red), with 21 parallel rays per wavelength, producing individual rays traced up to 12 bounces.
export function traceRay(origin, direction, polygon, index, width, height) {
const points = [origin];
let p = origin, v = direction;
for (let bounce = 0; bounce < 12; bounce++) {
const hit = intersectRay(p, v, polygon);
if (!hit) {
// Ray leaves the prism; extend to viewport boundary
points.push(extendToBoundary(p, v, width, height));
break;
}
points.push(hit.point);
const exiting = dot(v, hit.normal) > 0;
const transmitted = refract(v, hit.normal, exiting ? index : 1, exiting ? 1 : index);
v = transmitted ?? v.map((x, i) => x - 2 * dot(v, hit.normal) * hit.normal[i]);
p = hit.point.map((x, i) => x + v[i] * 1e-4);
}
return points;
}
In PhET’s Bending Light, enter the Prisms screen, drag a triangular prism into the beam, select the white-light source, and switch it on. Change the medium or prism angle to compare refraction at each interface with the outgoing color separation shown here.
GPU Batching in p5.js WebGL Mode
A per-ray Canvas 2D implementation would issue 2,121 context stroke calls per redraw, together with color compositing. Canvas 2D rasterization may use the CPU or GPU, depending on the browser and context configuration. This implementation batches beam geometry through WebGL to reduce drawing calls.
The implementation switches p5.js into WebGL mode and uploads ray segments into a single contiguous vertex buffer (BeamMesh):
// Each line segment expands into a quad (two triangles, 6 vertices)
// with position, premultiplied color, and signed normal offset
mesh.segment(p1, p2, [r, g, b, alpha], lineWidth);
// Upload all vertices to GPU in a single bufferSubData call
gl.bufferSubData(gl.ARRAY_BUFFER, 0, mesh.data.subarray(0, mesh.count * VERTEX_SIZE));
gl.drawArrays(gl.TRIANGLES, 0, beamVertexCount);
The fragment shader evaluates normal distance from the segment axis, generating smooth Gaussian falloff along beam edges without texture lookups.
Interaction Decoupling and Context Recovery
- Decoupled input: Dragging updates the angle in an internal reference and schedules a redraw via
requestAnimationFrame, avoiding React component rerenders at 60 FPS. Parameter controls are only synchronized when dragging ends. - WebGL context loss: Listens for
webglcontextlostandwebglcontextrestored, releasing and reinitializing shaders without memory leaks when switching screens on mobile or when GPU resources are preempted.