4. USD 基础与场景构建

好,咱们进入第四章。说实话,很多刚接触 Isaac Sim 的朋友,上来就被 .usd 文件搞懵了。它到底是什么?为什么 NVIDIA 放着好好的 .fbx、.obj 不用,非要搞个 USD?

我当年第一次接触 USD 时,也觉得这东西太抽象了。但后来在搭建一个多机械臂协作产线时,才真正体会到它的威力——没有 USD,那个项目根本做不下去。今天我就把 USD 的核心概念掰开揉碎讲给你听。

4.1 USD 到底是什么?

USD(Universal Scene Description)是皮克斯开源的一套场景描述框架。说白了,它不只是一个文件格式,而是一整套场景数据组织、组合、传递的规范。

你想想看,一个复杂的仿真场景里,可能有机械臂、传送带、工件、传感器、灯光、物理属性……这些东西如果都塞进一个文件里,谁来维护?谁改了什么?版本怎么控制?

USD 的答案很聪明:分层 + 非破坏性覆盖。每个环节只管自己的事,最后通过「合成」拼成完整场景。

核心认知:USD 不是建模工具,而是场景数据的「操作系统」。它管理的是数据怎么组合、怎么继承、怎么覆盖,而不是怎么画模型。

4.2 三大核心概念:Prim、Layer、Composition

这三个词你会在 Isaac Sim 里天天见到。我一个个说。

4.2.1 Prim——场景里的最小单元

Prim 就是 USD 世界里的「东西」。一个机械臂是 Prim,一个关节是 Prim,一个材质球也是 Prim。你可以把它理解成面向对象里的「对象实例」。

每个 Prim 都有:

  • 路径:比如 /World/robot_arm/shoulder_joint,类似文件系统的路径
  • 类型:Xform(变换节点)、Mesh(网格)、Joint(关节)、Camera(相机)等
  • 属性:位置、旋转、缩放、颜色、质量……
  • 元数据:比如 kind(种类)、purpose(用途,如渲染/代理/碰撞)
# 一个简单的 Prim 定义示例(.usda 格式)
def Xform "World"
{
    def Mesh "ground_plane"
    {
        float3[] extent = [(-10, -0.01, -10), (10, 0, 10)]
        int[] faceVertexCounts = [4]
        int[] faceVertexIndices = [0, 1, 2, 3]
        point3f[] points = [(-10, 0, -10), (10, 0, -10), (10, 0, 10), (-10, 0, 10)]
        uniform  subdivisionScheme = "none"
    }
}
我的习惯:在 Isaac Sim 里,我通常把场景根节点命名为 /World,所有静态物体放 /World/static,动态物体放 /World/dynamic。这样后期加物理、加碰撞时,一眼就能找到目标。

4.2.2 Layer——场景的「透明胶片」

Layer 是 USD 最巧妙的设计之一。你可以把它想象成 Photoshop 里的图层:

  • 每个 Layer 只记录自己关心的数据
  • 多个 Layer 叠在一起,形成最终场景
  • 上层 Layer 可以覆盖下层 Layer 的数据
  • 每个 Layer 可以独立保存、独立版本控制

举个例子。我在做抓取仿真时,会这样分层:

层级 内容 谁维护
Layer 0(底层) 机械臂基础模型、关节定义 供应商/建模师
Layer 1 物理属性、碰撞体、质量 仿真工程师(我)
Layer 2 抓取目标、工件位置 工艺工程师
Layer 3(顶层) 灯光、相机、调试辅助线 我自己调试用

这样做的好处是:供应商更新了机械臂模型,我只需要替换 Layer 0,其他层完全不受影响。我曾经因为没分层,手动改了一个 .usd 文件里的 200 多个关节位置……嗯,那次之后我再也不敢偷懒了。

4.2.3 Composition——怎么把这些层「合」起来

