1. 第三篇要讲什么:模型能显示,不等于能飞对
做自定义三维模型仿真这件事,最容易产生错觉的阶段就是"模型出来了"。屏幕上那架自己画的四旋翼转起来了,螺旋桨也转了,看着挺像那么回事,结果一给指令,飞机要么原地抽搐,要么穿地、要么往上飘得离谱、要么撞到障碍物直接穿墙而过。前两篇我们把主要精力放在资产侧:建模软件里的建模规范、导出格式选择、FBX 进引擎的导入设置、材质丢失怎么补、模型路径怎么在工具链里注册。这些做完,验收标准只是"看得见"。
这一篇要跨过的是另一道坎——从视觉资产变成仿真对象。换句话讲,自定义三维模型要在 RflySim 这套仿真链路里真正跑起来,需要三样东西同时对齐:几何(模型在哪个位置、朝哪个方向、多大)、物理(质量多少、转动惯量多大、碰撞体长什么样)、参数(电机拉力系数、气动阻尼、传感器安装位置)。任何一项错位,表现出来的都是"仿真不真实",但根因可能藏在完全不同的地方,这也是很多人调了半天越调越乱的原因。
我先把这一篇的定位说清楚:它适合已经把模型导进引擎、但还没跑通闭环的人;也适合跑通了单机、准备上多机集群的人;如果你刚开始接触这套工具链,前两节关于坐标系和原点的内容建议先看,因为这部分返工成本最高,模型进引擎之后再改原点,等于全部重来。下面所有操作都以工具链自带的示例工程为参照,不同版本的目录结构和参数名会有些出入,具体以本地安装目录里的示例文件为准,这点先说在前面,免得你对着我的字段名找不到对应的框。
2. 资产进入引擎前的最后一次体检
2.1 原点、朝向、单位:三个返工率最高的手工关
模型原点这件事,我在实际配置里见过太多翻车。建模软件里为了画着顺手,很多人会把原点放在机身某个角落,甚至放在地面投影点上。这个习惯在纯渲染项目里没问题,放到仿真里就是灾难:引擎按模型原点对齐机体质心,原点偏了,整机的旋转中心就偏了,姿态一变模型会绕着错误的位置甩,视觉上表现为"机身绕着尾巴转"。
正确做法是把模型原点卡在机体质心附近,也就是电池、机架、飞控板综合起来的重心位置,一般略低于几何中心。这个点不需要绝对精确,因为真正的动力学质心是在参数文件里单独定义的,这里的原点主要影响视觉对齐和相机挂点计算。但两者偏差太大时,相机画面会出现明显的视差漂移,做视觉算法验证的人会很痛苦。
朝向的坑更隐蔽。引擎侧通常采用 X 轴向前、Y 轴向右、Z 轴向上的左手坐标系,而飞控固件层的机体坐标系是 X 向前、Y 向右、Z 向下的右手系。这两个体系在 Z 轴方向上是相反的,导入时如果引擎的坐标转换选项没勾对,模型会呈现"倒扣"状态——螺旋桨朝下,机身朝上,第一眼看上去还挺对称,很容易被忽略。我的习惯是给模型加一个明显的非对称标记,比如在前机臂贴一块不同颜色的材质,导入后一眼就能确认机头朝向对不对。
单位是最容易算错的一环。引擎默认的尺度单位是厘米,而大多数建模软件默认按米输出。差 100 倍是什么概念:一架轴距 450 毫米的机器,导入后可能变成 45 米,直接撑满整个场景;反过来则是 4.5 毫米,小到看不见。导入对话框里的缩放系数该怎么填,其实有个简单的判断——把模型的轴距(对角电机中心距)作为参照,导入后量一下这个距离,和设计值比一下就知道对不对。我一般会记一个对照表:
| 建模软件导出单位 | 引擎导入缩放 | 说明 |
|---|---|---|
| 米 | 100 | 最常见的情况,务必确认 |
| 厘米 | 1 | 建模时就按厘米画,省事 |
| 毫米 | 0.1 | 小零件常用,容易漏改 |
| 英寸 | 2.54 | 少数老资产,建议先在建模软件里换算 |
注意:缩放不要靠引擎里的 Actor Scale 硬拉。用缩放组件去凑尺寸,碰撞体和挂点会跟着一起被拉伸变形,后面排查物理问题时会疯掉。正确做法是在导入设置里改,或者回建模软件里按正确单位重新导出。
2.2 面数预算与材质合并:给集群仿真留余量
单机演示时,模型做多精细都无所谓,十万面照样跑。但集群仿真完全是另一套逻辑。假设你规划的是 20 架同型机,单机 8 万三角面,总面数就是 160 万,再加上每架机器的材质实例、阴影投射、骨骼动画开销,帧率掉到个位数非常正常。帧率一崩,仿真步长和渲染就不同步,画面卡顿是小事,联调时的时间戳对齐会出大问题。
我的经验值是这样的:单机演示场景,控制在 3 万三角面以内;10 架以内集群,单机压到 1 万以内;20 架以上,单机最好控制在 5000 面上下。听起来很苛刻,但其实对无人机这种几何体来说完全够用——机身就是个盒子加几根圆柱,螺旋桨用一个面片加透明贴图就能糊弄过去,远看效果不比精细建模差。
材质合并的思路也一样。建模时习惯给每个零件单独一个材质,十几个材质意味着十几次绘制调用,乘上机群数量就是灾难。导入前把所有相同属性的零件合并成一个材质球,用 UV 图集区分区域,能把材质数量压到两三个。螺旋桨的动态模糊如果不需要,直接关掉;阴影如果场景里光照简单,可以只给主光源投射,其余关掉。
还有一点常被忽略:LOD。如果引擎版本支持,给模型做两级 LOD,近距离用高模,超过一定距离自动切低模。集群场景里远处那十几架其实只占几十个像素,用高模渲染纯属浪费。
2.3 碰撞体单独出模:不让物理去算几万个面
这一节是我认为整个流程里最值得单独拎出来讲的部分。很多人导入模型后直接勾上"Use Complex as Simple Collision",让引擎拿渲染网格当碰撞体。单机静态场景里这招能用,但在飞行动力学仿真里问题很大:一是碰撞检测开销随面数线性增长,二是复杂网格的碰撞法线不稳定,飞行器贴地飞行或者贴墙时会出现莫名其妙的弹飞,三是很多薄壁结构(比如机臂、桨叶)会产生穿透或者卡住。
正确做法是单独建一套低模碰撞体。通常用几个基础几何体拼出来:机身用一个略缩的立方体或者胶囊体,四根机臂用四个细长的盒子,起落架用两根圆柱。总面数控制在几十到一两百面,形状上略小于视觉模型(内缩 1 到 2 厘米),这样视觉上看起来是"贴着"的,物理上不会因为表面凹凸产生抖动。
拼碰撞体的时候有个细节值得说:碰撞体要和模型原点用同一套坐标系对齐,也就是说碰撞体也是单独一个资产,导入时保持相同的朝向和缩放,然后在引擎里挂到同一个父节点下。有些工具链允许在机型配置里分别指定视觉模型和碰撞模型,这种设计其实很友好,可以让你在不动视觉资产的前提下反复调整碰撞形状。
提示:如果你的场景里根本不需要碰撞(比如只是做编队飞行、不做避障验证),索性把碰撞完全关掉,性能能省一大截。别为了"完整性"给自己加负担。
3. 机型参数文件:把质量和惯量真正写进去
3.1 质量、转动惯量:手算一遍比瞎调快得多
模型显示正常了,接下来要让动力学"像那么回事"。这里绕不开三个参数:质量、三轴转动惯量、以及电机-螺旋桨的拉力模型。很多人的做法是拿默认机型的参数直接套,结果发现自己的机型响应快得像蚊子、或者迟钝得像卡车,然后开始漫无目的地调 PID,越调越远。根因其实在物理参数上。
质量最好办,称一下整机起飞重量就行,注意把电池算进去。转动惯量就没那么好测了,但可以估算,精度对仿真来说足够了。最常用的近似是把整机拆成几个规则体:
- 机身(含电池、飞控)近似为长方体,绕三个中心轴的惯量是 $I_x = m(b^2+c^2)/12$、$I_y = m(a^2+c^2)/12$、$I_z = m(a^2+b^2)/12$,其中 a、b、c 是三个方向的边长。
- 四个电机加桨,质量相对集中,可以当作质点处理,对 Z 轴(偏航轴)的贡献是 $I_z = \sum m_i d_i^2$,$d_i$ 是电机到质心的水平距离。
- 机臂如果比较长,也按细杆处理,绕端点的惯量是 $mL^2/3$,绕中心是 $mL^2/12$。
举个具体算例:整机起飞重量 1.2 公斤,机身箱体近似 0.20 米长、0.15 米宽、0.08 米高,取机身质量 0.9 公斤;四个电机各 0.05 公斤,安装在距质心 0.22 米的四个对角位置;机臂按 0.03 公斤每根、长度 0.25 米算。
机身部分:$I_x = 0.9 \times (0.15^2+0.08^2)/12 \approx 0.00217$,$I_y = 0.9 \times (0.20^2+0.08^2)/12 \approx 0.00348$,$I_z = 0.9 \times (0.20^2+0.15^2)/12 \approx 0.00469$,单位是 kg·m²。
电机对偏航轴的贡献:$4 \times 0.05 \times 0.22^2 \approx 0.00968$。这个数值比机身自身的还大,说明对这类小轴距机型,电机位置对偏航惯量的影响是主导性的,这也是为什么轴距改了之后偏航响应会明显变化。
算出来的量级一般在 0.003 到 0.02 kg·m² 之间,X、Y 两轴通常接近,Z 轴略大一些。如果你算出来的 Z 轴比 X 轴还小,说明哪里搞错了,回去检查。
3.2 拉力模型的拟合:别用厂家标称值直接填
电机加螺旋桨的拉力模型,常见形式是 $T = C_T \omega^2$,也有用转速一次项加二次项的。工具链的机型配置里一般会暴露几个系数,让你填"最大拉力""拉力系数"或者"转速-拉力曲线"。这里有个特别常见的错误:直接抄电机厂家给的"单轴最大拉力"。
厂家标称值通常是理想电压、满油门、静态台架条件下的峰值,跟你实际使用的电池电压、电调限流、桨叶型号都不一定对得上。更麻烦的是,很多标称值给的是"整机推力",四个电机加起来,填的时候又当成单轴填了,一下就差了四倍,飞机在仿真里垂直加速度会离谱到吓人。
我的做法是:有台架数据就用台架数据,没有的话至少做一次"两点校准"——取悬停油门和满油门两个工况,分别估算实际拉力,然后反推系数。悬停点的估算很直接:单轴拉力等于起飞重量除以四,转速通过油门-转速线性关系粗略换算。满油门点看电池电压和电机 KV 值,转速大约等于电压乘 KV 再打个八五折。两个点连起来,二次项的系数就能反推出来。
配置里还常有一个"电机响应时间常数",这个参数决定了油门指令到实际转速的延迟,一般给 30 到 80 毫秒。设得太小,飞控调出来的参数在真机上会抖;设得太大,仿真里的姿态响应会显得迟钝,调出来的参数上真机偏激进。这一项建议按实际电调的加速性能来,不确定就取中间值 50 毫秒起步。
3.3 参数写入后怎么校验:三步验证法
参数填完不能就这么信了,得验证。我一般走三步:
第一步,静态悬停验证。让飞机在仿真里悬停,看稳态油门百分比。如果算出来的悬停油门在 45% 到 60% 之间,说明拉力模型大致合理;如果悬停要 80% 以上油门,说明拉力给小了;如果 20% 就悬停住了,说明拉力给大了或者质量给小了。这一步不需要任何控制器参数调优,纯粹看物理。
第二步,阶跃响应验证。给一个固定幅度的姿态指令阶跃,看角速度响应的峰值和稳定时间。四旋翼的滚转响应时间常数通常在 0.1 到 0.3 秒量级,如果你的响应快到这个量级以下,八成是惯量给小了。
第三步,自由落体与弹跳验证。把飞机抬到两三米高,切断动力让它掉下来,看落地表现。如果穿地、弹飞、或者落下来之后疯狂抖动,回头去查碰撞体。这一步是碰撞体质量的直接检验。
4. 多实例与集群:模型与 ID 的一一映射
4.1 一个实例一个模型路径,别偷懒共用
单机跑通之后上集群,第一个遇到的问题往往是"所有飞机长得一样"。这在编队验证里其实还能忍,但做视觉相对定位、做多机协同搬运的时候,每架机器必须有自己的辨识特征,否则算法没法区分谁是谁。
工具链通常的机制是:每个仿真实例有一个唯一 ID,模型配置按 ID 索引。所以你要做的是准备 N 份模型或者 N 套材质变体,然后在配置里按 ID 一一映射。偷懒的做法是所有 ID 都指向同一个模型路径,结果是所有飞机同色同形,视觉算法直接失效。
我推荐的做法是:几何模型共用一份,只做材质变体。给每架机器的机身顶部贴一块不同颜色的标识块,颜色用高饱和度的红、绿、蓝、黄循环,这样既省资源,又能让视觉算法稳定区分。做目标检测验证的话,还可以在标识块上印不同形状,圆、三角、方块,增强区分度。
4.2 批量生成配置:写个脚本比手动点一百次强
20 架飞机就有 20 份配置,手动改路径、改 ID、改初始位置,出错概率极高。这种重复劳动一定要脚本化。思路很简单:拿一个模板配置文件当底本,用脚本读进来,循环替换其中的 ID 字段、模型路径字段、初始位置字段,然后按规则命名输出。
import json, os template = json.load(open("vehicle_template.json", encoding="utf-8")) out_dir = "./vehicle_configs" os.makedirs(out_dir, exist_ok=True) # 菱形编队的横向间距,单位与配置里保持一致 spacing = 2.0 for i in range(20): cfg = json.loads(json.dumps(template)) # 深拷贝,避免污染模板 cfg["vehicle_id"] = i cfg["model_path"] = f"/Game/Models/Drone/BP_Drone_Color{i % 4}" # 排成 4 行 5 列的方阵,坐标按索引推出来 cfg["init_position"] = [ (i % 5) * spacing, (i // 5) * spacing, 0.0 ] with open(f"{out_dir}/vehicle_{i:02d}.json", "w", encoding="utf-8") as f: json.dump(cfg, f, ensure_ascii=False, indent=2)字段名一定要对着你本地版本的示例配置来写,不同版本可能叫id、vehicle_id或者index。生成完之后,随便打开两份对比一下,确认差异只出现在预期的字段上,模板里没被意外改动,这步别省。
4.3 并发场景的性能预算:先算清楚再上规模
20 架同场景仿真,帧率往往是第一个撞墙的东西。我在动手之前会先做个粗算:单机渲染开销(面数、材质、阴影)乘架数,加上物理计算开销(碰撞检测对数量敏感),再留 30% 余量。
物理侧有个容易忽略的点:碰撞检测的碰撞对是随对象数量平方增长的。20 架飞机互相之间如果都开启碰撞,就是 190 对;如果场景里还有几十个障碍物,这个数字会很快膨胀。编队飞行其实很少有真的物理接触,我一般会把飞机之间的碰撞关掉,只保留飞机和场景障碍物之间的。真要做多机碰撞验证的,另开一个小规模的专用场景。
渲染侧最有效的三个开关:关掉动态阴影(或者只留主光)、关掉模型上的高光反射、把后处理效果降到最低。这三项做完,帧率通常能翻一倍以上。
5. 传感器挂载:视觉验证里最坑的一环
5.1 相机挂点的坐标换算与内参对齐
做视觉算法验证的人,最后都要在模型上装相机。这里的第一个坑是挂点坐标。机头下方的相机,在建模软件里的位置可能是相对模型原点的某个偏移量,而引擎里挂点用的是父节点的局部坐标,两者中间还夹着一次坐标系转换(Z 轴翻转)。
简单可靠的验证方法:先把相机对准正前方,让飞机悬停在空中,看渲染画面里的地平线是不是水平的,机头方向是不是画面正中央。如果画面是歪的或者上下颠倒,说明挂点的旋转设置错了;如果机头偏向一侧,说明偏移量的 Y 轴符号搞反了。
第二个坑是内参一致性。渲染侧有视场角,算法侧有焦距加传感器尺寸,这两组参数必须能互相推出来。视场角和焦距的关系是:水平视场角等于两倍反正切(传感器宽度除以两倍焦距)。举个例子,传感器宽度 6.4 毫米、焦距 4.0 毫米,算出来水平视场角约为 77.3 度。渲染侧如果填的是 90 度,那画面和算法模型的对应关系就全错了,位置估计的误差会随着距离线性放大。
注意:视场角填错导致的误差,在小距离、静态场景里几乎看不出来,一上动态轨迹就暴露,很容易被误判成算法问题,白白浪费几天。
5.2 深度图与点云偏移:量一下比猜十天准
深度相机和激光雷达的仿真,最常见的偏差是"看到的东西比实际近一截或者远一截"。原因通常是近裁剪面设置和数据归一化的方式不匹配。渲染管线的深度值是非线性的,靠近相机的部分精度高,远处的精度低,如果你的转换公式用了线性映射,中远距离的误差会非常夸张。
处理办法是拿一个已知距离的标定板做一次实测:把一块平面放在相机正前方 2 米处,看输出的深度值是不是 2.0 米。如果差了固定比例,说明缩放系数错了;如果误差随距离增大,说明非线性映射那一步没做对。这个校准做一次,后面所有数据都能复用。
点云那边还有一个细节是挂点高度。激光雷达挂在机身上方还是下方,扫描到的地面距离会差一个机身高度,做建图和避障的时候这个偏差会被当成障碍物高度误差,导致规划出的路径贴着地面摩擦。
6. 常见问题排查:我踩过的那些坑
6.1 模型不显示或者显示成灰色方块
排在第一位的问题永远是路径。工具链引用的模型路径写错了大小写、写错了目录层级、或者资产打包之后路径变了,表现就是场景里出现一个默认的灰色占位体。排查顺序建议这样:先在引擎自带的资产浏览器里确认这个资产能正常打开,再确认配置文件里的路径字符串逐字符匹配(大小写敏感这点很容易中招),最后看运行日志里有没有资源加载失败的报错。
材质丢失是第二类问题,表现是模型颜色全白或者全粉。原因一般是材质引用的是绝对路径,换台机器就断了,或者贴图文件没有一起打包。解决思路是在导入时就做好材质重定向,把所有外部引用收进工程目录内部。
6.2 飞起来抖动、抽搐、穿模
这一类问题的排查稍微费劲,我整理成一张表供对照:
| 现象 | 最可能的原因 | 排查动作 |
|---|---|---|
| 悬停时高频抖动 | 惯量给小了,或电机时间常数过小 | 复核惯量计算,把时间常数往 60ms 调 |
| 姿态变化时机身绕着奇怪的点转 | 模型原点偏离质心 | 回建模软件重新定原点 |
| 落地弹飞 | 碰撞体比视觉模型大,或者用了复杂碰撞 | 换低模碰撞体并内缩 1 到 2 厘米 |
| 贴地飞行穿入地面 | 碰撞检测未开启或地面无碰撞 | 检查地面碰撞与飞机的碰撞预设 |
| 编队时相邻机互相顶开 | 机间碰撞未关闭 | 关掉飞行器之间的碰撞对 |
| 高速飞行时机身拉长变形 | 用了组件缩放而非导入缩放 | 回导入设置改缩放系数 |
6.3 集群规模上去之后的各种卡
性能问题有个特点:单机时完全看不出来,一上规模就全暴露。除了前面说的关阴影、降面数、关机间碰撞,还有几个不太起眼但效果明显的动作:
一是降低渲染分辨率。多机仿真里,十几路相机同时出图是真正的性能黑洞,把每路的分辨率从 1080p 降到 640×480,出图开销能降一个数量级,对算法验证来说精度通常够用。
二是错帧出图。不需要每一帧都出图的话,让不同实例在不同帧出图,把负载摊平,能避免所有实例同一帧排队导致的卡顿尖峰。
三是把日志关掉。大规模仿真时每个实例都在写日志,磁盘 IO 会成为隐藏瓶颈,尤其是在机械硬盘上。确认参数稳定之后,把非必要的日志全关。
6.4 仿真对上了、真机对不上
这种情况出现的时候,先别急着怀疑仿真。我在实际项目里遇到的绝大多数"仿真实机不一致",根因都在三个地方:一是仿真里的质量取值是空机重量,没算电池,真机起飞重量比仿真大了两成,响应自然不一样;二是仿真里的拉力模型按满电电压算的,真机用的是半电电池;三是仿真里的传感器没有噪声,真机有,算法在仿真里调得过于激进。
对应的处理办法是把这三个差异补进仿真:质量按实际起飞重量填,模型里加上电池电压随放电下降的建模,传感器加上和实际量级接近的噪声。补完之后再对比,一致性会好很多。
提示:别指望仿真和真机完全一致。仿真的价值在于快速验证逻辑和参数的大致范围,最后一定量的实机微调是绕不开的。把仿真当成"粗调工具",心态会好很多。
7. 我在这一块的一些实际体会
从"模型能看见"到"模型能飞对",中间这段路比第一次做的时候想的要长。我现在做新机型的流程基本固定下来了:先在建模软件里把原点、朝向、单位这三件事做对,导出两个资产(视觉模型和低模碰撞体);进引擎之后先只验证视觉,把模型摆到场景里看看位置朝向对不对;然后填物理参数,只做悬停验证,看稳态油门;再做阶跃,看响应时间尺度;最后才上控制器调参数。这个顺序看起来慢,但实际比"跑起来再说、哪不对调哪"快得多,因为每一步都能独立定位问题,不会把参数问题和物理问题搅在一起。
还有一个经验是给自己留一份参数记录表。每次改了什么、为什么改、改完之后哪个指标变了,简单记一行。集群调参的时候改动会非常频繁,靠脑子记一定会乱,最后连"这版和上版差在哪"都说不清,白浪费一整天。
最后分享一个我用来快速判断"是不是物理参数问题"的小技巧:把控制器参数换成默认值,只给一个最简单的姿态阶跃指令,看响应曲线。如果曲线形状本身就不合理——比如超调大得离谱、或者稳态压根不收敛——那问题多半在物理侧,回去查质量和惯量;如果曲线形状合理只是慢或者快,那才是控制器参数该调的地方。这个判断顺序能帮你少走很多弯路。