VirtualLab与Unity联动:F-Theta扫描物镜激光打标数字孪生可视化实践

发布时间:2026/9/15 2:59:14
VirtualLab与Unity联动:F-Theta扫描物镜激光打标数字孪生可视化实践 做工业光学仿真和数字孪生相关项目这几年VirtualLab 和 Unity 这个组合我啃了不少时间。刚接触 F-Theta 扫描物镜时几乎所有仿真结果都堆在专业软件里一张张二维光斑图、一条条场曲曲线自己看得懂客户和产线工程师却很难直观理解。后来我把 VirtualLab 的仿真结果搬进 Unity 场景用三维光束路径和实时交互把它变成一台“虚拟激光打标机”整个项目的沟通效率一下就上来了。这篇文章就从 F-Theta 扫描物镜的原理讲起聊一聊 VirtualLab 里怎么建模、哪些数据要导出、Unity 端怎么做光路可视化与交互最后分享我在实际调试中踩过的坑和排查经验。适合光学工程师、Unity 开发以及想做工业数字孪生演示的朋友参考。1. 项目整体思路为什么是 VirtualLab 加 Unity而不是换一个“可视化更强的光学软件”1.1 F-Theta 扫描物镜到底特殊在哪里先快速对齐一下概念。F-Theta 物镜是激光扫描系统中很常见的镜头激光打标、雕刻、钻孔、选择性熔融这些设备里都有它。普通镜头的成像关系通常是像高 y 与视场角 tan(θ) 成正比也就是 y f·tan(θ)而 F-Theta 物镜刻意设计成 y f·θ这里的 θ 是以弧度为单位的扫描角度。你可能觉得差异不大但在大角度扫描时两者差别非常明显比如扫描角 ±20°tan 关系和线性关系在边缘位置能差出几个毫米到十几毫米直接体现在加工图形的畸变上。打标行业要的是“振镜转多少角度光点就均匀移动多少距离”这样控制器才能用线性映射关系直接生成扫描轨迹不用做复杂的非线性补偿。F-Theta 物镜正是为了实现这种等速扫描而存在的所以它也被称为平场扫描物镜。设计时除了焦距、工作波长、入瞳直径这些基础参数还要重点看 f-Theta 畸变、像面平坦度和光斑一致性这三个指标直接决定了扫描出来的图形边界是否变形、边缘光斑是否散焦。1.2 VirtualLab 在光路仿真上的角色VirtualLab 是物理光学仿真软件不是单纯的几何光线追迹工具它最擅长的是将电磁场视为矢量场并做场追迹所以能给出比光线追迹更丰富的信息焦平面光斑的强度分布、相位分布、偏振态变化、衍射效应、像差对光斑形状的影响等。对 F-Theta 物镜来说设计阶段最关心的就是不同扫描角下的像面光斑质量几何光学可以给你一个大致的弥散斑半径但物理光学能告诉你光斑内部能量怎么分布、峰值强度在哪、旁瓣有多大。后者才是真正决定激光加工效果极限的东西。在实际项目里我在 VirtualLab 中建立的模型包括激光光源、两个正交放置的振镜、F-Theta 物镜组以及像面探测器。振镜用一个折叠坐标系模拟每个振镜对光束施加不同方向的偏转物镜组可以从设计软件导入也可以使用 VirtualLab 自带的镜头库或者自定义面型。仿真的输出包括像面坐标、光斑尺寸、辐照度分布图和系统光路图这些数据就是后面 Unity 可视化的“原料”。1.3 Unity 负责的是“让仿真结果被人理解”Unity 在这里不是用来替代 VirtualLab 做光学计算的它承载的是场景重建、交互操作和结果演示。简单说VirtualLab 算出的是一条条经得起推敲的物理光束Unity 把它们变成三维修烁的光路、跳动的扫描轨迹和可调参数的操作面板。这种分工在数字孪生项目里尤其明显一台激光打标机如果要做成虚拟调试台控制逻辑、运动轨迹、操作交互都在 Unity 中完成但光斑怎么聚焦、扫描场怎么畸变、镜头位于哪个位置必须来自光学仿真的真实数据。如果只做静态展示用截图加标注就够了但我们要的是动态演示和参数联调这就需要 Unity 的实时渲染能力。一旦参数改变比如扫描角度从 10° 调到 35°Unity 场景里的光束方向、焦点位置、扫描轨迹要同步更新这些都可以通过读取 VirtualLab 导出的一组离散数据点再插值实现也可以在满足精度的前提下直接用 y f·θ 公式实时计算。两种方式我都用过下面详细展开。2. 仿真端准备从 F-Theta 设计到能交给 Unity 的数据2.1 上手前必须确定的五个关键参数不是所有 F-Theta 物镜都适合闭着眼拿来做演示做 VirtualLab 仿真前先把这五个参数钉死后续所有导出和 Unity 场景搭建都要围绕它们展开。第一个是工作波长。F-Theta 镜头的镀膜和材料色散都跟波长强相关同一颗镜头在 1064nm 和 532nm 下的焦距、透过率和焦点位置都不一样所以你必须先定激光器波长再用对应镀膜数据建模。第二个是入瞳直径也就是光束打到镜头的有效口径。入瞳大小直接决定衍射极限光斑也决定镜片尺寸和成本。振镜扫描系统中入瞳位置通常在两个振镜之间建模时要保证镜头位置正确。第三个是有效扫描角度和像面尺寸。通常厂商会标称 ±20°、±28° 之类的扫描范围像面大小由焦距和最大扫描角共同决定。你可以用公式算一下像面半径约等于有效焦距乘以最大扫描角弧度制。第四个是工作距离和像面平坦度要求。像面越平坦扫描场中心与边缘的焦点偏移越小这对大范围打标和切割很重要。VirtualLab 里可以用不同视场角的焦点偏移量来量化平坦度。第五个是扫描介质和工作环境。空气中还是保护玻璃后面是否有水冷镜座这些会影响热稳定性和杂散光路径。仿真时哪怕不建完整热模型也要把保护窗片加进去看看透射波前畸变。把参数确认单填好后VirtualLab 模型就不会在做一半时来回返工也给 Unity 端建立了一个统一的“坐标锚点”。2.2 VirtualLab 里的建模步骤VirtualLab 建模的思路和我最早用 ZEMAX 的习惯不太一样它更强调光学系统的场追迹框架而不是简单按面顺序塞透镜。我的操作流程大致分四步。第一步搭建光源。在 VirtualLab 光源模型里选择高斯光束或平面波设置波长、束腰半径、M²因子以及光束指向方向。振镜扫描系统的光源通常直接从激光器光纤输出后准直束腰位置可以放在第一个振镜镜面附近。第二步定义扫描装置。两个振镜在系统中相当于两个反射面VirtualLab 里可以用反射平面加旋转参数来表示每个振镜绕一个正交轴旋转。先定义系统全局坐标再给两个振镜分别建立子坐标。振镜的作用是改变光束传播方向F-Theta 镜头则负责把不同方向的光束聚焦到一个平面上。第三步建立 F-Theta 物镜组。镜头数据可以来自专利文件、厂商手册也可以用自己的设计。VirtualLab 支持从文件导入光学面也可以直接输入曲率半径、厚度、玻璃类型。导入后先跑一遍几何光线追迹确认系统焦距和主面位置符合预期再做场追迹。第四步放置像面探测器并设置扫描序列。在 VirtualLab 中用一个参数扫描功能让两个振镜的旋转角按照一系列离散值变化比如 X 方向从 -25° 到 25°每 1° 采一个点Y 方向同理。每个角度组合下记录像面坐标、光斑尺寸和能量分布最后写成数据文件供 Unity 使用。2.3 导出哪些数据给 Unity 用VirtualLab 能导出的数据很多但不是每条都有用导多了反而让 Unity 工程难以维护。经过几轮筛选我最终固定导出三类数据。第一类是最小集扫描角度、像面坐标和理论光斑直径。格式用 CSV 或 JSON 均可CSV 简单通用JSON 结构更清晰。每条记录包含两个振镜的目标角度、像面上 X/Y 坐标、光斑直径、峰值辐照度。Unity 端读取后直接用这些数据驱动扫描动画。第二类是光斑能量分布用于完整还原焦点细节。把代表性的扫描角比如 0°、10°、20°下的像面辐照度分布导出为二维矩阵Unity 中可以转成纹理或者按阈值提取轮廓线再把轮廓线作为粒子系统的发射形状。第三类是光路几何路径用于三维光束渲染。VirtualLab 里可以通过场追迹得到每个关键面的光束坐标比如振镜 1 反射点、振镜 2 反射点、镜头入瞳位置、像面位置。把这些点按顺序导出Unity 端连成折线再给折线加一个外发光材质就是一条很直观的激光束。数据导出时要注意坐标参考系。VirtualLab 默认使用自身的光学系统坐标如果你是正 Z 轴入射振镜 1 偏 X 方向、振镜 2 偏 Y 方向那导出数据的坐标轴定义一定要记录清楚否则 Unity 里对接非常痛苦。我习惯在导出文件头部写一个元信息 JSON记录坐标系方向、长度单位、原始波长和振镜安装角度这样半年后再看项目也不会懵。3. Unity 端核心实现激光扫描场景的搭建3.1 模型导入与坐标统一这是最容易被忽视的一步VirtualLab 的仿真结果用的是毫米Unity 里虽然物理单位也是“米”但你完全可以把 1 米对应 1 毫米也就是缩放系数为 1:1 的时候场景会非常小。更好的做法是统一约定Unity 世界坐标中的 1 unit 代表 1mm这样所有 VirtualLab 导出的坐标就可以直接填入 Transform不用乘缩放系数。镜头模型可以从 CAD 软件导出 STEP 或 STP 格式再转换成 Unity 支持的 FBX。注意 CAD 坐标系通常 Z 轴向上而 Unity 是 Y 轴向上导入后需要先旋转 -90°。如果不做这一步后面光线方向计算会整体旋转 90°排查起来特别容易忽略。将模型导入工程后我建议把镜头组整体挂在一个空物体下振镜 1 和振镜 2 分别建立子节点且各自旋转轴对齐到 Unity 的本地坐标轴。这样后续做扫描动画时直接修改振镜子节点的 localEulerAngles让光束跟随旋转逻辑会很清爽。3.2 激光光束可视化三种方案实测对比Unity 里表现一条激光束方案直接决定视觉真实度和性能开销。我试过三种方案这里把优缺点和适用场景都列一下。第一种是 LineRenderer 加发光材质这是最轻量的方案。用五六个点描述光路激光出射点、振镜 1 反射点、振镜 2 反射点、镜头入瞳位置、焦点。给 LineRenderer 设置一个从白色到黄色再到红色渐变的材质再加上 Bloom 后处理效果视觉上比较像一条能量光束性能几乎可以忽略。第二种是粒子系统适合表现光束能量分布和空气中的散射效果。粒子系统可以发射出沿光线方向运动的光点同时带一点随机抖动模拟灰尘散射。这个方案在表现高功率激光时非常合适但粒子数量不能太多否则移动端帧率会明显下降。第三种是Mesh 管道适合高精度展示。用一段圆柱体网格沿线路径生成光束形状更真实也能接收光照和阴影。缺点是需要动态更新网格顶点代码量较大粒子光晕效果还得额外叠加。以我做激光打标机演示的经验LineRenderer 加 Bloom 已经是性价比之王粒子系统作为辅助光晕两者结合最终效果最理想。3.3 振镜与 F-Theta 扫描动画的核心脚本思路扫描动画的核心逻辑其实不复杂根据当前扫描角度算出振镜旋转量再根据 y f·θ 公式算出像面目标点最后让代表光束的 LineRenderer 实时更新。这个过程中最容易出错的是角度的弧度制和角度制混用Unity 的 Mathf.Sin 和 Mathf.Cos 默认使用弧度而 Inspector 里显示的是角度我建议所有计算统一用弧度只在 UI 显示时转成角度。下面这段是简化后的关键逻辑你可以直接作为参考起点public class FThetaScanner : MonoBehaviour { public Transform galvo1; // 绕Y轴旋转的振镜 public Transform galvo2; // 绕X轴旋转的振镜 public float focalLength 160f; // 单位: mm public float scanAngleRad 0.3f; // 当前扫描角(弧度) public Vector3 CalculateImagePoint(float angleX, float angleY) { float x focalLength * Mathf.Tan(angleX); float y focalLength * Mathf.Tan(angleY); // 如果是理想F-Theta镜则应改为: // float x focalLength * angleX; // float y focalLength * angleY; return new Vector3(x, y, 0f); } void Update() { galvo1.localRotation Quaternion.Euler(0f, -scanAngleRad * Mathf.Rad2Deg, 0f); galvo2.localRotation Quaternion.Euler(scanAngleRad * Mathf.Rad2Deg, 0f, 0f); // 更新LineRenderer端点... } }实际项目中理想 F-Theta 公式和包含畸变的真实镜头数据会有几毫米偏差如果只是做概念演示用公式就够了如果要对标真实设备必须用 VirtualLab 导出的坐标表在 Unity 里做一个查表加插值。我通常两种都保留通过开关切换。4. 交互与数据联动让场景像“活”的实验台4.1 用 UI 滑块实时调节光学参数Unity 做参数调节面板很顺手Slider 是现成的唯一要留意的是 Range 属性要和实际物理范围匹配别让用户把焦距拉成负数。我做的面板通常包含三个滑块扫描角度、焦距、光斑缩放倍率。改变扫描角时振镜组旋转、像面光点位置、光路折线全部同步更新改变焦距时像面位置沿光轴移动光斑尺寸按比例变化。为了让操作有反馈我会在光斑位置放一个小球同时用 Text 显示当前像面坐标和理论光斑直径。如果你希望按钮的点击范围更大一些Unity 里可以给 Image 组件加一个透明的扩展层或者直接用 RectTransform.sizeDelta 配合 Image.alphaHitTestMinimumThreshold 做像素级点击测试。这个技巧在触屏演示机上很实用操作区域太小容易误触。4.2 摄像机跟随与多视角切换激光扫描设备结构紧凑操作者需要同时看到光束、镜头、像面三个区域。我用两套摄像机逻辑一套作为主视角可以自由旋转和缩放适合操作者观察整体布局另一套挂在扫描头位置跟随振镜旋转模拟“从镜头里看激光打点”的第一人称视角。实现上给摄像机挂一个简单的跟随脚本目标是扫描头节点。为了运行流畅我取消了摄像机的动态阴影只在主光方向放一点平行光避免阴影计算吃掉帧率。这里特别提醒Unity 的实时阴影在复杂工业模型上很容易出现闪烁阴影斑块特别是细长的激光光束模型强烈建议对光束对象禁用 Cast Shadows 和 Receive Shadows。4.3 把 VirtualLab 的光斑能量数据还原成粒子效果只用一个发光小球当焦点太粗糙想要更像样可以把 VirtualLab 导出的辐照度分布转成 Unity 粒子系统的发射形状和颜色。具体做法是把二维辐照度矩阵读取成 Texture2D每个像素的亮度值作为粒子数密度权重。粒子系统使用该纹理作为 Color Over Lifetime 的颜色来源同时在 Collision 模式下让粒子落在像面高度上。这样显示出来的焦点不是一个死板球体而是一团有能量梯度的光斑。用这种方法模拟高斯光束最方便因为粒子亮度分布本质就是能量分布。4.4 外部扩展串口、PLC 与数字孪生如果项目要对接真实设备Unity 还可以通过串口或 Ethernet 读振镜控制器的角度反馈再用真实角度驱动虚拟扫描头形成实时数字孪生。Unity 的 System.IO.Ports 在 Windows 上可以直接用但要注意发布平台如果目标是 WebGL串口不可用只能走 WebSocket 或者后端转发。我在产线调试时遇到过类似需求虚拟场景和真实 PLC 联动PLC 发来一组坐标序列Unity 解析后逐点驱动激光轨迹运行用于验证加工路径是否存在碰撞或越界。这种模式不但节省了试验材料还能提前发现程序里隐藏的异常跳变。这里的关键是定义一个稳定的通信协议比如用 JSON 传坐标列表字段名固定、单位固定。如果涉及到西门子 PLC可以通过 S7 协议或者 OPC UA 读取变量Unity 端做轮询和插值。虽然配置繁琐但做出来的效果很好管理层演示时一目了然。5. 实战踩坑记录坐标、阴影、UI 和 WebGL5.1 毫米与米的单位陷阱第一次联调就翻车第一版联调时我把 VirtualLab 导出的像面坐标直接填入 Unity结果光斑位置偏得非常离谱单独看 X/Y 都对得上合起来却成了一团乱麻。排查了一整天才发现是单位问题VirtualLab 导出的是毫米Unity 一个 unit 我当成米来用数值差了 1000 倍。后来我在数据加载代码里增加了单位校验凡是读取仿真数据一律先检查文件头里记录的 unit 字段再换算成 Unity 世界单位。比如 VirtualLab 里焦距 160mmUnity 里就是 160 units摄像机初始位置放在大约 800 units 的位置光圈孔径等也按毫米建模。这样场景里所有物体尺度一致灯光衰减、景深效果才正常。5.2 阴影和雾效把光束“淹没”了刚把光束 LineRenderer 做出来时视觉效果很淡因为场景里开了实时阴影和雾效激光束材质没有发光明亮感被环境光和阴影拖暗了。我把光束材质改为 Unlit 类型并加上 Bloom 后处理问题立刻缓解。如果你的项目里有室内环境照明记得给激光光束单独设一个 Layer让主摄像机的 Culling Mask 包含该层但让阴影相关设置对它不生效。或者最粗暴的做法全部光束不参与阴影计算肉眼几乎看不出差异帧率却能提升不少。工业演示场景里准确的光束位置可比虚拟阴影重要得多。5.3 World Space UI 被模型遮挡怎么让信息始终可见做参数提示板时我用的是 World Space Canvas想让它在 3D 场景中浮动显示。结果机械臂一转到镜头前面面板就被模型挡住了关键数据看不到。我搜到几个方案最简单的是把 Canvas 单独放到一个 layer用第二个摄像机只渲染 UI layer再对两个摄像机做透明叠加。实际项目里我采用更省事的方法当主要模型进入面板前方一定距离时主动降低面板透明度并把文字颜色调亮形成一种“半透明悬浮屏”的效果。要做得更精细可以通过 Raycast 检测遮挡被遮挡时把面板位置沿视线移动避开。5.4 WebGL 构建后数据写入失败的坑项目做到后期想把演示包发布成 WebGL 版本给客户在线体验。结果在本地 Editor 里一切正常部署到网页后数据配置怎么改都存不下来控制台报 IndexedDB 写入失败。原因是 WebGL 的本地存储有严格限制PlayerPrefs 内部走 IndexedDB但浏览器隐私模式、存储配额不足或者不支持 IndexedDB 的环境都会导致写入失败。我的处理方案是所有可修改配置优先放内存必要时通过后端接口保存如果必须本地持久化则在写入前用 PlayerPrefs.HasKey 检测是否可用并在 UI 上给出存储失败的提示。如果涉及大数据量比如光斑能量矩阵不要存 PlayerPrefs直接把序列化数据通过 JSON 文件放远端服务器运行时下载读取。6. 常见问题速查表与参数参考6.1 项目落地中的高概率问题速查问题现象可能原因解决办法光斑位置与 VirtualLab 数据不一致坐标单位未统一、坐标系旋转差异检查文件头元信息统一毫米/米比例对齐坐标轴光束太暗不显眼材质受光和雾效影响改用 Unlit 发光材质增加 Bloom 后处理扫描动画卡顿LineRenderer 点数过多、粒子数量过大、实时阴影减少点采样密度用对象池控制粒子去掉光束阴影WebGL 配置保存失败浏览器 IndexedDB 不可用或配额不足改用内存/后端存储或降级为服务器 JSON 保存UI 面板被 3D 模型遮挡World Space Canvas 层级问题单独渲染层或动态调整面板透明度/位置振镜旋转后光束方向不对本地轴与全局坐标系不一致检查 Transform 父子层级统一旋转轴向约定光斑尺寸和理论值差很多镜头入瞳位置建模错误回到 VirtualLab 检查入瞳和主面位置重新导出坐标6.2 常用公式速查F-Theta 物镜设计里最常用的几个公式方便你在 Unity 里做实时估算公式用途备注y f·θ理想 F-Theta 物镜像高公式θ 单位为弧度y_tan f·tan(θ)普通镜头的像高公式用来对比畸变两者偏差即 f-Theta 畸变畸变率 (y_tan - y_theta) / y_theta × 100%衡量 F-Theta 镜头线性度商用镜头通常小于 0.5%光斑直径 d 4λf / (πD)衍射极限光斑直径估算λ 波长D 入射光斑直径扫描范围 2f·θ_max视场全宽配合像面尺寸选型用这些公式做 Unity 里的实时预测逻辑可以大幅减少对仿真数据的依赖演示速度更快。追求高精度时再切回查表插值模式。6.3 后续还能怎么扩展这个项目做完后我最大的感受是它的可扩展性很强。比如接入一个 Pico 4 或 Quest 的 VR 环境操作者可以在虚拟设备间走动从任意角度观察光路和扫描过程教学培训效果极佳。VR 交互要考虑手柄射线拾取和 UI 面板重置Unity XR Interaction Toolkit 可以直接使用工作量主要集中在交互逻辑上。另外如果接上真实振镜控制器的通讯协议这个虚拟场景可以直接变成离线编程验证环境操作者提前在 Unity 里模拟加工轨迹没问题后再下发到实体设备。这种模式在激光加工行业非常有价值既能降低试错成本也能提升产线换型效率。我个人在实际操作中体会最深的是这类跨域融合项目里最难的往往不是单一技术而是两个软件之间数据约定的统一。VirtualLab 导出什么、Unity 按什么单位读取、坐标轴向如何对齐这些定义清楚项目就完成了一半。如果你也在做类似的事情建议先花半天把数据格式和坐标约定文档写出来。另外再分享一个小技巧VirtualLab 导出的光斑能量图直接在 Unity 里用粒子系统还原效果比单纯放一个发光小球好太多客户看到这种细节上的还原对整个项目的专业度评价都会不一样。