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
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 知识体系结构图
下面这张图展示了本章的核心知识脉络:
3.5 避坑指南与实战建议
最后,分享几个我踩过的坑:
- 路径不要用中文: USD 对中文路径支持不好,我曾经用中文命名物体,结果加载时报了一堆莫名其妙的错。
- 操作顺序: 平移、旋转、缩放的顺序会影响最终结果。如果你发现物体位置不对,先检查操作顺序。
- 保存前检查: 保存场景前,最好用
stage.Traverse()遍历一遍,确认所有物体都在正确的位置。 - 父子关系不要循环: 别把父物体设为子物体的子物体——这会导致无限递归,Isaac Sim 会直接崩溃。
/World 根节点,然后把所有物体都放在它下面。这样场景结构清晰,也方便后续导出和复用。