
把 3DGS 带进 NVIDIA Isaac Sim:实写品质导入流程与工业仿真四大弱点
把3DGS扫描导入Isaac Sim 6.0的通用流程:点数压到2^24以下、转.usdc、矫正朝向、沿轨迹放相机;并剖析单体块、单标签、无碰撞、固定光照四大弱点与网格分层对策。
一句话流程:3DGS 扫描 → 点数削减到 224 以下 → 转换为 USD → 放上 stage 并矫正朝向 → 把相机放到扫描轨迹上。
如果要把 3DGS 扫描用于机器人或自主移动的仿真,目的地不是游戏引擎,而是 NVIDIA Isaac Sim。本文把「从读入到以实写品质渲染」的完整流程整理成一套适用于任何数据的做法;后半部分则直面 3DGS 用于工业仿真时的本质弱点以及规避方法。
01 为什么是 Isaac Sim
3DGS 扫描要送进哪里,取决于用途。作为影像素材使用,UE5 的自由度最高;但如果目标是机器人、自主移动、传感器仿真,本命就是 NVIDIA Isaac Sim——因为你可以把真实存在的场所直接变成验证环境。
- 能在真实场所里运行机器人——仿真环境不再是虚构空间,而是实际扫描过的现场。
- 能与物理仿真共存——PhysX 的刚体、关节、传感器,可以在实写背景中运转。
- 能批量生产训练图像——以真实风景为背景,从任意角度生成相机图像。
Isaac Sim 6.0 原生渲染高斯泼溅,并参与 RTX 路径追踪,景深与运动模糊直接生效。但真正动手导入时,你会撞上几堵文档里没有写的墙。
02 需要准备什么
- Isaac Sim 6.0 或更高版本——解压后约 40 GB,请预留 50 GB 空闲空间。
- 扫描数据——3DGS 的
.ply文件。 - 转换工具——PlayCanvas 的 splat-transform(npm 安装,免费)。
- USD 转换——Isaac Sim 标准自带,无需额外安装。
不要把 Isaac Sim 装进 C:\Program Files。它运行时会写入数据,放在需要管理员权限的位置只会招来多余的麻烦。
扫描仪私有格式可以不转换直接用:splat-transform 除了 .ply,还能直接读取主流扫描仪和查看器的原生格式,省去转中间格式的工序。而且这些包里常常附带碰撞网格和扫描时的相机轨迹,会在后面的工序里派上用场。手上有原始格式的话,从它开始最划算。
03 第一道坎:超过绘制上限就什么都不显示
要处理大扫描,请先记住这一点:Isaac Sim 的 RTX 渲染器对每个 prim 最多只能绘制 224(16,777,216)个点。超过这个数,光线追踪结构的构建就会失败,该 prim 一个点都不会显示。转换工具不报错,Python API 也不报错——你会得到「转换成功、点数正确、但画面一片漆黑」的状态。
棘手的是:即使失败,每一道工序也都正常结束。USD 文件里正确写入了全部点的坐标、缩放、不透明度、颜色;放上 stage,prim 类型也能被正确识别。唯独不显示。
这个上限在官方文档里没有记载,唯一的线索是日志里的一行 Failed to create TLAS。规格上的正确解法是「减少点数,或拆成多个 prim」,本文采用前者。
看视口之前先看日志。放上 stage 后,在 Isaac Sim 日志里搜索
Gaussian prim loaded:带着点数出现,说明已注册为绘制对象;出现的是Failed to create TLAS,则不会显示。别把黑屏误诊为相机问题,先查这里最保险。
04 导入流程
把点数压到上限以下
用 splat-transform 的 --decimate 传入目标点数:
# 按绝对数量指定(压到 2^24 = 16,777,216 以下)
splat-transform input.ply -d 16500000 output.ply
# 也可以按比例指定
splat-transform input.ply -d 95% output.ply
这不是简单的随机抽稀,而是把相邻高斯逐级配对合并。相比随机删除,信息损失更少;削减率只有几个百分点的话,外观劣化几乎看不出来。
转换为 USD 并放上 stage
在 Isaac Sim 内的 Python 中调用自带的转换器,把 PLY 转成 USD。输出扩展名请用 .usdc——用 .usdz 会让内部变成文本格式,文件体积膨胀两倍以上。
放置之前,先清理已有的高斯泼溅 prim。残留旧数据会造成「只显示新场景的一部分」的假象,让问题定位变得极其困难。
from omni.kit.converter.gsplat import convertPlyUSD
convertPlyUSD(r"output.ply", r"scene.usdc")
import omni.usd
stage = omni.usd.get_context().get_stage()
# 删除已有的高斯泼溅 prim
for p in list(stage.Traverse()):
if p.GetTypeName() == "ParticleField3DGaussianSplat":
stage.RemovePrim(p.GetPath())
# 不指定类型,用 OverridePrim 引用
prim = stage.OverridePrim("/World/Scene")
prim.GetReferences().AddReference(r"scene.usdc")
用 OverridePrim,不要用 DefinePrim。先显式指定类型再添加引用,本地类型会压过引用目标的类型,高斯泼溅类型就此丢失:属性都进来了,prim 类型却变了,渲染不出来。请用不指定类型的
OverridePrim创建。
矫正上下翻转
转换器允许指定上方轴,但把 Z-up 的扫描数据按 Y-up 转换,会得到上下颠倒的场景——建筑物从地面向下生长。
如果没发现这一点,抱着「从上空俯瞰」的心态放置相机,看到的将是地面的背面,画面一片漆黑。对 prim 施加绕 X 轴的 −90° 旋转即可恢复原朝向:
from pxr import UsdGeom
x = UsdGeom.Xformable(prim)
x.ClearXformOpOrder()
x.AddRotateXOp().Set(-90.0)
05 放置相机
实用上的诀窍是:不要自己计算相机位置。从包围盒自动算距离的做法,会被稀疏离群点拖垮——扫描数据在拍摄范围之外也散布着稀疏的点,算出来的整体范围远比你想看的区域大。
改用扫描时实际走过的轨迹坐标。多数扫描仪会把移动轨迹作为位姿数据输出,从中取一点,把相机放在视线高度(约 1.7 m 前后),朝水平方向看即可。
朝向计算用 SetLookAt().GetInverse(),写进 AddTransformOp()——这种写法不会出现矩阵转置错误:
from pxr import Gf, UsdGeom
eye = Gf.Vec3d(...) # 轨迹上一点 + 视线高度
target = Gf.Vec3d(...) # 想看的朝向
xf = Gf.Matrix4d().SetLookAt(eye, target, Gf.Vec3d(0,0,1)).GetInverse()
cx = UsdGeom.Xformable(cam_prim)
cx.ClearXformOpOrder()
cx.AddTransformOp().Set(xf)
用轨迹的理由很简单:扫描人员走过的地方,正是该数据被记录得最稠密的地方。把相机放到拍摄范围之外,看到的只能是根本没被拍摄过的面。即使拿不到位姿数据,也应该先从近距离试拍,看着拍到的范围逐步后撤,这样更稳妥。
渲染等待时间。超过 1,000 万个点后,RTX 收敛需要时间。截图之前,请把
await app.next_update_async()跑满 150 帧以上;等待不足,保存下来的就是一张纯黑的图。
06 用于工业仿真的四个弱点
接下来才是正题。3DGS 在「外观」上具有压倒性优势,但工业仿真所需要的信息它几乎一概没有。以下是在 Isaac Sim 上实际取输出、逐项验证后的结论。
弱点 1:整个场景是「一整块」,无法按物体分离
这是最大的制约。3DGS 是几千万个高斯排成的一个 prim,不存在「这栋楼」「这个信号灯」「这辆车」的单位。它无法像 3D 模型那样选中物体 A、B,也无法只对某个物体做移动、隐藏、替换。
仿真默认你能「只移动目标物」「替换障碍物」——未经处理的 3DGS 做不到。
弱点 2:语义标签整个场景只能有一个
Isaac Sim 支持输出语义分割(逐像素分类「这是道路」「这是建筑」的图像),这是感知 AI 训练数据生成的核心功能。高斯泼溅也可以打标签,并反映到分割输出里。
但标签的单位是 prim。如弱点 1 所述,整个场景是一个 prim,所以结果就是整个场景只有一个标签。实际取输出:建筑、道路、人行道、信号灯,全被涂成同一个 ID。「只想学习道路区域」「只想提取标志」这类用途,现状直接不可用。
弱点 3:没有物理碰撞
高斯泼溅是外观数据,不携带面信息,因此无法参与物理引擎的碰撞判定。机器人会穿墙而过、穿透地板下坠——视觉上明明有地板,物理上却是一片虚空。
弱点 4:光照无法事后更改
3DGS 把拍摄时的光照作为颜色信息烘焙了进去。阴天扫出来的数据,即使往场景里加太阳光,也依然是阴天。想制作不同时间段、不同天气的训练数据时,这一性质就成了制约。
07 弱点的应对
每个弱点都有规避办法,共同的思路是「外观交给 3DGS,其余交给网格」的分工——这也是 NVIDIA 给出的架构。
应对 1:并用碰撞网格(物理)
在与 3DGS 相同的位置叠放一个简易网格(代理网格),把碰撞设在它身上:外观由泼溅负责,碰撞判定由网格负责,网格本身设为不可见。
from pxr import UsdPhysics, UsdGeom
mesh = stage.GetPrimAtPath("/World/CollisionMesh")
UsdPhysics.CollisionAPI.Apply(mesh) # 赋予碰撞
UsdGeom.Imageable(mesh).MakeInvisible() # 隐藏外观
网格来源有三条路:用扫描仪同时输出的网格最省事;没有的话,splat-transform 可以直接从 3DGS 生成碰撞网格;或者用简单的箱体近似地板、墙壁、障碍物——如果用途只是验证「机器人能不能通过」,箱体往往就够了。
这一思路的完整落地案例,是同一团队的后续文章:以 LiDAR 实测数据修补碰撞网格的地面孔洞,在涩谷十字路口叠加双层环境,并用 PhysX Vehicle 验证 53 米直行零穿透(中译本见 RobotWorld 博客)。
应对 2:按区域分割打标签(语义)
标签的单位是 prim,那么分开 prim 就能分开标签:把泼溅在空间上切开,分别导出为不同的 USD,再逐个赋标签。
分割在导入 Isaac Sim 之前完成:用 SuperSplat(PlayCanvas 的免费编辑器)选中区域单独导出,或按坐标机械切割。但工序量与分割细度成正比——「道路/建筑/其他」级别的粗分类是现实的,按物体逐一手工标注则不具实用性。
更稳妥的做法:把标签放在网格一侧。需要精细语义时,把带标签的网格作为独立图层叠上去才是现实解:复用应对 1 的碰撞网格,按部位拆分并赋标签。分割输出取自网格层,外观取自泼溅层。公开数据集也已经采用这种结构:泼溅 USD 与碰撞 USD 分开分发,使用时再合成。
应对 3:光照要么接受,要么换手段
对 3DGS 本体做重打光不在 Isaac Sim 的能力范围内。需要天气和时段的变化的话,可以把输出图像交给图像生成 AI 后处理,或者改变条件对同一场景扫描多次。UE5 一侧有支持重打光的插件,使用那边制作的素材也是一种办法。
判断用武之地
综合以上,3DGS 适合与不适合的用途分得很清楚:
| 用途 | 仅靠 3DGS | 补充说明 |
|---|---|---|
| 实写品质的背景 / 外观验证 | ◎ | 最大强项,真实现场原样复现 |
| 相机图像生成(作为背景) | ○ | 角度自由,但没有像素级真值标签 |
| 机器人移动 / 碰撞验证 | × | 必须并用碰撞网格 |
| 分割训练数据 | × | 需要叠加带标签的网格 |
| 物体级操作 / 替换 | × | 不分割做不到;并用 CG 资产更快 |
| 光照条件变化 | × | 已烘焙,需要别的手段 |
归纳起来:3DGS 是「把真实环境作为实写品质背景搬进来」的技术。在其中活动的目标物、需要真值标签的部分,照旧用网格准备。只要做好这种分工,3DGS 就是一个非常强力的选项。
08 总结
导入流程是一条直线:「点数压到 224 以下 → 转换为 .usdc → 用 OverridePrim 引用 → 矫正朝向 → 把相机放到轨迹上」。容易栽跟头的是首尾两端:点数超限会毫无报错地什么都不显示;相机放到扫描范围之外只能看到破碎的面。这两点掌握了,其余都会顺利推进。
用于工业用途时,把 3DGS 理解为「把真实环境作为实写品质背景搬进来的技术」——不多不少——就是捷径。物体级分离、语义标签、碰撞判定,泼溅本身一概没有;需要的话,就把网格作为独立图层叠上去分工。反过来看,这也说明「外观的忠实度」是其他手段无法替代的。
按用途与 UE5 分开使用也是现实选择:作为影像素材的输出,参见作者的 UE5 × XGRIDs SDK 文章。
来源:LocaHun 3D 技术博客 —— NVIDIA Isaac Simに3DGSを持ち込む(作者:中村航 / Kou Nakamura)
原文来源:LocaHun 3D 技术博客https://web.locahun3d.com/works/isaacsim-3dgs-import.html