Composition 是 USD 的合成引擎。它定义了 Layer 之间如何叠加、覆盖、引用。常用的合成方式有:

  • SubLayers:最简单的叠加,后加载的 Layer 覆盖先加载的
  • References:引用另一个 .usd 文件作为子场景,类似编程里的 import
  • Payloads:延迟加载,场景打开时不加载细节,需要时才加载——对大场景性能至关重要
  • Inherits:类继承机制,子 Prim 继承父 Prim 的属性
  • Specializes:更精细的覆盖,优先级高于 Inherits
避坑指南:我曾经在引用一个大型产线模型时,直接用了 References,结果 Isaac Sim 启动花了 5 分钟。后来改成 Payloads 延迟加载,启动时间降到 20 秒。记住:大模型用 Payloads,小零件用 References。

4.3 在 Isaac Sim 里实操:创建、编辑、保存

理论说完了,咱们上手。打开 Isaac Sim,跟着我做。

4.3.1 创建新场景

  1. 点击菜单 File → New Stage,创建一个空场景
  2. 在 Stage 面板(默认在左侧)右键根节点,选择 Add Prim → Xform,命名为 World
  3. 在 World 下继续添加:Mesh(地面)、Sphere(工件)、Capsule(障碍物)等
  4. 用 Transform 工具(快捷键 W/E/R)调整位置、旋转、缩放
# 你也可以直接在 USD Composer 里写代码创建
from pxr import UsdGeom, Sdf

stage = omni.usd.get_context().get_stage()
world_prim = stage.DefinePrim("/World", "Xform")
sphere = stage.DefinePrim("/World/sphere", "Sphere")
sphere.GetAttribute("radius").Set(0.1)

4.3.2 编辑场景属性

选中任意 Prim,在 Property 面板(右侧)可以修改:

  • Transform:位置、旋转、缩放、枢轴点
  • Visibility:可见性(Invisible 用于隐藏辅助物体)
  • Purpose:用途(default 渲染用、proxy 代理用、guide 辅助用)
  • Physics:物理属性(需要先添加 Physics 组件)
小技巧:按住 Ctrl 拖动物体,可以复制出一个新物体。我经常用这个功能快速摆满一排工件。

4.3.3 保存场景——分层保存是关键

保存时,Isaac Sim 默认会把所有内容存到一个 .usd 文件里。但我不建议这么做。

我的标准流程是:

  1. 先保存基础模型为 robot_base.usd(只包含几何和关节)
  2. 再保存物理配置为 robot_physics.usd(引用 robot_base.usd,添加碰撞和物理属性)
  3. 最后保存完整场景为 grasp_scene.usd(引用前两个,加上工件、灯光、相机)
# 在 Isaac Sim 里通过 Python 分层保存
from pxr import Usd, Sdf

# 保存基础层
stage_base = Usd.Stage.CreateNew("robot_base.usd")
# ... 添加模型数据 ...
stage_base.Save()

# 保存物理层(引用基础层)
stage_physics = Usd.Stage.CreateNew("robot_physics.usd")
stage_physics.GetRootLayer().subLayerPaths.append("./robot_base.usd")
# ... 添加物理属性 ...
stage_physics.Save()

4.4 知识体系总览

下面这张图,是我自己总结的 USD 在 Isaac Sim 里的知识结构。你保存下来,后面每学一章都可以回来对照。

USD 在 Isaac Sim 中的知识体系 USD 核心概念 Prim(场景单元) Layer(分层) Composition(合成) 路径 / 类型 属性 / 元数据 Xform / Mesh / Joint 独立保存 版本控制 非破坏性覆盖 SubLayers References Payloads Isaac Sim 实操:创建 → 编辑 → 分层保存 提示:每个分支都可以独立学习,但实操时三者缺一不可

4.5 本章小结

USD 的三个核心概念,你记住了吗?

  • Prim 是场景里的每个物体——路径、类型、属性,缺一不可
  • Layer 是场景的透明胶片——分层保存,互不干扰
  • Composition 是胶水——把各层按规则合成最终场景

我个人觉得,USD 最难的不是概念本身,而是「分层思维」的转变。很多工程师习惯把所有东西塞一个文件里,改起来心惊胆战。但一旦你习惯了分层,你会发现:场景不再是静态的「文件」,而是动态的「数据流」。