3. 场景与物体管理:创建与加载 USD 场景、基本几何体操作

场景管理,说白了就是搭建你的仿真舞台。你想想看,没有舞台,演员往哪儿站?在 Isaac Sim 里,这个舞台就是 USD 场景。我个人习惯把 USD 理解成一种「万能容器」——它不仅能装几何体,还能装材质、灯光、动画,甚至物理属性。嗯,这一章我们就来聊聊怎么搭这个舞台,以及怎么摆弄舞台上的「演员」。

3.1 创建与加载 USD 场景

在 Isaac Sim 中,一切操作都围绕 USD 场景展开。你可以从零开始创建,也可以加载已有的场景文件。

3.1.1 创建新场景

用 Python 创建一个空场景,其实就一行代码:

from omni.usd import get_context

# 获取 USD 上下文并新建场景
usd_context = get_context()
usd_context.new_stage()

这段代码会创建一个全新的、空的 USD 舞台。我记得刚开始用的时候,总觉得少了点什么——嗯,确实,它连个地板都没有。不过别急,后面我们会手动加。

3.1.2 加载已有 USD 文件

如果你有现成的场景文件(比如 .usd、.usda、.usdz),可以直接加载:

usd_context.open_stage("path/to/your_scene.usd")
小提示: 路径建议用路径,或者确保工作目录正确。我曾经因为相对路径写错,浪费了半小时排查——结果发现是路径少了个斜杠。

3.1.3 保存场景

做完了修改,记得保存:

usd_context.save_stage()

我个人习惯每做几个关键操作就保存一次。仿真调试时程序崩了,没保存的场景就白干了。

3.2 基本几何体操作

场景有了,接下来就是往里面放东西。Isaac Sim 提供了几种基本几何体:立方体、球体、圆柱体、圆锥体、平面等。这些几何体是搭建复杂场景的「积木」。

3.2.1 创建立方体

下面这段代码会在场景原点创建一个立方体:

import omni.usd
from pxr import UsdGeom, Gf

# 获取当前舞台
stage = omni.usd.get_context().get_stage()

# 在路径 /World/Cube 下创建立方体
cube_path = "/World/Cube"
cube = UsdGeom.Cube.Define(stage, cube_path)

# 设置位置(平移)
cube.AddTranslateOp().Set(Gf.Vec3d(1.0, 0.0, 0.5))

# 设置旋转(欧拉角,单位:度)
cube.AddRotateXYZOp().Set(Gf.Vec3d(0.0, 45.0, 0.0))

# 设置缩放
cube.AddScaleOp().Set(Gf.Vec3d(2.0, 1.0, 1.0))

你看,位置、旋转、缩放分别对应三个操作:TranslateOp、RotateXYZOp、ScaleOp。顺序很重要——先平移再旋转,和先旋转再平移,结果完全不同。

注意: 旋转使用的是欧拉角,单位是度,不是弧度。我见过不少新手在这里翻车,旋转了半天发现角度不对。

3.2.2 其他几何体

创建球体、圆柱体、圆锥体的方式类似:

几何体 类名 示例代码
球体 UsdGeom.Sphere UsdGeom.Sphere.Define(stage, "/World/Sphere")
圆柱体 UsdGeom.Cylinder UsdGeom.Cylinder.Define(stage, "/World/Cylinder")
圆锥体 UsdGeom.Cone UsdGeom.Cone.Define(stage, "/World/Cone")
平面 UsdGeom.Plane UsdGeom.Plane.Define(stage, "/World/Plane")

3.2.3 批量操作与属性修改

有时候你需要批量修改物体的属性。比如,把所有几何体都移到同一高度:

import omni.usd
from pxr import UsdGeom, Gf

stage = omni.usd.get_context().get_stage()

# 获取所有 Xformable 类型的物体
for prim in stage.Traverse():
    if prim.IsA(UsdGeom.Xformable):
        xform = UsdGeom.Xformable(prim)
        # 获取已有的平移操作,如果没有则创建
        translate_op = xform.AddTranslateOp()
        current_pos = translate_op.Get()
        # 只修改 Y 轴高度为 0.2
        translate_op.Set(Gf.Vec3d(current_pos[0], 0.2, current_pos[2]))

