3. USD与场景构建
在Isaac Sim中,场景构建是一切仿真工作的起点。无论是搭建一个简单的物体掉落实验,还是构建完整的仓储物流环境,都离不开对USD(Universal Scene Description,通用场景描述)的理解。USD最初由皮克斯动画工作室开发,用于解决电影制作中大规模场景数据的协作与交换问题,如今已成为NVIDIA Omniverse和Isaac Sim的底层数据格式。可以这样理解:USD之于Isaac Sim,就像HTML之于网页——它定义了场景中“有什么”“在哪里”“如何组织”。本章将系统讲解USD的核心概念、场景层级结构、Prim操作流程以及环境搭建的基础方法,帮助你建立清晰的场景构建思维。
3.1 USD核心概念:从Prim到场景的基石
USD的核心设计思想是“分层合成”(Layering & Composition)。一个USD场景由多个层(Layer)叠加而成,每个层可以包含对场景的部分描述,最终通过合成规则生成一个统一的“舞台”(Stage)。这种设计使得多个开发者可以并行编辑同一场景的不同部分,而不会互相覆盖。在Isaac Sim中,我们通常直接操作Stage对象,但理解层的概念有助于排查场景加载异常的问题。
在USD中,Prim(Primitive的缩写)是场景图的基本节点。每个Prim代表场景中的一个实体,可以是一个物体、一个光源、一个相机,甚至是一个逻辑分组。Prim通过路径(Path)唯一标识,例如 /World/Table 表示World下的Table Prim。Prim可以包含以下内容:
- 属性(Attribute):Prim的数据字段,如位置(translate)、旋转(rotate)、缩放(scale)、颜色(color)等。属性有明确的类型,如float、double、bool、、color3f等。
- 关系(Relationship):Prim之间的引用或连接,例如一个Mesh Prim的材质关系指向某个Material Prim。
- 元数据(Metadata):描述Prim自身的信息,如显示名称、文档说明、激活状态(active/inactive)等。
在Isaac Sim中,最常用的Prim类型包括:Xform(变换节点,用于分组和定位)、Mesh(网格几何体)、Camera(相机)、Light(光源)、Material(材质)、PhysicsScene(物理场景)、RigidBody(刚体)、Collider(碰撞体)等。理解这些Prim类型是后续搭建仿真环境的基础。
3.2 场景层级结构:Stage、Prim与Property的组织逻辑
USD场景采用树状层级结构,从Stage(舞台)根节点开始,向下逐级展开。Stage是场景的顶层容器,相当于一个完整的USD文件。在Isaac Sim中,每个仿真环境对应一个Stage。Stage之下是Prim层级,Prim可以嵌套,形成父子关系。父Prim的变换会传递给子Prim,这是场景组织中最核心的规则。
一个典型的场景层级如下:
/World
├── Ground (Xform)
│ └── GroundPlane (Mesh + Collider)
├── Table (Xform)
│ ├── TableTop (Mesh + Collider)
│ └── TableLeg (Mesh + Collider)
├── Robot (Xform)
│ ├── Base (Mesh + RigidBody)
│ └── Arm (Xform)
│ └── Link1 (Mesh + RigidBody)
└── Lighting (Xform)
├── Sun (Light)
└── Camera (Camera)
这种层级结构的设计有三大优势:变换继承(移动父节点,子节点跟随移动)、逻辑分组(将相关物体组织在一起,便于统一操作)、路径寻址(通过路径可以精确访问任意Prim)。在Isaac Sim的Stage面板中,你可以直观地看到这棵树,并拖拽调整层级关系。
| 层级 | 说明 | 示例 | 操作方式 |
|---|---|---|---|
| Stage | 场景根节点,包含所有内容 | 当前打开的USD文件 | 通过 omni.usd.get_context().get_stage() 获取 |
| Prim | 场景中的实体节点 | /World/Table | 通过 stage.GetPrimAtPath() 获取 |
| Property | Prim的属性或关系 | translate、rotate、material:binding | 通过 prim.GetAttribute() 获取 |
3.3 Prim操作实战:创建、查询、修改与删除
在Isaac Sim中,Prim操作主要通过Omniverse USD API完成。以下是最常用的操作流程,建议在Script Editor中逐步执行验证。
3.3.1 创建Prim
创建Prim的标准流程是:先获取Stage,然后定义路径和类型,最后设置必要的属性。以下代码创建一个位于 /World/Table 下的立方体:
import omni.usd
from pxr import UsdGeom, UsdPhysics, Gf
# 获取当前Stage
stage = omni.usd.get_context().get_stage()
# 创建Xform作为父节点
xform_path = "/World/Table"
xform_prim = UsdGeom.Xform.Define(stage, xform_path)
# 创建立方体Mesh
cube_path = "/World/Table/Cube"
cube_prim = UsdGeom.Cube.Define(stage, cube_path)
# 设置位置属性
cube_prim.AddTranslateOp().Set(Gf.Vec3d(0.0, 0.0, 0.5))
# 添加碰撞体
UsdPhysics.ColliderAPI.Apply(cube_prim.GetPrim())
3.3.2 查询Prim
查询Prim是调试场景的常用操作。你可以通过路径直接获取Prim,也可以遍历子Prim。以下代码演示如何遍历 /World 下的所有子Prim并打印其路径和类型:
world_prim = stage.GetPrimAtPath("/World")
for child in world_prim.GetChildren():
print(f"路径: {child.GetPath()}, 类型: {child.GetTypeName()}")
3.3.3 修改Prim属性
修改属性是动态调整场景的关键。例如,在仿真过程中移动物体位置:
cube_prim = stage.GetPrimAtPath("/World/Table/Cube")
cube_xform = UsdGeom.Xformable(cube_prim)
# 清除已有的变换操作
cube_xform.ClearXformOpOrder()
# 添加新的平移操作
cube_xform.AddTranslateOp().Set(Gf.Vec3d(1.0, 2.0, 0.5))
3.3.4 删除Prim
删除Prim相对简单,但需要注意:删除后无法通过Undo恢复(在Script Editor中)。建议在删除前先确认路径正确:
stage.RemovePrim("/World/Table/Cube")
3.4 环境搭建基础:从空场景到可仿真环境
搭建一个可用的仿真环境,需要完成四个基础步骤:设置Stage参数、创建地面、添加光照、配置物理场景。以下是一个完整的环境搭建流程,适用于大多数AGV仿真项目。
3.4.1 Stage设置
Stage设置包括单位、重力方向和坐标系。Isaac Sim默认使用米(meters)作为单位,Z轴向上。在创建环境前,建议先确认Stage的元数据:
from pxr import UsdGeom
stage = omni.usd.get_context().get_stage()
# 设置Stage单位为米
UsdGeom.SetStageMetersPerUnit(stage, 1.0)
# 设置Stage轴为Z轴向上
UsdGeom.SetStageUpAxis(stage, UsdGeom.Tokens.z)
3.4.2 创建地面与光照
地面是AGV仿真中最基础的碰撞面。Isaac Sim提供了快速创建地面的方法,同时需要添加光照以保证视觉可见性:
# 创建地面(带碰撞)
from omni.isaac.core.utils.stage import add_reference_to_stage
from omni.isaac.core.utils.prims import create_prim
# 使用Isaac Sim内置的地面资产
create_prim("/World/Ground", "Xform")
create_prim("/World/Ground/Collider", "Plane",
attributes={"size": 100.0, "semantic:type": "ground"})
# 添加光照
create_prim("/World/Lighting/DomeLight", "DomeLight",
attributes={"intensity": 1000.0})
3.4.3 配置物理场景
物理场景是仿真能够运行的前提。Isaac Sim默认会创建一个PhysicsScene,但建议显式配置以确保参数正确:
from pxr import UsdPhysics, Gf
# 创建物理场景
physics_scene = UsdPhysics.Scene.Define(stage, "/World/PhysicsScene")
# 设置重力
physics_scene.CreateGravityDirectionAttr().Set(Gf.Vec3f(0.0, 0.0, -1.0))
physics_scene.CreateGravityMagnitudeAttr().Set(9.81)
3.5 案例拆解:搭建一个AGV测试环境
下面通过一个完整的案例,将本章知识串联起来。假设我们需要搭建一个AGV小车测试环境,包含地面、障碍物、目标点和AGV本体(简化为一个立方体)。
场景需求:地面尺寸20m×20m,障碍物位于(3, 2, 0)处,目标点位于(8, 5, 0)处,AGV初始位置在(0, 0, 0.5)。
操作步骤:
- 创建Stage并设置单位为米,Z轴向上。
- 创建地面:使用Plane Prim,尺寸20×20,添加Collider。
- 创建障碍物:使用Cube Prim,尺寸0.5×0.5×1.0,设置位置(3, 2, 0.5),添加Collider和RigidBody(固定质量)。
- 创建目标点:使用Sphere Prim,半径0.2,设置位置(8, 5, 0.5),添加视觉材质(绿色)。
- 创建AGV:使用Cube Prim,尺寸0.8×0.6×0.4,设置位置(0, 0, 0.5),添加Collider和RigidBody(质量10kg)。
- 添加光照和相机,调整视角。
关键代码片段:
# 创建障碍物
obstacle_path = "/World/Obstacle"
UsdGeom.Cube.Define(stage, obstacle_path)
obstacle = stage.GetPrimAtPath(obstacle_path)
obstacle.GetAttribute("size").Set(0.5)
UsdGeom.Xformable(obstacle).AddTranslateOp().Set(Gf.Vec3d(3.0, 2.0, 0.5))
UsdPhysics.ColliderAPI.Apply(obstacle)
UsdPhysics.RigidBodyAPI.Apply(obstacle)
# 创建AGV
agv_path = "/World/AGV"
UsdGeom.Cube.Define(stage, agv_path)
agv = stage.GetPrimAtPath(agv_path)
agv.GetAttribute("size").Set(0.8)
UsdGeom.Xformable(agv).AddTranslateOp().Set(Gf.Vec3d(0.0, 0.0, 0.5))
UsdPhysics.ColliderAPI.Apply(agv)
rb = UsdPhysics.RigidBodyAPI.Apply(agv)
rb.CreateMassAttr(10.0)
结果验证:运行仿真后,AGV在重力作用下落在地面上,障碍物保持静止,目标点可见。通过Stage面板可以查看完整的Prim层级结构。
复盘经验:在搭建环境时,建议先创建所有静态物体(地面、障碍物),再创建动态物体(AGV),最后添加光照和相机。这样可以避免在调整层级时影响物理属性。另外,所有物体的初始位置应确保不与地面重叠,否则物理引擎会强制修正位置,导致视觉跳动。
3.6 常见误区与总结
在USD场景构建中,初学者常犯以下错误:
- 忽略Xform父节点:直接创建Mesh而不挂载在Xform下,导致无法通过Transform工具操作。
- 路径错误:USD路径区分大小写,且必须从根节点
/开始。使用/World/table和/World/Table是两个不同的Prim。 - 属性类型不匹配:设置属性时使用了错误的数据类型,例如将float值赋给double属性,会导致赋值失败或精度损失。
- 忘记添加Collider:物体没有碰撞体,物理仿真中会直接穿透地面。
- 物理API应用顺序错误:先添加RigidBody再添加Collider,或反之,可能导致物理引擎无法正确识别。