3. 光线追迹算法:逐面追迹法、交点计算与斯涅尔定律
光线追迹,说白了就是让一根虚拟的光线在光学系统里「跑」一遍。我刚开始做这个的时候,总觉得不就是画几条线嘛,有什么难的?直到第一次自己手算一个三片式镜头,才发现这里面的门道比想象中深得多。
这一章,我们就来聊聊光线追迹的核心算法。我会把我在项目中踩过的坑、总结的经验,一并讲给你听。
3.1 逐面追迹法:光线是怎么「走」过系统的?
光线追迹的基本思路很简单:从物点出发,依次计算光线与每个光学表面的交点,然后根据表面特性(折射或反射)改变光线的方向,再继续追迹下一个表面。
这个过程,我们称之为逐面追迹法。你想想看,就像接力赛跑一样,光线在每一个表面处「交接棒」——位置和方向被更新,然后传给下一个表面。
核心流程:
- 初始化:给定光线的起始点坐标 (x₀, y₀, z₀) 和方向余弦 (L₀, M₀, N₀)。
- 找交点:计算光线与当前光学表面的交点 (x, y, z)。
- 算法线:求出交点处表面的法线方向。
- 判方向:根据斯涅尔定律(折射或反射),计算光线的新方向余弦。
- 传下去:将新位置和新方向作为下一表面的输入,重复步骤 2-4。
我个人习惯把这一步叫做「光线接力」。在 Zemax 或 Code V 里,这个循环每跑一次,就是一条光线追迹的完整生命周期。
3.2 光线与表面的交点计算
这是整个追迹里最「数学」的部分。不同的表面类型,交点公式完全不同。我当年在项目里吃过亏——用球面公式去算非球面,结果像差分析全错了。
3.2.1 球面交点
球面是最基础的光学表面。给定球心坐标 (x₀, y₀, z₀) 和半径 R,光线方程代入球面方程,解一个一元二次方程就行。
// 光线参数方程:P = P₀ + t * D
// 球面方程:|P - C|² = R²
// 代入后得到:a t² + b t + c = 0
a = Dx² + Dy² + Dz²
b = 2 * [Dx*(P₀x - Cx) + Dy*(P₀y - Cy) + Dz*(P₀z - Cz)]
c = (P₀x - Cx)² + (P₀y - Cy)² + (P₀z - Cz)² - R²
// 判别式 Δ = b² - 4ac
// 取最小的正 t 值,即为光线首次到达球面的位置
我的经验: 实际编程时,一定要处理 Δ < 0 的情况——光线可能完全错过表面。另外,t 值要排除 0 附近的极小值,避免光线「原地打转」。
3.2.2 平面与二次曲面
平面就简单多了,直接解线性方程。二次曲面(如抛物面、椭球面)则和球面类似,只是方程里多了非球面系数。
| 表面类型 | 方程形式 | 求解方法 |
|---|---|---|
| 平面 | Ax + By + Cz + D = 0 | 线性方程,直接代入 |
| 球面 | (x - x₀)² + (y - y₀)² + (z - z₀)² = R² | 一元二次方程 |
| 二次曲面 | z = (x² + y²) / (2R) + 高阶项 | 迭代法或解析法 |
避坑指南: 我曾经在计算非球面交点时,直接用球面公式近似,结果高次项带来的误差让整个系统的像差分析完全失真。后来老老实实用了牛顿迭代法,才把精度提上来。
3.3 折射与反射的矢量形式(斯涅尔定律)
找到交点之后,最关键的一步就是确定光线的新方向。这里用到的就是斯涅尔定律的矢量形式。
为什么用矢量形式?因为在实际编程中,我们处理的是三维空间中的光线方向,用标量公式根本没法算。矢量形式简洁、高效,而且能统一处理折射和反射。
3.3.1 折射的矢量公式
设入射光线方向矢量为 I,表面法线矢量为 N,折射光线方向矢量为 T。折射率分别为 n₁ 和 n₂。
// 斯涅尔定律矢量形式
// n₁ * sin(θ₁) = n₂ * sin(θ₂)
// 计算折射光线方向
T = (n₁ / n₂) * I + [ (n₁ / n₂) * cos(θ₁) - cos(θ₂) ] * N
// 其中:
cos(θ₁) = -I · N
cos(θ₂) = sqrt(1 - (n₁ / n₂)² * (1 - cos²(θ₁)))
这里要注意,cos(θ₂) 必须是实数。如果根号内为负,说明发生了全反射——光线不会进入第二种介质。
3.3.2 反射的矢量公式
反射就更简单了,相当于折射中 n₂ = -n₁ 的特例。
// 反射光线方向
R = I - 2 * (I · N) * N
这个公式我用了无数次,几乎成了肌肉记忆。你想想看,反射其实就是把入射光线沿法线方向「镜像」过去。
关键点对比:
| 特性 | 折射 | 反射 |
|---|---|---|
| 光线方向 | 进入第二种介质 | 返回原介质 |
| 矢量公式 | T = (n₁/n₂)I + [ (n₁/n₂)cosθ₁ - cosθ₂ ]N | R = I - 2(I·N)N |
| 特殊条件 | 需检查全反射 | 无特殊条件 |
| 能量分配 | 透射光 | 反射光 |
3.4 完整追迹流程:一张图看懂
说了这么多,我们来画一张流程图,把整个光线追迹的脉络理清楚。
3.5 实际项目中的注意事项
理论讲完了,我们来聊聊实战中容易踩的坑。
- 精度问题:光线追迹是迭代过程,每一步的误差都会累积。我建议用双精度浮点数,尤其是在计算交点时。
- 边界情况:光线刚好擦过表面边缘、或者与表面相切时,判别式可能为 0。要单独处理这些情况。
- 全反射检查:在折射计算中,一定要先检查 cos(θ₂) 是否为实数。如果发生全反射,光线会全部反射回原介质。
- 性能优化:如果系统有几十个表面,每条光线都要做几十次交点计算。优化算法(比如用空间分割)能大幅提升速度。
我的习惯: 在写追迹代码时,我会先写一个「单表面追迹」的函数,测试通过后再封装成循环。这样调试起来方便很多。
嗯,这一章的内容就到这里。光线追迹是光学设计的基石,理解了逐面追迹法和斯涅尔定律的矢量形式,后面的像差分析、优化算法才能扎得稳。你可以在自己的代码里试着实现一个简单的球面透镜追迹,跑通之后,你会对光学系统有更直观的理解。