Probe light as spherical harmonics
32-bit, display onlySums a small traced panorama into the four coefficients per color the view shades with.
c = Σ L × Y × dΩ, dΩ = (2π ÷ w)(π ÷ h) sin θ
The sums are 64-bit, from the tracer's accumulation buffer and ray directions held in a Float32Array, and the twelve results are written to a Float32Array for a GPU texture. Y00 and Y1 are the rounded constants 0.282095 and 0.488603.
export function projectL1(accum, dirs, w, h, out = new Float32Array(12), at = 0) {
const cell = ((2 * Math.PI) / w) * (Math.PI / h);
const c = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
for (let i = 0; i < w * h; i++) {
const n = accum[i * 4 + 3];
if (!(n > 0)) continue;
const dx = dirs[i * 3], dy = dirs[i * 3 + 1], dz = dirs[i * 3 + 2];
// dΩ from the pixel's latitude: every panorama row is a band of equal height in θ (RENDER-SPEC § 1)
const row = Math.floor(i / w), sinT = Math.sin((Math.PI * (row + 0.5)) / h);
const wgt = (cell * sinT) / n;
const a = [accum[i * 4], accum[i * 4 + 1], accum[i * 4 + 2]];
for (let k = 0; k < 3; k++) {
const L = (AP1_TO_REC709[k][0] * a[0] + AP1_TO_REC709[k][1] * a[1] + AP1_TO_REC709[k][2] * a[2]) * wgt;
c[k] += L * Y0; c[3 + k] += L * Y1 * dy; c[6 + k] += L * Y1 * dz; c[9 + k] += L * Y1 * dx;
}
}
for (let k = 0; k < 12; k++) out[at + k] = c[k];
return out;
}