ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

MATLAB光线追踪实操:从数学原理到透镜光路模拟与调优

2026/9/21 1:08:08 拓冰建站 浏览量
MATLAB光线追踪实操:从数学原理到透镜光路模拟与调优 简介面向MATLAB开发者与计算机图形学学习者这套光线追踪实验方案提供了一套纯MATLAB实现的核心算法流程覆盖光线传播、物体求交、反射折射与阴影等关键环节不依赖硬件加速适合教学演示、算法验证或作为自研渲染器的原型参考。压缩包共18个文件以15个.m脚本为主包含场景构建、材质定义、相机设置与图像输出等模块另附1张渲染示例图、1份README说明及1个license文件包体仅163KB轻量易读。目前已有75人学习。通过代码可掌握金属、电介质、朗伯体等材质模型以及随机采样、Schlick近似等关键技术的具体实现并能对照代码理解数学公式与几何算法的映射关系清晰的文件划分和示例图也便于快速定位模块、调试参数在此基础上进一步扩展适合希望在MATLAB中深入实践计算机图形学的中高级用户。 MATLAB做光线追踪这事我在实验室里前后折腾了小半年从最开始只会用plot画几条直线到能稳定模拟一整套透镜组的光路中间踩的坑比想象中多得多。今天就把这套完整的东西整理出来包括数学原理、代码实现、参数调优和常见报错的排查思路给准备入坑Ray tracing的朋友一份能直接照抄的作业。这个项目说到底就是一件事用MATLAB模拟光线在介质中的传播路径包括直线传播、折射、反射以及和透镜、曲面这类光学元件的交点计算。它回答的核心问题是一个工程师经常要问的——“这束光经过这套光学系统之后到底会落在哪里、变成什么形状”。适合光学工程专业的同学、做光电检测的科研人员以及刚接触计算光学想找个练手项目的开发者下面全部按实操经验来写。1. 内容整体设计与思路拆解1.1 核心需求解析为什么光线追踪要选MATLAB光线追踪的底层逻辑并不复杂核心就是几何光学的三大定律光在均匀介质中沿直线传播、反射角等于入射角、折射满足斯涅尔定律。理论上这些直接用C或者Python都能写但MATLAB在这个场景里有几个天然优势我用下来感觉特别明显。第一个优势是向量化运算。一条光线在三维空间里就是一个起点加一个方向向量一束光就是几十上百条这样光线的集合。MATLAB的矩阵运算天生就是批量处理的逻辑不需要像C那样写显式的循环代码量能少一半以上。比如我要生成一束包含200条光线的平行光每一条都从物面出发、方向略有偏差用矩阵直接构造就是两行代码的事。第二个优势是可视化工具链完整。光线追迹题最核心的输出不是数值表格而是光路图。MATLAB的plot3、surf、quiver这几个函数组合起来能把每一条光线在透镜表面的偏折路径画得明明白白。我在调试曲面透镜的时候很多时候根本不用看具体数值就盯着图看光是不是汇聚在预期焦点附近效率极高。第三个优势是生态成熟包括内置的优化工具箱、曲线拟合工具箱以及Interpolation类的插值工具。在做实际的镜头设计时往往需要光线追踪配合优化算法比如改变曲率半径让焦点误差最小MATLAB里直接用lsqnonlin之类的最小二乘函数就可以在追迹过程外面套一层优化循环这个组合在纯C环境下搭起来会费劲得多。当然MATLAB的短板我也提一句它的循环效率确实低如果光线数量上万、元件数量几十个纯for循环追迹可能会卡到让人崩溃。解决办法有两个一是尽量把光线组织成矩阵用向量化运算替代循环二是做元件间传播时把整个光线的“批量”和“逐次交互”拆开只在必要的环节循环。1.2 典型应用场景与影响范围从透镜设计到波前分析MATLAB光线追踪的实际应用场景远远不止画一张漂亮的教科书式光路图。我做过、也见过别人做的项目至少有这几类透镜组系统设计多片透镜组合后的光路验证比如焦距、主平面位置、球差分布的计算。这个方向最适合入门练手因为系统相对简单验证方便。非成像光学设计比如LED配光透镜需要追踪几十万条光线并统计它们落在接收屏上的能量分布MATLAB可以在追迹结束后直接用histogram2生成照度分布图。波前分析与像差评价对光线追迹得到的各条光线在像面处的坐标和光学路径长度进行处理可以重构波前相位。网上搜“matlab光学追踪实现波前”就是这类项目本质上是把几何光线数据反过来推光学系统的像差。与外部光学软件联动MATLAB可以写脚本批量调用TracePro这类专业软件做蒙特卡洛追迹再用MATLAB做前后的数据处理和可视化。我自己的感觉是MATLAB适合做“算法验证层”和“数据后处理层”而真正大规模的商业级追迹运算由专业软件完成。这不是说MATLAB做不了大规模叠加而是说它在工程流程里的最佳定位是快速验证和结果分析中心。理解这一点你就不会一上来就想着和Zemax拼速度而是把它当作设计链路里的高效节点。2. 核心细节解析与实操要点2.1 光线传播的数学表达与常见误区每条光线在三维空间中用一个起点和一个方向向量定义这很好理解。真正容易被新手忽略的是方向向量的归一化问题这一点原理很简单但影响很大。光是这么定义的假设光线的起点是P0 [x0, y0, z0]方向向量是D [dx, dy, dz]那么光线上的任意一点可以写成P(t) P0 t * D其中t是传播参数表示沿方向走了多远。这里的D最好归一化成单位向量t就直接对应实际传播距离。这样在计算“这条光线到下一个元件的距离是d”的时候直接拿t就能表达物理距离不需要再做一次向量长度换算。我刚开始写的时候就没有归一化方向向量结果在计算交点的时候总是差一个系数光路图画出来歪歪扭扭的。后来排查了半天才发现问题是方向向量长度不统一导致的。所以有一条实操纪律所有光线方向向量在定义时必须统一归一化。可以用一行代码批量完成D D ./ sqrt(sum(D.^2, 2));2.2 透镜表面的交点计算从平面到球面的递进计算光线和光学表面交点是整个光线追踪里最核心的几何运算也是出错率最高的地方。我们按从易到难的顺序来。最简单的平面比如接收屏、平面反射镜。平面用点法式表示给定平面上一点P_plane和法向量N光线和平面相交的条件是(P(t) - P_plane) · N 0。把光线方程代入得到t (P_plane - P0) · N / (D · N)这里有一个除法陷阱如果分母D · N接近0说明光线几乎平行于平面要么没有交点、要么无限多个交点代码里必须加一个容差判断直接跳过否则会出现除以零的警告甚至复数结果。球面稍微复杂一点也是最常见的透镜表面形式。设球心为C半径为R光线与球面相交的条件是|P0 t*D - C|^2 R^2展开后是一个关于t的一元二次方程|D|^2 * t^2 2 * D · (P0 - C) * t (|P0 - C|^2 - R^2) 0因为方向向量归一化后|D|^2 1方程简化成标准的t^2 2b t c 0判别式Δ b^2 - c。如果Δ 0说明光线没碰到球Δ 0是相切Δ 0存在两个交点一般是取较小的正t代表光线进入球面的那个点。我在写球面交点的过程中总结了一个经验一定要把判别式小于0的情况单独处理而不是等它自然算出复数然后报错。因为在光线追迹中一束光里总有那么几条打在透镜边缘外面、直接飞走的这是正常物理现象不应该因此让程序崩溃。2.3 折射计算斯涅尔定律的向量形式做完交点之后下一步就是计算折射方向。斯涅尔定律的标量形式是n1 * sin(θ1) n2 * sin(θ2)这个初中物理就学过。但在三维空间里光线的方向不仅仅是角度问题还涉及绕一个轴旋转所以必须要用向量形式的折射定律。设入射方向D、表面法向量N指向入射介质一侧、入射介质的折射率n1、折射介质的折射率n2折射方向T用如下公式计算cosI -dot(D, N); sin2T (n1 / n2)^2 * (1 - cosI^2); if sin2T 1 % 全反射 T D - 2 * dot(D, N) * N; % 按反射处理 else T (n1 / n2) * D ((n1 / n2) * cosI - sqrt(1 - sin2T)) * N; T T / norm(T); end这段代码里有个隐含条件就是dot(D, N)的符号必须准确。判断标准是如果光线从空气进入玻璃D和N的夹角应该是钝角即dot(D, N) 0如果cosI出现正数说明法向量方向取反了需要翻转N。这个方向问题我当初调了整整一个下午最后发现只是曲面法向量的朝向在球面内外侧之间不一致导致的。解决办法是统一约定法向量永远从光密介质指向光疏介质一侧或者永远指向入射光线来源一侧在代码里保持一套标准不要变来变去。3. 实操过程与核心环节实现3.1 一个完整的平行光入射球面透镜案例说了这么多理论直接上一个能跑通的完整案例。这个案例实现的是一束平行光多条光线沿Z轴正向传播遇到一个球面凸透镜后发生折射最终会聚到焦点附近。镜头参数玻璃折射率 n 1.5168BK7玻璃常用值透镜前表面为球面球心在 Z 300半径 R 50透镜厚度 d 10mm后表面为平面物方介质是空气折射率为 1完整的核心代码如下% 参数设置 clear; close all; clc; n_air 1.0; n_glass 1.5168; R 50; C [0, 0, 300]; thickness 10; N_rays 21; % 生成平行光线入射高度从 -20 到 20 mm ys linspace(-20, 20, N_rays); P0 [zeros(N_rays,1), ys, zeros(N_rays,1)]; D repmat([0, 0, 1], N_rays, 1); % 追迹每条光线 figure; hold on; axis equal; grid on; xlabel(X (mm)); ylabel(Y (mm)); zlabel(Z (mm)); for k 1:N_rays p0 P0(k,:); d D(k,:); % 计算与前球面的交点 b dot(d, p0 - C); c dot(p0 - C, p0 - C) - R^2; delta b^2 - c; if delta 0, continue; end t1 -b - sqrt(delta); P_front p0 t1 * d; % 计算前表面法向量球面外法向取从球心指向点 N_vec (P_front - C) / R; % 折射 cosI -dot(d, N_vec); sin2T (n_air/n_glass)^2 * (1 - cosI^2); if sin2T 1, continue; end d_refr (n_air/n_glass) * d ... ((n_air/n_glass) * cosI - sqrt(1 - sin2T)) * N_vec; d_refr d_refr / norm(d_refr); % 传播到后表面平面z C(3) - thickness z_back C(3) - thickness; t2 (z_back - P_front(3)) / d_refr(3); P_back P_front t2 * d_refr; % 后表面折射回空气 N_back [0, 0, 1]; cosI2 -dot(d_refr, N_back); sin2T2 (n_glass/n_air)^2 * (1 - cosI2^2); if sin2T2 1, continue; end d_out (n_glass/n_air) * d_refr ... ((n_glass/n_air) * cosI2 - sqrt(1 - sin2T2)) * N_back; d_out d_out / norm(d_out); % 延长到接收平面 z 350 t3 (350 - P_back(3)) / d_out(3); P_end P_back t3 * d_out; % 画光路 plot3([p0(1), P_front(1)], [p0(2), P_front(2)], [p0(3), P_front(3)], b-); plot3([P_front(1), P_back(1)], [P_front(2), P_back(2)], [P_front(3), P_back(3)], r-); plot3([P_back(1), P_end(1)], [P_back(2), P_end(2)], [P_back(3), P_end(3)], g-); plot3(P_end(1), P_end(2), P_end(3), ko, MarkerSize, 3); end这段代码跑出来后你会看到红色光路在透镜内部是收拢的绿色光路从后表面出来后进一步收拢向一个点汇聚。由于球差的存在边缘光线高度±20mm的焦点会比近轴光线高度0附近的焦点略微靠近透镜这就是球差在光路图上的直观体现。3.2 参数选择逻辑与计算过程补充认真看这段代码会发现有几个参数的选择是值得讨论的。ys linspace(-20, 20, N_rays)高度范围选±20mm镜面的有效半口径大约是50mm所以这束光全部打在透镜的有效区域内。N_rays选21条是为了结果图清晰如果数量太少光路的汇聚趋势不明显如果太多比如200条图上会糊成一团黑色区域反而不利于观察球差。球心位置选C [0, 0, 300]、半径50mm意味着前表面的顶点在Z250处因为300-50250透镜后表面在Z250-10240处代码里z_back C(3) - thickness。这样透镜位于Z240~250区间平行光先到达前表面在球面上发生偏折经过10mm厚度后在后表面再次折射最后在Z大约300附近汇聚成焦点让整个光路图留在可视范围内。接收平面放在Z350是为了能看到光线在后表面之后的完整传播路径。如果放在焦点位置大约是300附近汇聚点会正好落在接收屏上但光路图上面后面出来的光线就短了很多看起来不够直观。我一般会在调试时把接收屏放在焦点附近看光斑的大小来判断追迹是否正确而在出展示图时放到稍远的位置让整个光路完整呈现。3.3 可视化增强与数值验证光画光线还不够我还会在图上叠加透镜轮廓。球面部分用sphere函数生成单位球面缩放半径并平移到球心然后只保留Z在240到250之间的曲面片作为透镜的前表面后表面直接用平面近似画在Z240处。这样做出来的图光路和镜体是一体的汇报答辩的时候很加分。数值验证方面我强烈建议对每条光线在接收面上的坐标做一次统计。把P_end的x、y坐标分别存入数组计算它们的标准差。标准差小说明光斑集中焦点质量好如果标准差很大多半是某个表面的折射方向算错了或者法向量朝向了反方向。实操中我先用disp打印第1条光线、第11条光线的所有中间变量cosI、sin2T、t1、t2等人肉检查这些值是否物理合理再决定是否继续调试。% 统计接收面上的光斑半径距离中心轴的距离 r_end sqrt(P_end_x.^2 P_end_y.^2); fprintf(RMS spot radius: %.4f mm\n, sqrt(mean(r_end.^2)));这个RMS光斑半径就是后续做优化时的目标函数。如果你想把焦距调到某一个特定值就让所有光线到焦点位置的距离之和最小从而反推球面的曲率半径R。这个思路就是镜头自动设计的雏形。4. 工具选型与MATLAB生态搭配4.1 自写脚本 vs 专业工具箱 vs 外部软件联动入坑MATLAB光线追踪第一步就会面临工具选型问题。我自己做过的方案有三种适合的场景完全不同。纯自写脚本是最灵活的方式代码完全可控适合学习和定制化需求。比如我们前面写的这套代码就属于这一类。缺点也很明显如果要做非常复杂的非序列光学系统光线数量大、散射模型复杂代码量会急剧膨胀这时候自写脚本的维护成本很高。MATLAB官方没有专门的光学追迹工具箱但有一些相关的比如“Partial Differential Equation Toolbox”可以做些波动光学模拟不过对几何光学追迹的直接帮助有限。在MATLAB生态里更多是社区工具箱比如OptiNanoscopy、PSF相关工具等用于特定光学问题。如果你要做的是像差分析、PSF计算可以搜一搜MATLAB File Exchange上现成的代码能省很多事。外部软件联动则适用于严肃的光学设计。比如调用TracePro、Zemax的接口把设计参数传给这些软件做蒙特卡洛光线追迹再把结果读回MATLAB做数据处理。这里我建议的方式是用MATLAB的system命令调用可执行文件或者通过COM接口。虽然配置起来略麻烦但好处是前后端分离各用各擅长的领域非常稳。4.2 我推荐的工程化项目结构如果你准备认真做一个光线追踪项目不太建议把所有代码塞在几个.m文件里。我个人的项目结构一般是这样的src/核心追迹函数比如raySphereIntersect.m、rayPlaneIntersect.m、refractVector.m、traceSystem.mconfig/系统参数定义脚本包括所有元件的坐标、折射率、曲率半径统一放在一个结构体里examples/各场景的入口脚本比如demo_singleLens.m、demo_doubleLens.mutils/可视化辅助函数比如plotLensSurface.m、plotRays.mresults/输出图片和数据这么拆之后最大的好处是调参不需要到处改代码。比如把透镜的曲率半径从50改成80只改config里的一个字段其他函数完全不用动。这个习惯我一开始没有导致项目中期改一次参数要同步改三个文件非常痛苦。MATLAB的classdef可以做面向对象的光学系统建模但这对于小项目来说过度设计。我的建议是百行以内的脚本就直接写过程式代码项目到几千行规模再考虑模块化和类封装不要一上来就搞复杂架构容易把自己绕晕。4.3 性能优化从循环到向量化再到并行当光线数量上升到上千条时MATLAB的循环性能问题就凸显了。我实测一个纯for循环逐条追迹的场景1000条光线追迹20个表面大约耗时15秒虽然能接受但反复调试参数时就很烦了。最直接的优化方式是把所有光线组成一个大矩阵每次和光表面的交点计算都作用于整个矩阵而不是一条一条算。用矩阵运算重写后同样1000条光线追迹时间可以降到2秒以内。用tictoc实测是质的飞跃。MATLAB的并行计算工具箱parfor也可用于光线追迹前提是每条光线之间没有数据依赖这让光线追踪天然适合并行。但要注意并行池每次启动有额外开销几百条光线的场景可能还没串行快建议至少上千条光线时再考虑parfor。我在代码里习惯写一个总开关useParallel true; if useParallel parfor k 1:N_rays [P_end(k,:), valid(k)] traceOneRay(P0(k,:), D(k,:), sys); end else for k 1:N_rays [P_end(k,:), valid(k)] traceOneRay(P0(k,:), D(k,:), sys); end end这样两种模式可以随时切换方便对比速度和正确性。5. 常见问题与排查技巧实录5.1 典型报错和异常现象速查表现象可能原因排查方法delta 0导致光线全部跳过球心、半径设置不合理或方向向量方向反了打印第一条光线的p0-C点积和半径对比光路图在某个表面突然断裂发生了全反射光线没有进入下一个介质检查sin2T是否超过1检查入射角是否过大焦点位置明显偏离理论焦距法向量方向不统一折射方向角度计算错误用单条光线手算一遍逐步核对每个中间量出现Inf或NaN方向向量分量接近0导致除法溢出比如水平光线穿不过竖直平面加一个小量保护例如分母绝对值小于1e-12时跳过光斑形状不对称光线采样分布不均匀或透镜没有对准光轴检查初始ys取值是否对称分布检查球心Z坐标和光轴是否一致计算结果和Zemax对不上折射率波长对应关系、坐标正方向定义不一致统一坐标系Z轴正向设为光传播方向Y轴向上X轴依右手定则排查时有一个通用招数选取一条特殊光线比如沿光轴传播的那条它的折射方向应该保持不变。如果这条不对说明折射公式本身有误如果这条对了而其他光线不对说明问题出在曲面法向量的计算上。这个排除法帮我快速定位过好几次Bug。5.2 调试心得逐条打印中间变量是我最推荐的做法说实话我最开始调试这类程序时喜欢直接在plot上找毛病。但光线路径在三维空间里眼睛非常容易被干净的曲线带偏误以为“图形很顺就对了”。后来我的调试顺序调整为用format long打开高精度显示。选3条特殊光线近轴光线、离轴中等高度的光线、边缘光线。对每条光线打印它经过每个表面之前的cosI、sin2T、折射后的方向向量和手动理论值比对。全部通过后再整体画图。以第11条中心光线为例平行光沿Z轴入射入射角应为0cosI 1前表面折射后方向应保持沿Z轴不变因为正入射不偏折后表面同理。如果中心光线在图上发生了弯折那一定不是物理问题而是代码逻辑错了。这条检查能过滤掉一大部分低级错误。另一个容易忽略的是数值精度。MATLAB默认双精度但两次相减相近大数时会损失有效数字。比如某个交点坐标很大减去距离很近的小量时精度会下降。这种情况尽量用d abs(b) - abs(c)这类避免减法抵消的形式或者直接用更高精度的vpa不过vpa太慢一般不是必须。5.3 环境配置与工具箱版本建议MATLAB做光线追踪其实只需要基础环境加一个Optimization Toolbox做优化时用和一个Parallel Computing Toolbox做大规模追迹时用此外就没什么特殊依赖了。版本方面我目前用的2023a2021以上的版本基本都够用函数接口兼容性很好不用担心旧代码跑不起来。至于网上那些关于“matlab 2025b密钥”、“matlab下载安装教程”的搜索词我只能提醒一句尽量走学校或单位的正版授权渠道MATLAB官方对学生和教师有很优惠的许可证用正版工具在科研和答辩环节会省掉很多麻烦。如果你计划调用TracePro等外部软件做联合仿真还需要注意MATLAB版本和TracePro版本的Bit位数一致32位配32位64位配64位否则COM接口会莫名失败。这个问题在我同事的机器上出现过排查了很久才发现是版本位数不匹配。6. 从球面透镜到波前分析的进阶方向6.1 输出相位信息与波前重构光线追踪得到的最基本信息是光线路径和交点坐标但通过路径信息可以进一步获得光学系统的波前信息。每一条光线从物面出发到像面的光程Optical Path LengthOPL都可以算把每段介质的几何路径长度乘以对应的折射率累加。如果系统没有像差所有光线的光程相等波前是理想球面由于像差的存在不同高度的光线光程并不一致波前就会有变形。在MATLAB里算出每条光线的OPL后再根据入射高度和出射坐标重构波前相位这其实已经不涉及复杂的物理模拟本质是一个数学映射光程差除以波长乘上2π就是相位。很多时候在项目报告中这一张波前分布图比一百条光路的图更能说明问题。我当时做这个方向的原因很简单只画光线和透镜导师觉得像“初中物理”但把相位信息加进去论文的深度和说服力都会上一个台阶。实际操作中用scatteredInterpolant把离散光线对应的相位插值到网格上再画三维曲面图效果很不错。6.2 随机光线采样与蒙特卡洛方法在做照明设计时一束光不可能是理想平行光或者理想点光源而是带有角分布和空间分布的光源。这时就要用随机数生成器来模拟光源的空间位置和发射角度对每一条“携带能量”的光线进行追迹最后统计接收面上的能量密度分布。MATLAB里生成随机方向和随机位置都不难但要注意伪随机数的种子。默认的rng每次运行都会变化如果想让实验可复现就要在代码开头写rng(42)固定随机种子否则哪怕只改了一行无关代码结果可能完全不同这也是调试时一个让人抓狂的坑。蒙特卡洛方法收敛速度是1/sqrt(N)所以想提高两倍精度需要四倍光线数量。这也是为什么在照明仿真中动辄需要十万甚至百万条光线数量不够的话统计结果会有明显的散斑噪声。如果只是验证算法可以先用几千条跑通流程再逐步加量。6.3 多透镜组与像差优化一个球面透镜搞定了多透镜组其实只是把单透镜流程复制几遍。但多透镜组的关键难点不是追迹本身而是参数空间变大后如何找到最优的透镜参数组合。这里的“最优”通常是在某个视场范围内让所有视场的光斑半径平方和最小。MATLAB的fmincon或lsqnonlin可以用于这个优化问题设计变量可以是每个表面的曲率半径、厚度、空气间隔等。值得注意的是目标函数本身往往有很多局部极小值直接丢给优化器很容易陷入不好的解。我的经验是多起点搜索随机生成几十组初始参数分别优化然后取最优解。这个策略虽然笨但非常有效。优化和光线追踪结合还有一个好处可以直接在每次迭代后把光路图画出来直观看到优化器在怎么调整。这种交互式调试体验我在其他编程环境里很少见到。写在最后的实操感想做了这么久的MATLAB光线追踪最大的体会是这个研究方向的门槛不在编程而在对每一步物理意义的清醒认识。很多次程序跑出莫名其妙的图回到纸上画一遍草图就立刻发现是法向量方向弄反了。代码只是把物理思想的逻辑翻译成机器语言思想错了代码怎么调都是错的。如果你现在刚开始接触建议按这个顺序走先把单球面折射吃透再扩展到多个表面再尝试光源建模和优化。不要一上来就想着做一个完整的镜头设计系统那样反而会让人在复杂的参数调整中迷失方向。光线追踪是一门越做越有成就感的学问因为每一条光线的路径都是你亲手对物理规律的精准复现。本文还有配套的精品资源点击获取