Skip to main content

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 v\mathbf{v}, outward surface normal n\mathbf{n}, and refractive index ratio η=n1/n2\eta = n_1 / n_2:

  1. Orient the normal n\mathbf{n} opposite to the incoming ray such that cos⁡θi=−(v⋅n)>0\cos \theta_i = -(\mathbf{v} \cdot \mathbf{n}) > 0.
  2. Compute the transmission discriminant: Δ=1−η2(1−cos⁡2θi).\Delta = 1 - \eta^2 (1 - \cos^2 \theta_i).
  3. Total Internal Reflection (TIR): If Δ<0\Delta < 0, light cannot transmit into the second medium and reflects completely: v′=v−2(v⋅n)n.\mathbf{v}' = \mathbf{v} - 2(\mathbf{v} \cdot \mathbf{n})\mathbf{n}.
  4. Refraction: If Δ≥0\Delta \ge 0, the refracted transmission vector is: t=ηv+(ηcos⁡θi−Δ)n.\mathbf{t} = \eta \mathbf{v} + \left(\eta \cos \theta_i - \sqrt{\Delta}\right)\mathbf{n}.
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 λ∈[400,700] nm\lambda \in [400, 700]\,\text{nm} via an empirical Cauchy relation:

n(λ)=1.5+B⋅0.008(1(λ/1000)2−10.552),n(\lambda) = 1.5 + B \cdot 0.008 \left(\frac{1}{(\lambda / 1000)^2} - \frac{1}{0.55^2}\right),

where BB controls dispersion strength and the refractive index is fixed at n(550 nm)=1.5n(550\,\text{nm})=1.5 for every value of BB. The simulation uniformly samples 101 wavelengths from 400 nm (violet) to 700 nm (red), with 21 parallel rays per wavelength, producing 101×21=2,121101 \times 21 = 2{,}121 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):

CharacteristicCanvas 2D PipelineWebGL Batching Pipeline
Geometry2,121 individual Path2D strokesPre-allocated Float32Array vertex buffer
Draw calls2,121 drawing calls per redraw1 call for all beams, 1 for prism fill, 1 for outline
Edge renderingBrowser Canvas 2D stroking and antialiasingGPU fragment shader with signed distance and antialiasing
Color blendingCanvas compositingAdditive blending (gl.blendFunc(gl.ONE, gl.ONE))
// 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 webglcontextlost and webglcontextrestored, releasing and reinitializing shaders without memory leaks when switching screens on mobile or when GPU resources are preempted.
Explore connectionsOpen network