3. 光线追迹算法:逐面追迹法、交点计算与斯涅尔定律

光线追迹,说白了就是让一根虚拟的光线在光学系统里「跑」一遍。我刚开始做这个的时候,总觉得不就是画几条线嘛,有什么难的?直到第一次自己手算一个三片式镜头,才发现这里面的门道比想象中深得多。

这一章,我们就来聊聊光线追迹的核心算法。我会把我在项目中踩过的坑、总结的经验,一并讲给你听。

3.1 逐面追迹法:光线是怎么「走」过系统的?

光线追迹的基本思路很简单:从物点出发,依次计算光线与每个光学表面的交点,然后根据表面特性(折射或反射)改变光线的方向,再继续追迹下一个表面。

这个过程,我们称之为逐面追迹法。你想想看,就像接力赛跑一样,光线在每一个表面处「交接棒」——位置和方向被更新,然后传给下一个表面。

核心流程:

  1. 初始化:给定光线的起始点坐标 (x₀, y₀, z₀) 和方向余弦 (L₀, M₀, N₀)。
  2. 找交点:计算光线与当前光学表面的交点 (x, y, z)。
  3. 算法线:求出交点处表面的法线方向。
  4. 判方向:根据斯涅尔定律(折射或反射),计算光线的新方向余弦。
  5. 传下去:将新位置和新方向作为下一表面的输入,重复步骤 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(θ₂) 是否为实数。如果发生全反射,光线会全部反射回原介质。
  • 性能优化:如果系统有几十个表面,每条光线都要做几十次交点计算。优化算法(比如用空间分割)能大幅提升速度。

我的习惯: 在写追迹代码时,我会先写一个「单表面追迹」的函数,测试通过后再封装成循环。这样调试起来方便很多。

嗯,这一章的内容就到这里。光线追迹是光学设计的基石,理解了逐面追迹法和斯涅尔定律的矢量形式,后面的像差分析、优化算法才能扎得稳。你可以在自己的代码里试着实现一个简单的球面透镜追迹,跑通之后,你会对光学系统有更直观的理解。