VirtualLab+Unity:折衍混合红外物镜三维交互仿真展示 1. 项目概述当光学仿真遇上交互引擎做光学设计仿真的人日常工作基本都绕不开 VirtualLab、Zemax 这类专业软件。而 Unity 这名字一出来大家第一反应基本是游戏开发。老实说把 VirtualLab 和 Unity 放在同一个项目里乍一听确实有点混搭但这恰恰是折衍混合红外物镜这类光学系统在做工程落地时一个非常实用的技术组合。这个项目的核心就是用 VirtualLab 完成折衍混合红外物镜的建模仿真再通过 Unity 搭建一个可交互的三维展示和验证环境。折衍混合红外物镜简单说就是把传统折射透镜和衍射光学元件DOE结合在一起的红外成像物镜。这种设计在红外光学系统里很常见因为它能在控制体积重量的同时很好地校正色差和热差。但问题来了光学设计软件里的仿真结果往往是二维的 MTF 曲线、点列图、波前图这些数据对于光学工程师自己看没问题可一旦要跟机械结构工程师、嵌入式工程师、甚至客户沟通就非常不直观。人家看不懂你这个 RMS 波前误差 0.05λ 到底意味着什么但如果你能在一个三维场景里让光路、镜片、像面实时显示还能鼠标拖拽旋转、切换视场、开关某一片透镜看光线变化那沟通效率完全是两个量级。这个项目的目标就是解决这个问题。我用 VirtualLab 搭好光学模型跑完仿真把镜片模型和光线路径的数据导出来在 Unity 里重建整个光机系统做成一个可交互的光学数字孪生。这篇文章我打算从选型逻辑、建模细节、Unity 实现、问题排查四个维度聊透适合手里有光学仿真基础、又想往三维可视化方向拓展的工程师参考。2. 为什么选折衍混合红外物镜做这个项目2.1 红外物镜设计避不开的两座山色差和热差做红外光学系统的人都知道红外材料的选择面很窄。中波红外常用 InSb 探测器配合硅Si、锗Ge、硒化锌ZnSe这些材料长波红外则基本是锗的天下。锗的折射率高、色散也大单用一片锗透镜色差和热差都很夸张所以红外物镜很少是单片结构基本都是多片式。但多片式也有问题。红外材料贵尤其是大尺寸的锗镜片价格跟直径基本是超线性关系。而且红外系统的应用场景往往是机载、弹载、手持热像仪这类对重量体积极度敏感的地方你不可能为了校正像差无限堆镜片。这时候折衍混合的优势就出来了。衍射光学元件DOE有一个非常独特的性质它的光焦度跟材料折射率无关只跟表面微结构有关而且色散特性是负的跟常规折射材料的正色散正好相反。把 DOE 放在一片折射透镜上可以在几乎不增加重量和厚度的前提下同时校正色差和热差。一片折衍混合透镜往往能顶两到三片传统球面透镜的效果。2.2 折衍混合红外物镜的典型参数我做这个项目时用的是一套典型的中波红外折衍混合物镜参数大家可以参考参数项数值工作波段3.7~4.8 μm中波红外焦距50 mmF 数2.0入瞳直径 25 mm视场2ω 10°全视场探测器320×256像元尺寸 30 μm 制冷型探测器镜片结构3 片折射透镜 1 片衍射面叠加在第二片透镜表面镜片材料硅、锗、硒化锌组合这个参数组合在实际工程里非常典型。F 数 2.0 是制冷探测器匹配的常见值保证了足够的能量收集能力焦距 50 mm 的视场角能覆盖常规搜索跟踪场景折衍混合的设计让 MTF 在奈奎斯特频率约 17 lp/mm处能保持在 0.4 以上衍射极限水平。2.3 为什么选 VirtualLab 而不是 Zemax很多人的第一反应是折衍混合镜片设计不是 Zemax 也能做吗为什么非要碰 VirtualLab这里我得说清楚。Zemax现在叫 OpticStudio当然能做折衍混合设计它的二元面、全息面模型对于初步设计阶段完全够用。但 VirtualLab 在处理衍射光学元件的微观结构方面有独特优势它内置了严格的电磁场传播算法Fourier Modal Method, FMM可以精确仿真 DOE 表面微结构比如台阶深度、周期、占空比对衍射效率的影响还能分析加工误差带来的效率损失。另一个重要原因是 VirtualLab 的场追迹Field Tracing概念。传统光线追迹假设光在所有介质中沿直线传播遇到表面才发生偏折而 VirtualLab 的场追迹是把光当成电磁场来传播可以精确处理光的偏振、相干、干涉和衍射效应。这对折衍混合系统尤其重要因为 DOE 的衍射效率跟波长、入射角、偏振都有关系传统光线追迹很难把这些效应完整地放进同一个模型里。当然VirtualLab 的建模方式跟 Zemax 差别很大它的逻辑更像构建一个物理光学实验台而不是填表式光学设计。上手门槛高一些但一旦跑通了对 DOE 这种非序列、含微结构的光学系统理解深度是 Zemax 给不了的。3. VirtualLab 建模与仿真核心环节3.1 镜片结构设计与模型搭建先用 Zemax 做初始结构设计拿到合理的镜片半径、厚度、玻璃材料这是常规做法。我可不是说 VirtualLab 不适合做初始设计而是 Zemax 的全局优化能力确实成熟先用它把结构搜出来效率高得多。把 Zemax 里的镜片数据拿到 VirtualLab 里建模时我用了 VirtualLab 的界面组件创建透镜元件。每个镜片的曲率半径、厚度、材料、口径都按设计值设置好。这里有一个关键细节折衍混合透镜在 VirtualLab 里需要把折射面和衍射面分开定义然后在同一个位置叠加。衍射面的参数不是直接输入相位函数系数就行而是要先在 VirtualLab 的光栅工具箱里设计 DOE 的微结构参数槽深、周期、台阶数或者直接用界面编辑器里的 Binary 面、谐波衍射面模型。我给项目里那片 DOE 设定的是 8 阶量化台阶、Kinoform连续浮雕近似型衍射结构。8 阶是加工成本和衍射效率之间一个很平衡的点再往上到 16 阶效率提升有限但加工成本翻倍往下到 4 阶效率损失明显一级衍射效率只有 81% 左右。3.2 衍射效率分析与温度建模仿真折衍混合系统的核心优势一半在光学设计另一半在 DOE 的衍射效率。DOE 的衍射效率决定有多少光能落在设计焦点上剩下的光散到其他衍射级次会形成杂散光背景直接影响成像对比度。我在 VirtualLab 里分别仿真了不同波长、不同入射角、不同温度下的一级衍射效率。用的方法是先建一个单周期的 DOE 单元用 FMM 求解它的衍射特性再扫描参数。这一步有个很重要的结论DOE 的效率曲线随波长和入射角都会变化在设计优化时不能只看中心波长的效率要加权整个波段和整个视场。红外系统还要额外关心温度特性。VirtualLab 里没有内置的热膨胀同步功能但可以手动改材料折射率和镜片曲率半径。锗的折射率温度系数 dn/dT 高达 396×10⁻⁶/°C是所有常用红外材料里面最高的这也是长波红外系统里锗单片化设计困难的根本原因。我仿真的方式是设置 -40°C、20°C、60°C 三个温度点分别提取折射率变化和面型变化在 VirtualLab 里重新跑一遍点列图和 MTF。3.3 仿真结果导出与定位仿真完不是终点把数据导出给 Unity 才是关键。我需要导出三类数据镜片的三维几何模型每片镜片的前表面、后表面、口径关键视场的光线路径不同视场角对应的光线在每片镜片表面的入射点、出射方向像面上的结果数据点列图坐标、MTF 数据用于 Unity 里做对比验证镜片几何我导出成 STL 格式。VirtualLab 里可以先生成镜片轮廓二维图再绕光轴旋转生成三维实体。光线路径我导出了每个视场从物面出发的 9 条特征光线边缘、0.7 口径、轴上每个位置取主光线和上下光线这些光线在各表面的交点坐标和方向向量就是 Unity 里画光线的基础数据。4. Unity 侧的场景重建与交互实现4.1 场景搭建与材质处理Unity 里搭建光学场景有两个坑需要提前说。第一个坑是坐标系统的差异。VirtualLab 里光轴方向是 Z 轴Unity 里默认相机朝向也是 Z 轴负方向这个要统一。我在导出数据时直接把所有坐标做了一次坐标系变换以 VirtualLab 的像面中心为 Unity 世界坐标原点光轴方向设为 Unity 的 Z 轴正方向这样后面摆放相机、探测器都非常直观。第二个坑是单位问题。VirtualLab 里所有尺寸单位是 mmUnity 的默认单位虽然文档写的是米但物理引擎的步长、光照的衰减距离都是按 1 单位 ≈ 1 米来算的。所以我在导入 STL 时统一做了一个 0.001 的缩放把毫米换算成米。如果不做这步镜片直径 25 mm 在 Unity 里会变成 25 米整个场景没法看。镜片的材质处理也值得说。红外镜片在成像波段是透明的但在可见光波段看起来就是深灰色甚至黑色的。为了展示效果我给镜片做了两套材质一套是物理正确的折射材质用来模拟光路另一套是外观着色器给镜片表面加上菲涅尔反射效果看起来像镀膜后的镜片。后处理上加了抗锯齿和环境光遮蔽整体观感提升很多。但我没有加高光反射和实时光追因为场景本身以教学展示和沟通为主不追求照片级真实感。4.2 光线可视化实现Unity 里画光线我一开始想的是用 LineRenderer但很快发现一个问题LineRenderer 是线渲染器只有 1 像素宽在复杂场景里光线会显得非常单薄。后来我改成用 Unity 的 Mesh Line 方案每条光线用一个细长的四边形 Mesh 来表示宽度可以控制颜色可以渐变在场景里很清晰。光线数据是从 VirtualLab 导出的每段光线的起点和终点决定了 Mesh 四边形的两个顶点对。我这里写了个光线管理脚本核心逻辑是public class RayVisualizer : MonoBehaviour { public GameObject raySegmentPrefab; public Material rayMaterial; public void DrawRays(ListRaySegment segments) { foreach (var seg in segments) { // 每条线段生成一个四边形 Mesh Vector3 start seg.startPoint; Vector3 end seg.endPoint; // 计算光线的方向 Vector3 dir end - start; // 垂直于光线的两个偏移方向用于生成四边形宽度 Vector3 up Vector3.up; Vector3 right Vector3.Cross(dir, up).normalized * seg.width; // 四个顶点 Vector3 v0 start right; Vector3 v1 start - right; Vector3 v2 end - right; Vector3 v3 end right; Mesh mesh new Mesh(); mesh.vertices new Vector3[] { v0, v1, v2, v3 }; mesh.triangles new int[] { 0, 2, 1, 0, 3, 2 }; mesh.RecalculateNormals(); GameObject go new GameObject(RaySegment); go.transform.SetParent(transform); MeshRenderer mr go.AddComponentMeshRenderer(); MeshFilter mf go.AddComponentMeshFilter(); mf.mesh mesh; mr.material rayMaterial; mr.shadowCastingMode UnityEngine.Rendering.ShadowCastingMode.Off; } } }这里有个细节每条光线都生成一个 GameObject 效率太低几百条光线还行几千条就会卡。我最终做了个材质合批和网格合并优化把同一颜色的多个光线段合并成一个静态 Mesh性能飙升。如果读者只是做展示用不用优化也能跑但如果要做实时交互、切换视场、动态更新光路合并网格就非常必要了。4.3 交互功能设计与参数联动Unity 场景的核心交互功能我做了以下几个鼠标拖拽旋转观察镜筒、镜片、整体装配用 Unity 的 Cinemachine 相机控制实现同时支持滚轮缩放。分层显示开关可以单独显示或隐藏镜片、镜筒、光线、探测器——这个对讲解调试非常实用。视场切换点击不同视场角按钮光线会实时切换到对应视场的光路高亮当前光线在镜片上的足迹footprint。点击镜片高亮并显示该镜片的设计参数曲率半径、厚度、材料这个功能我用最简单的方式实现给每片镜片加 BoxCollider用OnMouseDown事件触发 UI 面板。需要注意的是Unity 的 UI 动态画线这里有一个典型的坑如果用OnGUI画线或者用CanvasRenderer动态创建顶点坐标系的转换很容易算错。我最后采用的方式是在世界空间里生成 Mesh然后用Camera.WorldToScreenPoint把世界坐标投影到屏幕坐标再画 UI 上的标注线。原因是 UI 画线的需求往往伴随着标注文字、引线箭头这些都需要屏幕空间坐标。5. 关键问题排查与避坑心得5.1 许可证激活与工程协作问题做 Unity 开发的第一步很多人会卡在许可证上。如果你打开 Unity Hub 发现提示 No valid Unity Editor license found. Please activate your license.别慌分几种情况处理个人版免费许可去 Unity Account 官网申请个人版许可证然后在 Unity Hub 里登录并激活。个人版和商业版的区别在于年收入是否超过 20 万美元往上的就得买 Pro 许可。企业内网环境如果你在公司内网可能激活请求被防火墙挡住了。这种情况要么走 Unity Hub 的离线激活流程要么让 IT 放行 Unity 的授权服务器地址。团队协作如果同一个项目多人开发许可证是绑定个人账号的不要在公用的服务器上登录自己的账号容易被判 unlicensed machine 导致封禁。正确做法是每个开发者用自己账号激活工程通过 Git LFS 或 Perforce 共享。这里要顺带提一个 Unity 工程协作的隐藏坑Unity 工程目录里的Library文件夹会缓存大量本地生成的产物这个文件夹绝不能提交到版本控制里。我见过很多团队直接把整个工程目录拖进 Git结果合并冲突刷屏每次打开工程都卡半天。正确做法是用.gitignore过滤掉Library/、Temp/、Obj/、Logs/这几个文件夹。5.2 DLL 加载失败排查做 Unity 项目难免会碰到DllNotFoundException: Unable to load DLL这类报错。尤其在光学仿真工程里很多人会写 C 插件调用算法库然后把生成的 DLL 放在 Assets 目录下结果运行时依然报找不到。这种报错 90% 的原因不是 DLL 不存在而是 DLL 的依赖库没找到或者在 x86/x64 体系结构上不匹配。以我项目里用到的 slua一个 Lua 脚本插件举例如果你下载的是 slua 的二进制包一定得确认你导入的 .dll 是 Unity 目标平台对应的版本。Windows 编辑器下 Unity 是 64 位的如果误导入 32 位编译的 DLL就会毫无悬念地报这个错。排查步骤我总结一下打开 Unity 的 Console 窗口看完整报错信息里的 DLL 名称。用 Depends 或者dumpbin /dependents查看这个 DLL 依赖了哪些系统 DLL 和第三方 DLL。检查 DLL 是否被放在Assets/Plugins目录下并且Plugin Import Settings里的平台勾选是否正确。如果依赖的是 Visual C 运行库确认目标机器装有对应版本的 VC Redistributable。最后再排查是否被杀毒软件误删或隔离。这一步排查下来能解决绝大多数 DLL 加载问题。5.3 UI 动态画线的坐标问题与性能优化前面提过在 Unity 的 UI 系统里动态画线最容易出的问题是坐标系混淆。我在项目里一开始用了RectTransform.rect来取 Canvas 的宽高结果画出来的线全偏了。原因在于Unity UI 的坐标原点在 Canvas 的中心或者底部取决于 Canvas 的锚点和 pivot 设置而RectTransform.rect返回的是局部坐标。如果你的 Canvas 是 Overlay 模式从鼠标屏幕坐标转 UI 坐标必须用RectTransformUtility.ScreenPointToLocalPointInRectangle这个方法而不是自己手动做换算。这是我踩过最久的一个坑。另外一个问题是 UI 画线的性能。如果用Graphic类每帧重建顶点Draw Call 会飞涨。我后来的做法是把要画的线全部合并到一个 Cached Mesh 里然后用一个简易的自定义Raycaster做点击检测这样画 100 条线和画 1000 条线的开销差不多。5.4 场景光照与物体显示异常处理很多人在 Unity 里建立光学模型后会发现镜片渲染出来死黑一片或者反射效果怪怪的。原因通常是 Unity 默认的材质管线跟透明物体渲染不兼容透明物体默认不会参与深度写入导致透明物体之间的遮挡关系混乱。我的处理方案是给镜片使用自定义 Shader用 Unity 的 Surface Shader 做两层渲染深度预写和透明混合。同时把镜片的渲染队列设为Transparent3000。如果镜片之间互相遮挡出问题可以把镜片上的 Mesh Collider 打开用后处理CommandBuffer手动控制深度写入。在 WebGL 平台发布时帧率控制是一个绕不开的优化话题。Unity WebGL 默认启用全屏自适应分辨率有时候会把 GPU 拉满导致帧率反而下降。我在 Build Settings 里关闭了Auto Graphics API里的 VulkanWebGL 2.0 更稳然后把QualitySettings.vSyncCount设为 0手动用Application.targetFrameRate 60锁定帧率。实测下来这种配置在集成显卡笔记本上也能稳定 50 帧以上。5.5 视场光线缺失与焦点漂移问题最后分享一个 VirtualLab 导出数据到 Unity 后比较隐蔽的问题光线缺失和焦点漂移。光线缺失的原因往往是 VirtualLab 里设置光线的密度和口径不匹配。比如你设置了 9 条光线但其中几个视场角超出镜片口径那几条光线在某个表面会没有交点导出数据里就缺了这几段。我的解决办法是在 VirtualLab 里给每个视场单独设置光线口径用Field Trace时把Raytracing的光线采样扩展为 11×11 然后在导出时做一步数据补全如果某个视场的光线数量不足就用已存在的边缘光线插值生成完整的路径并在 Unity 里用虚线渲染提醒操作者这条光线是插值结果而非仿真结果。焦点漂移的问题则几乎都是因为坐标变换或者单位换算没有统一。我在项目里踩过的坑是VirtualLab 导出像面数据时默认的像面中心可能在最后一个表面顶点之前几毫米的地方而我在 Unity 里直接拿这个位置当探测器中心导致点列图中看起来像是系统存在严重离焦。排查方法很简单在 VirtualLab 里看Focus的位置导出时把Detector放在最佳焦面位置而不是默认像方焦面。6. 项目扩展方向和工程落地思考走到这一步折衍混合红外物镜的 VirtualLab 仿真和 Unity 交互展示基本已经打通。但我在实际项目中体会到这个方案的价值远不止做一个好看的演示它还可以向几个方向扩展这些方向都是我亲自尝试过或者正在推进的。6.1 向虚拟现实VR方向扩展如果你手头有 Pico 4 或 Quest 3把项目导出到这些设备上做沉浸式光学系统评估是完全可行的。我在一个子项目中做了 Pico 4 的适配核心工作有两块一是手柄射线交互替代鼠标点击二是把 UI 面板做成空间锚定面板放置在场景中固定位置。这里有一个值得注意的坑Pico 4 的 Unity 开发需要开启Pico XR扩展包并且在 Player Settings 里把Minimum API Level和Target API Level都提到 35。如果停留在默认值部分设备测试时会随机出现渲染穿透问题。而在虚拟现实环境里观察光学系统和平面屏幕完全不同视差效应能让你非常直观地感受到镜片间的空气间隔这对评审光学结构的紧凑性帮助很大。6.2 联动物理引擎做虚拟装调和公差分析另一个我在推进的方向是把 Unity 的物理引擎跟光学仿真的公差分析结合起来。传统公差分析是在 VirtualLab 里用蒙特卡洛随机采样生成一组组镜片偏移、倾斜、厚度误差然后统计 MTF 下降的分布情况。我可以把这组误差条件下的镜片位姿直接映射到 Unity 场景中生成一系列仿真快照再用脚本自动渲染成动画模拟装配偏差对光路的影响。这个思路的工程意义在于可以把光学系统和机械结构放在同一个空间里做干涉检查。在 Unity 里加载镜筒的 CAD 模型和镜片的位姿误差就能用碰撞检测算法提前发现某个误差组合下镜片边缘会不会碰壁。传统流程里这需要分别在光学软件和机械软件里做这次我把它整合到一个环境里省掉了大量的数据转手时间。6.3 与算法仿真结合做图像退化模拟折衍混合系统的杂散光特性也可以用 Unity 的渲染管线和后处理做模拟。DOE 不等于 100% 衍射效率残余级次的光会形成环形杂散光或鬼像。我计划把 VirtualLab 导出的杂散光点扩散函数PSF做成 Unity 的卷积后处理效果这样在一个三维场景里就能直观看到杂散光在探测器上叠加的效果。这个方向本质上是把光学 CAE 和游戏引擎的渲染能力做了一次桥接不需要真正实现严格的物理光传播只需要把光学软件计算好的 PSF 数据贴到屏幕渲染管线上就行。实施上比预想的简单效果却非常直观。6.4 工程落地的推进建议如果读者有意把这个方案落地到实际项目我的建议是按三个阶段走第一阶段只做静态展示把 VirtualLab 的数据导入 Unity做一个可以旋转观察、分层显示的演示场景这个是基础。第二阶段做交互验证加入视场切换、光线动态显示、参数面板联动让使用者在展示环境里就能查看光学系统的各个性能和结构细节。第三阶段才接入真实的 CAD 模型和公差数据实现光学机械联动分析这一步是最花时间但也是价值最高的。我见过不少团队在第一阶段就放弃了说这玩意不就是做个动画吗有什么意义。但以我的经验问题恰恰相反当你的光学项目需要跟非光学专业的人交流、评审、验收时一个能玩起来的三维光学系统比一百页仿真报告都有说服力。最后分享一个小技巧如果你打算长期维护这个方案建议把 VirtualLab 的导出做成批处理脚本而不是每次手动导出。VirtualLab 有 Python 脚本接口VyPython可以把镜头设计参数、光线采样、导出格式全部写成脚本这样你改一个镜片半径重新仿真的同时Unity 工程里的数据也能自动更新效率和准确性都会高很多。我按这个流程做完之后整个项目维护成本降低了大半而且同事再也不会拿着过期的仿真结果来找我讨论问题因为 Unity 场景里的数据和 VirtualLab 永远是一致的。