这段代码会遍历场景中所有可变换的物体,把它们的 Y 轴高度统一设为 0.2。我在做机器人抓取仿真时经常用这个技巧——把散落一地的零件统一摆到桌面上。

3.3 物体的父子关系与层级结构

USD 场景的核心就是层级结构。每个物体都有一个路径,比如 /World/Cube,路径的每一级就是一个节点。你可以把物体组织成树状结构,父物体的变换会传递给子物体。

3.3.1 创建父子关系

创建父子关系很简单——只要把子物体的路径放在父物体下面就行:

from pxr import UsdGeom, Gf

stage = omni.usd.get_context().get_stage()

# 创建父物体(一个立方体)
parent = UsdGeom.Cube.Define(stage, "/World/ParentCube")
parent.AddTranslateOp().Set(Gf.Vec3d(0.0, 0.0, 0.0))

# 创建子物体(一个球体),路径在父物体之下
child = UsdGeom.Sphere.Define(stage, "/World/ParentCube/ChildSphere")
child.AddTranslateOp().Set(Gf.Vec3d(2.0, 0.0, 0.0))  # 相对于父物体的局部坐标

这里的关键点:子物体的位置是相对于父物体的。如果你移动父物体,子物体会跟着移动。我曾经用这个特性做了一个机械臂的仿真——每个关节都是父物体,后面的连杆是子物体,移动父关节,整个臂都跟着动。

3.3.2 重新设置父子关系

有时候你需要把已经存在的物体重新挂到另一个父物体下:

from pxr import UsdGeom, Sdf

stage = omni.usd.get_context().get_stage()

# 获取要移动的物体
prim = stage.GetPrimAtPath("/World/Cube")

# 重新设置路径(相当于改变父子关系)
new_path = Sdf.Path("/World/NewParent/Cube")
stage.CreatePrim(new_path)
# 复制属性并删除原物体
# 注意:这里需要手动处理属性复制,或者使用 Reparent API
核心概念: 在 USD 中,物体的路径就是它的「身份证」。改变路径,就改变了它在层级树中的位置。父物体的变换矩阵会级联传递给所有子物体。

3.3.3 层级遍历与查询

你可以遍历场景中的所有物体,或者只查询某个父物体下的子物体:

stage = omni.usd.get_context().get_stage()

# 遍历所有物体
for prim in stage.Traverse():
    print(f"路径: {prim.GetPath()}, 类型: {prim.GetTypeName()}")

# 只查询某个父物体下的直接子物体
parent_prim = stage.GetPrimAtPath("/World/ParentCube")
if parent_prim:
    for child in parent_prim.GetChildren():
        print(f"子物体: {child.GetPath()}")

嗯,这里要注意:Traverse() 会递归遍历所有层级,而 GetChildren() 只返回直接子物体。选哪个取决于你的需求。

3.4 知识体系结构图

下面这张图展示了本章的核心知识脉络:

场景与物体管理知识体系 场景与物体管理知识体系 创建与加载场景 基本几何体操作 父子关系与层级 new_stage() open_stage() 位置(Translate) 旋转(Rotate) 路径层级 变换传递 核心原则 场景是容器,几何体是演员,父子关系是剧本 路径即身份,变换即动作,层级即组织 保存与加载 批量属性修改 层级遍历查询

3.5 避坑指南与实战建议

最后,分享几个我踩过的坑:

  • 路径不要用中文: USD 对中文路径支持不好,我曾经用中文命名物体,结果加载时报了一堆莫名其妙的错。
  • 操作顺序: 平移、旋转、缩放的顺序会影响最终结果。如果你发现物体位置不对,先检查操作顺序。
  • 保存前检查: 保存场景前,最好用 stage.Traverse() 遍历一遍,确认所有物体都在正确的位置。
  • 父子关系不要循环: 别把父物体设为子物体的子物体——这会导致无限递归,Isaac Sim 会直接崩溃。
我的习惯: 每次创建新场景,我都会先创建一个 /World 根节点,然后把所有物体都放在它下面。这样场景结构清晰,也方便后续导出和复用。