2、几何光学基础:光线模型、反射定律与折射定律、斯涅尔定律、全反射条件
各位同学,咱们今天聊几何光学。说白了,就是研究光怎么走直线、怎么拐弯、怎么被挡住的一套理论。我刚开始做光学仿真那会儿,觉得这东西太简单了,不就是画几条线嘛。结果真上手做复杂光路系统时,才发现基础不牢,后面全是坑。
好,咱们一步步来。
2.1 光线模型:把光想象成一根线
几何光学里,我们不考虑光的波动性,也不管它是什么电磁波。我们只做一件事——把光抽象成一条带箭头的直线。这条线代表能量传播的方向。
核心假设:在均匀介质中,光沿直线传播。遇到不同介质的界面时,才会发生反射或折射。
这个模型虽然粗糙,但在绝大多数工程场景下够用了。我做过一个激光准直系统仿真,用光线模型算出来的光斑位置,和实测结果误差不到 0.1mm。所以别小看这几条线。
光线模型有几个关键要素:
- 光线方向:用单位向量表示,指向能量传播方向
- 光线起点:光源位置或前一个界面的出射点
- 光线颜色/波长:决定折射率,影响后续行为
- 光线强度:在纯几何模型中常忽略,但做能量分析时要用
我的习惯:写仿真代码时,我会把光线定义成一个类,包含起点坐标、方向向量、波长三个属性。方向向量一定要归一化,不然后面算交点时会出各种奇怪的问题。
2.2 反射定律:入射角等于反射角
这个大家初中就学过。但做仿真时,我建议你用向量形式来理解,而不是只记角度。
反射定律的向量表达式:
r = i - 2 * (i · n) * n
其中:
- r:反射光线方向向量
- i:入射光线方向向量(指向界面)
- n:界面法向量(指向入射介质一侧)
- ·:点积运算
为什么用向量?因为写代码时,你不可能每次去算角度再转回来。直接用向量运算,一行代码搞定。
我曾经踩过的坑:法向量方向搞反了。反射光线直接射到介质内部去了,仿真结果完全不对。记住:法向量要指向入射光线所在的那一侧。
2.3 折射定律与斯涅尔定律
光从一种介质进入另一种介质时,传播方向会改变。这就是折射。定量描述这个现象的,就是斯涅尔定律。
公式很简单:
n₁ * sin(θ₁) = n₂ * sin(θ₂)
其中:
- n₁、n₂:两种介质的折射率
- θ₁:入射角(光线与法线的夹角)
- θ₂:折射角
折射率是啥?说白了就是光在真空中的速度除以在介质中的速度。空气的折射率约等于 1.0003,一般工程计算直接取 1。水的折射率约 1.33,玻璃在 1.5 左右。
| 介质 | 折射率(约值) | 常见应用场景 |
|---|---|---|
| 真空 | 1.0000 | 理想参考基准 |
| 空气 | 1.0003 | 大多数地面光学系统 |
| 水 | 1.3330 | 水下成像、海洋光学 |
| 普通玻璃 | 1.5000 | 透镜、棱镜 |
| 蓝宝石 | 1.7700 | 高硬度窗口、LED衬底 |
| 金刚石 | 2.4200 | 特殊光学窗口 |
折射定律的向量形式稍微复杂一点,但写代码时更实用:
t = (n₁/n₂) * i + [ (n₁/n₂) * cos(θ₁) - sqrt(1 - (n₁/n₂)² * (1 - cos²(θ₁))) ] * n
其中 cos(θ₁) = - (i · n)。注意这里 i 指向界面,n 指向入射介质。
我建议:写仿真代码时,先判断根号里的值是否大于 0。如果小于 0,说明发生了全反射,这时候就别算折射了。
2.4 全反射条件
光从光密介质射向光疏介质时(比如从玻璃射向空气),如果入射角大于某个临界值,光就不会进入第二种介质,而是全部反射回去。这就是全反射。
临界角公式:
θ_c = arcsin(n₂ / n₁)
当 n₁ > n₂ 时,这个公式才有意义。如果 n₁ ≤ n₂,永远不会发生全反射。
举个例子:光从玻璃(n=1.5)射向空气(n=1.0),临界角约为 41.8°。入射角超过这个值,光就出不去了。
工程应用:光纤通信就是靠全反射。光在光纤芯层里不断全反射,才能传输几公里甚至上百公里。我做光纤耦合仿真时,全反射条件判断错了,结果光全部泄漏出去,仿真结果完全不能用。
全反射的判断流程:
- 判断是否从光密到光疏(n₁ > n₂)
- 计算临界角 θ_c = arcsin(n₂ / n₁)
- 比较入射角 θ₁ 与 θ_c
- 若 θ₁ ≥ θ_c,发生全反射,只计算反射
- 若 θ₁ < θ_c,正常计算反射和折射
2.5 知识体系总览
下面这张图把本章的核心逻辑串起来了。你写仿真代码时,按这个流程走,基本不会漏掉关键步骤。
2.6 仿真代码示例
最后,给你一段 Python 代码。这段代码实现了光线与平面界面的交互,包含反射、折射和全反射判断。我自己的仿真工具包里,核心逻辑和这个差不多。
import numpy as np
def trace_ray(origin, direction, normal, n1, n2):
"""
光线与平面界面交互
origin: 光线起点 (3,)
direction: 光线方向 (3,),已归一化
normal: 界面法向量 (3,),指向入射介质
n1: 入射介质折射率
n2: 出射介质折射率
返回: (反射方向, 折射方向, 是否全反射)
"""
# 确保方向向量归一化
direction = direction / np.linalg.norm(direction)
normal = normal / np.linalg.norm(normal)
# 计算入射角余弦
cos_theta1 = -np.dot(direction, normal)
# 反射方向
reflect_dir = direction - 2 * cos_theta1 * normal
# 判断是否全反射
ratio = n1 / n2
sin_theta1_sq = 1 - cos_theta1**2
discriminant = 1 - ratio**2 * sin_theta1_sq
if discriminant < 0:
# 全反射
return reflect_dir, None, True
# 折射方向
cos_theta2 = np.sqrt(discriminant)
refract_dir = ratio * direction + (ratio * cos_theta1 - cos_theta2) * normal
return reflect_dir, refract_dir, False
# 示例:光从玻璃射向空气
origin = np.array([0.0, 0.0, 0.0])
direction = np.array([0.0, -1.0, 0.0]) # 垂直入射
normal = np.array([0.0, 1.0, 0.0]) # 法线向上
n1, n2 = 1.5, 1.0
r_dir, t_dir, is_total = trace_ray(origin, direction, normal, n1, n2)
print(f"反射方向: {r_dir}")
print(f"折射方向: {t_dir}")
print(f"是否全反射: {is_total}")
我建议:跑这段代码时,试试把入射方向改成斜入射,比如 direction = [0.707, -0.707, 0],看看折射角的变化。再试试入射角大于 41.8° 的情况,你会看到全反射被触发了。