跳到主要内容

棱镜色散

打开棱镜实验。按住画布拖动或使用方向键可旋转正三角形棱镜;在控制抽屉中可精细调节旋转角度、柯西色散强度,或一键重置为基准状态。

几何光学模型

模拟在二维平面中追踪白光束射入正三棱镜后的光路演化。光线与棱镜多边形边界相交时,严格按向量形式的斯涅尔定律计算折射与全反射。

向量形式的折射与反射定律

设入射光线单位方向向量为 v\mathbf{v},界面外法线为 n\mathbf{n},两介质折射率比值为 η=n1/n2\eta = n_1 / n_2

  1. 统一法线朝向,使其与入射光反向,即满足 cosθi=(vn)>0\cos \theta_i = -(\mathbf{v} \cdot \mathbf{n}) > 0
  2. 计算透射判别式: Δ=1η2(1cos2θi).\Delta = 1 - \eta^2 (1 - \cos^2 \theta_i).
  3. 全内反射(TIR):Δ<0\Delta < 0 时发生全反射,光线无法透射,反射方向为: v=v2(vn)n.\mathbf{v}' = \mathbf{v} - 2(\mathbf{v} \cdot \mathbf{n})\mathbf{n}.
  4. 折射透射:Δ0\Delta \ge 0 时,折射透射方向为: 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; // 全反射
return unit(direction.map((x, i) => ratio * x + (ratio * cosine - Math.sqrt(discriminant)) * normal[i]));
}

柯西色散模型与波长采样

为将复合白光分解为连续光谱,介质折射率随波长 λ[400,700]nm\lambda \in [400, 700]\,\text{nm} 变化,采用柯西经验公式:

n(λ)=1.5+B0.008(1(λ/1000)210.552),n(\lambda) = 1.5 + B \cdot 0.008 \left(\frac{1}{(\lambda / 1000)^2} - \frac{1}{0.55^2}\right),

其中 BB 为色散调节系数。在 400 nm(紫光)至 700 nm(红光)间均匀采样 101 个波长,每种波长分布 21 条平行光线,合计 101×21=2,121101 \times 21 = 2{,}121 条光线,每条最多追踪 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):

特性Canvas 2D 管线WebGL 批量管线
几何表示2,121 条独立的 Path2D统一分配的 Float32Array 显存缓冲区
绘制调用每次重绘 2,121 次绘制调用光束仅 1 次 Draw Call,棱镜填充与描边各 1 次
边缘柔化CPU 端线条反走样抗锯齿片元着色器按法向有向距离实时计算高斯衰减
颜色混合浏览器 Canvas 合成硬件加法混合 (gl.blendFunc(gl.ONE, gl.ONE))
// 每条光线线段展开为一个矩形(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 上下文丢失防护: 监听 webglcontextlostwebglcontextrestored 事件,在移动端切屏或 GPU 资源抢占时优雅释放与重新初始化着色器,不产生内存泄漏。
探索关联打开关联网络