/** Directional orthographic projection, snapped in light space to avoid crawling shadows. */ export function shadowFrame(eye, sun, resolution = 2048, radius = 48) { const length = Math.hypot(...sun), z = sun.map((v) => v / length), horizontal = Math.hypot(z[0], z[2]), x = horizontal > 1e-6 ? [z[2] / horizontal, 0, -z[0] / horizontal] : [1, 0, 0], y = [z[1] * x[2] - z[2] * x[1], z[2] * x[0] - z[0] * x[2], z[0] * x[1] - z[1] * x[0]], dot = (a, b) => a.reduce((sum, v, i) => sum + v * b[i], 0), texel = 2 * radius / resolution, sx = Math.round(dot(x, eye) / texel) * texel, sy = Math.round(dot(y, eye) / texel) * texel, sz = dot(z, eye), depth = radius * 2, center = eye.map((_, i) => x[i] * sx + y[i] * sy + z[i] * sz); return { center, radius, matrix: new Float32Array([ x[0] / radius, y[0] / radius, -z[0] / depth, 0, x[1] / radius, y[1] / radius, -z[1] / depth, 0, x[2] / radius, y[2] / radius, -z[2] / depth, 0, -sx / radius, -sy / radius, sz / depth, 1, ]), }; }