棱镜色散
打开棱镜实验。按住画布拖动或使用方向键可旋转正三角形棱镜;在控制抽屉中可精细调节旋转角度、柯西色散强度,或一键重置为基准状态。
几何光学模型
模拟在二维平面中追踪白光束射入正三棱镜后的光路演化。光线与棱镜多边形边界相交时,严格按向量形式的斯涅尔定律计算折射与全反射。
向量形式的折射与反射定律
设入射光线单位方向向量为 ,界面外法线为 ,两介质折射率比值为 :
- 统一法线朝向,使其与入射光反向,即满足 。
- 计算透射判别式:
- 全内反射(TIR): 当 时发生全反射,光线无法透射,反射方向为:
- 折射透射: 当 时,折射透射方向为:
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; // 全反射
return unit(direction.map((x, i) => ratio * x + (ratio * cosine - Math.sqrt(discriminant)) * normal[i]));
}
柯西色散模型与波长采样
为将复合白光分解为连续光谱,介质折射率随波长 变化,采用柯西经验公式:
其中 为色散调节系数。在 400 nm(紫光)至 700 nm(红光)间均匀采样 101 个波长,每种波长分布 21 条平行光线,合计 条光线,每条最多追踪 12 次弹射。
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) {
// 光线射出棱镜,延伸至视口边缘
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;
}
p5.js WebGL 模式下的 GPU 批量渲染
在传统的 Canvas 2D 模式下,每次重绘需逐条执行 2,121 次 stroke() 调用和加法颜色混合,导致交互拖拽时产生沉重的 CPU 栅格化开销。
实现采用 p5.js WebGL 模式,将全部光线线段展平成顶点数据,一次性写入连续的 Float32Array 顶点缓冲区(BeamMesh):
// 每条光线线段展开为一个矩形(2 个三角形,6 个顶点)
// 携带顶点坐标、预乘颜色以及法向偏移距离
mesh.segment(p1, p2, [r, g, b, alpha], lineWidth);
// 单次 bufferSubData 传输整屏光束顶点,并执行单次绘制
gl.bufferSubData(gl.ARRAY_BUFFER, 0, mesh.data.subarray(0, mesh.count * VERTEX_SIZE));
gl.drawArrays(gl.TRIANGLES, 0, beamVertexCount);
片元着色器通过顶点传入的有向法向距离实时计算高斯羽化,无需纹理采样即可实现柔和通透的光束边缘。
交互解耦与上下文恢复
- 交互与 React 状态解耦: 拖拽交互直接修改内部引用的角度,并在
requestAnimationFrame中按需重绘,避免以 60 FPS 频繁触发 React 组件重新渲染;拖动结束时才同步参数控件。 - WebGL 上下文丢失防护: 监听
webglcontextlost与webglcontextrestored事件,在移动端切屏或 GPU 资源抢占时优雅释放与重新初始化着色器,不产生内存泄漏。