1. 这一篇到底要解决什么:从"看见模型"到"飞得像那么回事"
系列写到第三篇,前面两篇把自定义三维模型从建模软件里"搬"进了 RflySim 的三维显示环境,能加载、能旋转、能在场景里摆着,很多人到这一步就觉得成了——模型显示出来了嘛。但真正跑起来才知道,能看见和能用于仿真,中间隔着一条不浅的沟。模型尺寸大了一圈、贴图一片惨白、机头指错方向、旋翼不转、撞上去直接穿模、多机集群一开帧率掉到个位数,这些问题全都是"模型进来了"之后才会暴露出来的。这一篇就专门啃这块硬骨头,把三维模型在 RflySim 中实现可信仿真的完整链路讲透:材质与单位的对齐、碰撞体与物理属性的设置、动力学参数与飞行数据的一致性、以及多机场景下的性能与排查。
RflySim这套环境本质上是把PX4飞控固件、Simulink控制算法和基于Unreal Engine的三维视景揉在一起,三维模型在这里承担的角色不只是"好看",它还参与位置姿态数据可视化、碰撞检测、载荷与传感器挂载点的定义。所以自定义三维模型能不能实现仿真,判断标准不是"屏幕上有没有东西",而是:姿态对不对、尺寸准不准、旋翼转不转、碰撞有没有、多机卡不卡。这五条任何一条不过关,仿真数据就没法当参考,飞出来的曲线只能当动画看。
适合读这篇的人,我大致分两类。一类是已经跟着前两篇把模型导进去了、但卡在细节上出不来的;另一类是想跳过前面直接拿现成模型改的,那我建议至少回头把坐标系和网格原点那节补一下,否则后面每走一步都要返工。下面的内容尽量按"动手顺序"排,前面的没做完别急着往后翻,尤其是单位对齐和原点设置这两步,做错了后面全部白搭。
说明:文中涉及的具体配置文件名称、蓝图节点名称,以你手上平台版本的官方说明为准,不同版本会有出入。我给的是通用做法和判断逻辑,不是死参数。
2. 模型落地的第一道坎:单位、材质与朝向的三种对齐
2.1 厘米和米的战争:导入缩放到底该填多少
几乎所有人第一次导入自定义模型都会遇到同一个场景:模型进场景后不是像芝麻就是像怪兽,缩放到合适大小一看,位置又飞了。根因在于两套单位体系打架。绝大多数建模软件(如常见的三维建模工具)默认按"1 单位 = 1 米"组织数据,而 Unreal Engine 内部默认1 单位 = 1 厘米,也就是 1 UU = 1 cm。你在建模软件里画了一个翼展 0.5 米的四旋翼,导出 FBX 时如果不做任何处理,UE 会把它理解成 0.5 厘米——小了整整一百倍。
正确的处理方式是三条路选一条,不要混用:
| 方案 | 操作位置 | 适用场景 | 风险 |
|---|---|---|---|
| 导出时乘 100 | 建模软件 FBX 导出面板的单位缩放 | 单模型、一次性导入 | 容易忘,批量时易错 |
| 导入时设 Scale | UE 导入对话框的 Transform 缩放 | 批量导入、需要统一 | 单位设置被继承,改一次影响全部 |
| 建模时就按厘米 | 建模软件里直接把场景单位改成厘米 | 长期维护的项目 | 老的资产库全要重做 |
我个人的习惯是走第二条:模型文件保持"1 单位 = 1 米"的干净状态,导入时在 Import 对话框里把缩放统一设成 100,并且勾选"Apply Transform"之类把变换烘焙进网格的选项。这么做的好处是源文件永远是标准尺寸,换平台、换引擎都不会乱,坏处是每次导入都要盯一眼那个输入框。踩过一次坑之后我给自己定了个死规矩:导入完成后的第一件事,拿场景里自带的参照物量一遍尺寸,比如标准跑道宽度、默认多旋翼模型的轴距,量出来对不上就说明缩放错了,别急着往下做。
还有一层更隐蔽的问题:缩放错了但你没发现,因为模型看起来"比例是对的"。曾经有个模型整体放大了一百倍,但它是个球形飞行器,没有明显特征尺寸,视觉上完全看不出来,一直到做避障仿真的时候才发现碰撞范围大得离谱——碰撞体是按网格生成的,网格大了一百倍,碰撞体自然也大了一百倍,两架飞机隔着一米就"撞"上了。所以别靠眼睛,靠数值。
实操心得:先把默认的多旋翼模型加载出来作为"尺子",把自定义模型并排放在旁边,轴距、桨盘直径、机体高度三项对得上,基本就能确认缩放正确。三项里任何一项偏差接近 100 倍或 0.01 倍,回去查单位。
2.2 材质丢失与贴图路径:为什么你的模型是一身灰
模型导进来了,尺寸也对,但整个机体是灰白色的,界面上看不出任何涂装和标识。这不是渲染坏了,是材质没有正确关联。FBX 这类格式本身是可以携带材质信息的,但携带的只是"引用",它会告诉引擎"这个面的材质叫某某名字、贴图在同目录下的某个文件名",引擎导入器会尝试用这个引用去重建材质实例。一旦路径变了、贴图文件名改了、或者贴图格式引擎不认,重建就会失败,引擎退回到默认材质,看起来就是一身灰。
排查顺序我一般这么走:先看贴图文件有没有跟着模型一起走,很多人导出时只导了模型本体,贴图留在原地没带过来;再看贴图格式,常见图片格式一般都没问题,但某些专业格式和特殊位深的图会读不出来;最后看材质命名,如果模型里用了大量中文名或者带特殊符号的材质名,重建过程容易出岔子。这三个排查完,八成问题能定位。
如果重建一直失败,别死磕自动关联,手工建材质更稳。做法是:在引擎的材质目录里新建一个材质,把贴图挂到基础颜色的输入上,然后把这个材质赋给网格体的对应插槽。听着麻烦,但好处是完全可控,后续想加自发光、想加透明、想换涂装都不用回建模软件重导。尤其是做集群仿真,几十架飞机需要不同颜色区分编队,用材质实例动态改颜色比准备几十套模型高效得多。
关于材质对帧率的影响,这里给个提醒。很多人喜欢给模型加一大堆高分辨率贴图、法线贴图、自发光,单机看着漂亮,一上集群就崩。我的经验是:机体主材质控制在一张 1K 或 2K 的基础色贴图,法线贴图有就用、没有不强求,自发光尽量砍掉,透明材质能不用就不用——透明材质在渲染管线里要单独排序,几十个透明物体叠在一起,帧率掉得肉眼可见。
2.3 机头朝向与网格原点:姿态数据对不上的元凶
这是最容易被忽略、也是最致命的一步。仿真里飞行器位置姿态是靠数据包实时驱动的,数据包里的姿态是相对某个坐标系定义的,如果模型的机头朝向和这个坐标系定义不一致,你会看到飞机在侧着飞、倒着飞、横着飞,甚至原地打转。这类现象非常迷惑人,因为模型本身很漂亮,视角一转就以为是自己看错了。
要先搞清楚两件事:数据侧用的是什么坐标系,模型侧的原点在哪、朝向哪。
数据侧常见的两种约定:一种是航空常用的机体坐标系,X 轴指向机头、Y 轴指向右侧、Z 轴指向下方;另一种是机器人领域更常见的,X 轴向前、Y 轴向左、Z 轴向上。而三维引擎大多是左手坐标系,X 轴向前、Y 轴向右、Z 轴向上。你看,除了 Z 轴方向可能一致,Y 轴的正负往往是反的。这意味着如果你的模型是按"Y 轴向左"建的,直接导入引擎而不做处理,整机会左右镜像,外观上不那么容易发现,但机臂、桨的旋向、传感器位置全反了。
所以我的做法是固定一套流程:
- 建模阶段就把模型摆成机头朝世界坐标的正前方,垂直方向朝上,机体的几何中心尽量对齐世界坐标原点;
- 如果建模软件的坐标系和引擎不一致,在导出前用一个空物体做旋转校正,把旋转烘焙进去,不要留到引擎里再转;
- 导入引擎后,用引擎的坐标轴显示功能核对一遍,确认模型本地坐标的正前方就是视觉上的机头方向;
- 最后把模型拖到场景原点,检查它是不是"悬"在半空或者"陷"在地里。
这里还有一个细节:网格原点不一定等于几何中心。有些模型设计师为了好看,把原点放在了机体底部或者某个角上,这种模型一旦被位置数据驱动,就会出现"绕着一个奇怪的支点旋转"的现象,姿态一变化整机就大幅晃动。解决办法是在建模软件里把原点重设到质心附近,重新导出。如果你没法改源文件,那就在引擎里给模型套一个父级组件,用父级组件承担位置姿态驱动,模型作为子级做固定的偏移补偿,这样也能绕开原点不对的问题。
3. 让模型不只是个"壳":碰撞体与物理属性的设置逻辑
3.1 简单碰撞与复杂碰撞:什么时候该省,什么时候该花
三维模型在引擎里默认只有视觉网格,没有碰撞几何。也就是说,两架飞机互相穿过去,什么都不会发生。碰撞检测在仿真里有两个用途:一是视觉层面的干涉判断,比如编队飞行时判断会不会撞上;二是物理层面的响应,比如撞地、撞墙之后的表现。要不要做碰撞,取决于你的仿真目的,不是所有场景都需要。
引擎里的碰撞体分两类。简单碰撞是用盒子、球、胶囊、凸包这类基本形体近似出来的,计算开销极小,一个物体几十个三角面都用不满;复杂碰撞直接用视觉网格的三角面做精确碰撞,精度高但开销巨大,一个精细的多旋翼模型动辄几万个面,几十架一起跑,帧率立刻崩。所以在集群仿真场景里,简单碰撞几乎是唯一选择,复杂碰撞只在极少数单机精细分析时才考虑。
简单碰撞的生成方式有两种。一种是自动生成,导入网格体时在碰撞设置里选择自动生成凸包,引擎会根据网格形状自动拼出若干凸包来近似;另一种是手工指定,在建模软件里单独做一套极简的碰撞网格,命名时加特定前缀,导入时引擎会自动识别为碰撞体。手工指定的方式可控性高得多,我一般推荐这种做法:机臂用细长盒子,机身用一到两个凸包,桨盘用一个扁圆柱或者干脆不要碰撞。四个机臂加一个机身,总共五六个碰撞体,开销可以忽略,形状也够用。
注意:碰撞体不是越精确越好,是越"够用"越好。做编队避障,你需要的是"两机能感知到彼此靠近",未必需要精确到毫米的接触判定。碰撞体做太细,除了吃性能,还会带来另一个麻烦——数值抖动,两架飞机轻微接触时由于浮点误差反复触发和解除碰撞,表现为模型高频抖动。
3.2 质量、惯量与质心:三个必须和参数文件对齐的数字
很多人的自定义模型只关注了外观,参数文件里还是默认值,结果仿真出来的响应和真实飞行器差得远。质量、转动惯量、质心位置这三个物理量,直接决定动力学响应的快慢和耦合程度,必须和你的真实平台(或者你想仿真的目标平台)对齐。
质量好办,称一下就知道。转动惯量麻烦一点,理想情况是从 CAD 软件里直接导出,大多数三维建模软件都能根据材质密度自动算出惯量张量。如果没有这个条件,可以用近似公式估算。把机体近似成一个长方体,长宽高分别为 a、b、c,质量 m,那么绕三个主轴的转动惯量大致是:
- 绕 X 轴(滚转):Ixx = m(b² + c²) / 12
- 绕 Y 轴(俯仰):Iyy = m(a² + c²) / 12
- 绕 Z 轴(偏航):Izz = m(a² + b²) / 12
注意这是均匀长方体近似,实际机器人的电池、电机、机载设备分布不均,会有偏差。不过对于多数控制算法的验证来说,这个量级的估算已经够用。如果你手上有真实的辨识数据,当然优先用辨识出来的值,公式估算只作为兜底。
质心位置更容易被忽略。质心偏离几何中心,会引入额外的力矩耦合,表现出来就是悬停时总有一点残余的俯仰或滚转趋势,控制器得靠积分项慢慢压下去。所以模型建模时尽量把质心画在几何中心附近,电池、载荷这些重的部件在建模阶段就摆到实际位置。如果仿真里需要模拟挂载不同载荷的影响,那就得准备几套参数,或者用可变参数的方式动态调整质心偏移。
这里有个实用的自检方法:把仿真跑起来,给一个小的姿态阶跃指令,看响应的超调量和振荡周期是不是和你预期的一致。如果响应明显比预期"迟钝"或者"暴躁",先怀疑惯量填错了量级,比如把 0.01 填成了 0.1,或者把 g·cm² 当成 kg·m² 填了。
3.3 旋翼绑定:让桨叶跟着转速数据转起来
一个多旋翼模型如果桨不转,视觉上就永远是"悬停"状态,看不出它在加速还是在爬升,这对调试来说损失很大信息。所以自定义模型导入后,一般还要做旋翼的绑定:把每个桨叶拆成独立的网格体组件,挂到机体的相应位置上,然后把这些组件和仿真数据里的电机转速信号关联起来,让它们的旋转速度随数据变化。
具体做法:在引擎的蓝图编辑环境里,把机体网格作为根组件,四个(或更多)桨叶各做一个子组件,位置按实际的电机安装位置填。电机位置要填准,因为有些平台的旋转动画是依据位置来匹配信号的,位置填错会出现"信号给了一号电机,转的却是三号桨"的情况。桨叶的形状最好做成扁平的一片,绕自身轴旋转时视觉上更接近真实的桨盘。
旋转轴的方向也要注意。不同的旋转方向,从上方看是顺时针还是逆时针,直接对应反扭矩的方向。仿真里如果桨的旋向显示反了,虽然不影响数据,但会让人产生误判,尤其在做故障注入演示的时候,看到反桨还在正常转,会怀疑信号是不是没发出去。
实操心得:绑定完旋翼,先别急着上控制算法,用最简单的方式验证一遍——手动发一个固定转速,四个桨应该同时转起来;单独给某一个通道加速度,看是不是对应位置的桨变快。这一步验证通过,后面再上闭环才不会被"到底是模型问题还是算法问题"绕晕。
4. 参数一致性:从模型文件到控制回路的完整链路核对
4.1 模型参数与控制参数的对应关系
三维模型、参数文件、控制算法,这三者是三份独立的数据,但它们描述的必须是同一架飞机。我见过太多案例,模型是按 450 轴距做的,参数文件里还是默认的小轴距参数,控制增益也是默认的,跑出来的曲线看着挺像回事,实际这个仿真结论一点参考价值都没有。
核对清单我自己是这么定的:
| 参数项 | 模型侧的来源 | 参数文件中的字段 | 一致性检查方式 |
|---|---|---|---|
| 轴距 | 建模软件量取 | 机臂长度 / 电机位置 | 数值对比,误差控制在几个百分点内 |
| 质量 | 称重或者CAD统计 | 质量参数 | 直接比对 |
| 转动惯量 | CAD导出或公式估算 | 三个轴向惯量 | 检查数量级是否合理 |
| 电机安装位置 | 模型中的桨叶组件位置 | 电机位置向量 | 坐标数值逐一核对 |
| 旋向 | 模型桨叶旋转方向 | 电机转向配置 | 视觉验证 |
| 最大拉力 | 实测或电机手册 | 拉力系数 | 悬停油门验证 |
最后一条"悬停油门验证"是我觉得最有效的整体验证方式。参数对齐之后,让飞机在仿真里悬停,看稳态油门大概在什么位置。真实的四旋翼悬停油门一般在百分之五十上下浮动,如果你的仿真悬停油门到了百分之八十或者百分之二十,说明拉力系数或者质量其中至少一个填错了。这是一个非常灵敏的整体校验,比逐项核对效率高得多。
4.2 位置姿态数据的驱动链路怎么走通
数据从控制算法到三维显示,中间是有好几跳的:控制算法算出期望姿态,飞控(或者仿真里的动力学模型)积分出实际位姿,位姿被打包通过通信链路发出去,三维显示端接收解析,再把数值映射到场景里对应模型的位置和朝向上。这条链上任何一环出问题,现象都是"模型不动"或者"模型乱动"。
排查的时候从后往前推更高效。先确认三维显示端有没有在收数据——多数平台会有数据监视窗口或者日志,能看到收包计数在涨。收包正常但模型不动,问题多半出在模型与数据通道的绑定关系上:多机仿真时每架飞机有独立的编号,模型在场景里也要有对应的编号或者标签,编号对不上,数据就发给了空气。这个坑非常常见,尤其是复制粘贴配置文件的时候忘了改编号,十架飞机全绑到同一个通道上,画面上只有一架在动。
收包正常、绑定也对,模型还是不动,那就要看数据包里的位姿是不是恒为零。这时候把数据打印出来看,如果姿态四元数一直是单位四元数、位置一直不动,那问题回到控制侧,可能是算法没输出、可能是模型没被激活。反过来如果数值在剧烈跳变,模型在场景里疯狂抽搐,那就是数据频率或者数值范围有问题,比如单位搞错了(弧度当成了度,米当成了厘米)。
提示:链路排查别靠猜,靠日志。养成在关键节点打印一行的习惯——算法输出打一行、发出去打一行、收到打一行、映射后打一行。四行日志一摆,问题卡在哪一段一目了然,比盯着屏幕看模型省太多时间。
4.3 多机场景下的命名与编号规范
单机跑通了,下一步基本都是集群。集群仿真最容易翻车的地方不是算法,是命名混乱。每架飞机至少涉及这些东西:三维模型实例、数据通道编号、参数文件、控制器实例。这四个东西如果命名不统一,改一处忘一处,调起来就是灾难。
我的做法是建立一套固定的编号规则,比如统一用两位数编号,从 01 到 N。模型在场景里的标签、配置文件里的通道号、参数文件名后缀,全部用同一套编号。批量生成配置文件的时候用脚本生成,不要手工复制粘贴,手工复制是编号错乱的最大来源。生成完做一次自检:把所有编号列出来排序,看有没有重复、有没有断号。
场景摆放也有讲究。多机初始位置如果摆得太近,起飞瞬间就会互相碰撞,仿真直接崩。如果摆得太整齐,又容易掩盖掉一些由初始条件引起的耦合问题。我的经验是:初始位置按编队队形摆,但每架飞机的高度稍微错开一点,避免完全共面;间距至少留出两个机身长度,给控制器的初始收敛留余量。
还有一点,模型实例化。集群场景里如果每个模型都是独立的完整网格体,显存和绘制调用会成倍增长。引擎提供了实例化渲染的机制,同一套网格体可以在场景里出现多次而只占一份资源。如果你的平台支持,尽量用实例化的方式部署多机,这个优化对帧率的影响是数量级的。
5. 性能优化与常见问题实录:那些文档里不会写的坑
5.1 帧率暴跌的五个典型原因
三维显示跑得流畅不流畅,直接决定了你能不能一边仿真一边观察。帧率掉下来的时候,人的第一反应是"电脑不行了",其实多数情况是场景配置的问题。按我遇到过的情况排个序:
- 网格面数过高。一个精细模型几十万面,单机看着没事,十架一起就是几百万面。解决办法是做减面,仿真用的模型不需要和展示用的模型一样精细,能看清形状和姿态就够了,面数砍到原来的十分之一通常没什么视觉损失。
- 材质过于复杂。前面说过,透明材质、大量高分辨率贴图、复杂着色器都是性能杀手。仿真场景优先用最简单的材质。
- 阴影与后处理。动态阴影、环境光遮蔽、抗锯齿这些在展示场景里是加分项,在仿真里是纯负担。仿真跑起来第一件事就是把阴影关掉、后处理调到最低。
- 碰撞体过多。每一帧都要做碰撞检测,碰撞体数量直接线性影响开销。前面强调过,碰撞体越少越好。
- 数据频率不匹配。如果三维显示端的更新频率远高于数据发送频率,大部分帧都在做无意义的重复渲染;反过来如果显示频率低于数据频率,又会丢帧。两边频率尽量匹配。
我用过的判断方法是二分法定位:先把场景里的模型删到只剩一架,看帧率恢复没有,恢复了就是模型数量问题;删到一架还是卡,那就是单模型本身太重。这样两三轮就能锁定方向,比一项项改配置快得多。
5.2 常见问题速查表
下面这张表是我这几年攒下来的,基本覆盖了自定义模型仿真时八成以上的异常现象。遇到问题先对照查一遍,能省不少时间。
| 现象 | 最可能的原因 | 排查与处理 |
|---|---|---|
| 模型整体巨大或极小 | 导入缩放与单位不一致 | 与默认模型并排比对尺寸,检查缩放是否差 100 倍 |
| 模型一身灰白无贴图 | 材质重建失败 | 检查贴图是否随模型一起导入,手工重建材质 |
| 模型侧着飞或倒着飞 | 机头朝向与数据坐标系不一致 | 核对坐标系约定,导入前做旋转烘焙 |
| 模型绕奇怪支点旋转 | 网格原点不在质心附近 | 重设原点或加父级组件做偏移补偿 |
| 模型在场景里不动 | 绑定编号错误或数据未发出 | 检查通道编号,打印收发日志 |
| 模型高频抖动 | 碰撞频繁触发解除或数据频率不匹配 | 简化碰撞体,匹配数据与渲染频率 |
| 两机隔着距离就"撞" | 碰撞体按放大后的网格生成 | 重做碰撞体,或重新校准缩放 |
| 桨叶不转或转错位 | 旋翼组件未绑定或位置填错 | 逐一核对电机位置与信号通道 |
| 悬停油门明显偏离常规值 | 拉力系数或质量参数填错 | 用悬停油门作为整体校验点反查 |
| 集群一开就卡 | 模型面数、材质、碰撞体未优化 | 减面、简化材质、关闭阴影、实例化渲染 |
5.3 几条我觉得最值钱的经验
第一条,建模阶段就把仿真需求考虑进去,别等导入了再改。具体说,模型分开建:视觉网格一套,碰撞网格一套,桨叶独立成组件,原点放在质心,机头朝正前方,尺寸以米为单位。这套规范一旦定下来,后面每一个模型都这么做,导入环节就几乎不需要返工。我早期没有这套规范,一个模型导入折腾两三天是常事,后来规范固定了,半小时搞定。
第二条,保留一个可用的参照模型。手里始终留一个经过验证的、各项参数都对的默认模型,遇到任何异常先把它加载出来对比。是只有自定义模型出问题,还是所有模型都出问题?这个对比一步就能把问题范围缩小一半。我见过太多人拿着自己的模型死磕,磕了两天发现是平台配置被人改过,换个模型也一样。
第三条,参数改动留记录。模型、参数文件、控制增益,这三样东西任何一样改了都要记一笔,改了什么、为什么改、改完效果如何。仿真最怕的就是"改花了"——一圈优化下来参数面目全非,回头想复现某个好结果都复现不出来。我的习惯是每个版本打个标签,配上当时的参数文件和截图,出问题可以随时回退。
第四条,也是我最近才真正重视起来的:不要追求一次到位。很多人想让自定义模型一上来就承担高保真的仿真任务,结果每个环节都要抠到极致,进度拖得极慢。更实际的做法是分级推进——第一级只要求模型显示正确、姿态正确,用它跑通闭环;第二级把质量和惯量对上,让动力学响应可信;第三级再补碰撞、补旋翼动画、补视觉细节。每一级都能出可用的中间结果,哪一级出问题就在哪一级停下解决,不会因为一个细节卡死整个项目。
第五条关于时间成本。自定义模型的完整落地,从建模到能在集群里稳定跑,我这边一个熟练的人大概是两到三个工作日。如果超过五天还在打转,基本可以判断是方向错了,要么是单位和坐标系这类基础问题没排干净,要么是平台版本和文档对不上。这时候最好的选择是退回去,用一个已知可用的最小场景重新走一遍,而不是在复杂场景里继续挣扎。
最后说个我觉得挺有意思的延伸:模型做扎实之后,很多原本麻烦的事情会变简单。比如想验证不同机型的控制效果,只要换模型换参数就行,算法一行不用改;想做故障注入演示,把某个桨叶的转速信号人为置零,视觉上立刻能看到不对称,比看曲线直观得多;想做载荷影响分析,把载荷模型挂上去、更新质心和惯量,剩下的交给仿真。这些用法能不能顺滑地跑起来,取决于前面那些看起来枯燥的细节有没有做到位——单位、原点、朝向、惯量、碰撞体,这五样东西扎实了,模型就不再是个摆设,而是真正能参与仿真的一个部件。