Reusable GPU wave optics — 2026-10-25 Copy the makeWaveOptics function from science-recipes/wave-optics.js into the saved CS editor script. The team package also contains catalog/examples/wave-optics.js. It is a recipe builder, not a preinstalled browser command. No new activity needed. The existing WebGPU player must report typedPrograms, runtimeWGSL and gpuSurfaces. var wave = makeWaveOptics({nx:128, ny:128, spacing:0.05, wavelength:1}); await createGPUProgram("waves", wave.buffers); await compileGPUProgram("waves", wave.passes); await stepGPUProgram("waves", wave.dt); await showGPUSurface("waves", "positions", "triangles", "electric", wave.vertices, wave.triangleCount, {min:-0.1, max:0.1, extent:20, grid:false}); await runGPUProgram("waves", wave.dt, 1, 16); This empty configuration is deliberately dark: add source maps to create waves. Pause with pauseGPUProgram("waves"). Reset or change geometry/materials by removing and recreating the program from the saved recipe. clearRun disposes it. Never step with a larger dt than the returned wave.dt. Play speed changes displayed time only. settings nx,ny: integer32..192, default128. spacing: square cell length, default0.05. pml: absorbing border cells, default12; minimum4, at most one third of shorter axis. wavelength: free-space wavelength, default1, same length units as spacing. index: nx*ny refractive indices1..3, default1. Nonmagnetic epsilon=index squared. conductivity: nx*ny normalized electrical conductivities0..20, default0. conductor: nx*ny zeros/ones; 1 imposes Ez=0, default0. Use for walls and apertures. sourceReal,sourceImaginary: nx*ny signed amplitudes[-10,10], default0. probes:1..128 [xCell,yCell] pairs; defaultcentre. averageAfter: normalized time before averaging starts; default4*wavelength. Choose it to exclude travel/startup. All maps are ordinary arrays indexed x+y*nx. Omit a map to use its default. Keep non-air material, conductors and sources strictly outside PML and a one-cell margin. Probes also stay inside the physical domain. Sources inside PEC are invalid. At least12 cells per shortest material wavelength; check convergence at finer grids. Initialization is validated against the existing CS 2MB command limit. Very detailed large maps may require a smaller grid. No silent clipping or automatic coarsening. Configure arbitrary shapes once in JavaScript before allocating GPU state. A disk, curved lens cross-section, prism or graded-index region is just a refractive-index map; barrier openings are zeros in the conductor map. The GPU then advances fields. No per-frame JavaScript solver. Boundaries are staircased on the grid; no automatic import from GLB meshes, Unity colliders or ray-surface descriptions. Coherent sources The imposed electric drive is a*sin(2*pi*t/wavelength)+b*cos(2*pi*t/wavelength). For amplitude A and phase phi radians, sourceReal=A*cos(phi), sourceImaginary=A*sin(phi). Multiple source cells superpose coherently. Two opposite amplitudes are pi out of phase. A line with a phase gradient makes oblique finite illumination; a localized source radiates in multiple directions. No one-way TFSF injection is implied. Sources ramp over two periods. All sources use one frequency; a broadband pulse or several frequencies need a different source kernel. Measurements var data = await measureGPUBuffer("waves", "measurements", 0, 4); Each probe stores [mean Ez squared, mean Sx, mean Sy, sample count]. S=E cross H: Sx=-Ez*Hy, Sy=Ez*Hx. Electric values are centered to the magnetic half timestep; magnetic values are interpolated to the electric point. Signed flux distinguishes forward/backward net energy flow. These are normalized quantities, not W/m². Ez squared alone is not energy flux. Values are running means after averageAfter, not integrals or normalized-to-peak plots. Reset clears statistics. Use all probes on a line and integrate normal flux times spacing to estimate line power; compare against a reference run. Incident and reflected waves may need separate runs. Float32 sample counts lose integer precision after roughly16 million samples; reset averaging for runs that long. Sparse readback only (max1024 scalars per call). Physics 2D TMz Yee grid: Ez, Hx, Hy; mu=1, epsilon=n², classical linear nondispersive media. Central time update with conductivity damping and cubic split-field Berenger PML in air. No periodic wrap. dt=.7*spacing/sqrt(2), below vacuum 2D CFL limit. Index>=1 keeps vacuum the fastest wave. Electric and magnetic updates are separate ordered GPU passes. Measurements and display are separate passes without neighbor races. Units normalized with vacuum c=epsilon0=mu0=1. For a selected physical length unit L, time unit is L/c0; scaling amplitudes needs an explicit physical field normalization. A small simulation covering tens of wavelengths can study diffraction, interference, refraction and 2D lens focusing. It is not a way to resolve an entire metre-scale bench at visible wavelength. Use geometrical optics for that scale. No full 3D, TE/TM mixing, birefringence, material dispersion, nonlinear optics, quantum emission or automatic coupling into live Unity ray-traced images. The display is signed Ez, not the appearance of a photograph. PML is approximate, so test convergence/reflection. Validation: independent double-precision reference; native Unity GPU and browser WGSL tests for air, dielectric, PEC aperture, electrical loss and opposing sources. Existing Wave Interference recipe separately tested propagation and PML decay. Theory reference (independent implementation): John B. Schneider, Understanding the FDTD Method, chapters8 and11, https://eecs.wsu.edu/~schneidj/ufdtd/.