场景图与Stage操作:掌握Isaac Sim场景管理的核心方法
在Isaac Sim的Python二次开发中,Stage(场景)是所有虚拟世界内容的根容器。无论是加载一个机器人模型、摆放障碍物,还是查询物体的当前位姿,所有操作都围绕Stage展开。理解Stage的数据结构——即USD(Universal Scene Description)场景图,是进行高效开发的前提。本章将系统讲解Stage的概念、USD场景的加载与保存、场景图节点的遍历方法,以及物体属性(位置、姿态、缩放)的读写技巧,帮助你建立对Isaac Sim场景操作的全局认知。
上图展示了Stage操作的三条核心主线:加载/保存USD文件、遍历场景图节点、读写物体属性。这三者构成了Isaac Sim场景管理的基础闭环。下面我们将逐一深入剖析每个环节的具体实现方法与实战要点。
4.1 Stage核心概念与USD场景图结构
核心原理:Stage在Isaac Sim中对应USD的Usd.Stage对象,它是整个虚拟世界的根容器。USD采用层级化的场景图(Scene Graph)结构,所有物体(称为Prim)都挂载在Stage之下,形成一棵树。每个Prim可以拥有子Prim,也可以包含属性(Attribute)和关系(Relationship)。这种设计使得场景的组织、复用和协作变得高效。
在Isaac Sim中,Stage的获取方式非常简单:
from pxr import Usd, UsdGeom
# 获取当前活动的Stage
stage = omni.usd.get_context().get_stage()
# 或者通过Usd API直接创建/打开
stage = Usd.Stage.Open("/path/to/scene.usd")
Stage对象一旦获取,你就可以通过它访问场景中的所有Prim。每个Prim都有一个唯一的路径(如/World/Robot),路径的层级关系直接反映了场景图的父子关系。
/World通常是默认的根Prim,所有用户添加的物体都位于其下。
4.2 加载与保存USD场景
操作流程:加载和保存USD场景是开发中最频繁的操作之一。Isaac Sim提供了两种主要方式:通过UI界面操作,或通过Python API编程控制。对于二次开发,掌握API方式至关重要。
| 操作类型 | API方法 | 说明 | 典型场景 |
|---|---|---|---|
| 打开场景 | omni.usd.get_context().open_stage(path) | 加载USD文件到当前Stage | 启动时加载预设环境 |
| 保存场景 | omni.usd.get_context().save_stage() | 保存当前Stage到原路径 | 修改后持久化 |
| 另存为 | omni.usd.get_context().save_as_stage(path) | 保存到新路径 | 创建场景副本 |
| 新建场景 | omni.usd.get_context().new_stage() | 创建空白Stage | 从零搭建场景 |
加载场景时,需要注意路径格式。Isaac Sim支持本地文件路径(/home/user/scene.usd)、相对路径(./scenes/room.usd)以及Omniverse服务器URL(omniverse://server-name/Projects/scene.usd)。
# 加载场景的完整示例
import omni.usd
from pxr import Usd
# 获取USD上下文
usd_context = omni.usd.get_context()
# 打开场景
result = usd_context.open_stage("/home/user/isaac_sim_scenes/warehouse.usd")
if result:
print("场景加载成功")
stage = usd_context.get_stage()
print(f"Stage路径: {stage.GetRootLayer().identifier}")
else:
print("场景加载失败,请检查路径")
# 保存场景
usd_context.save_stage()
print("场景已保存")
save_stage()会抛出异常。建议在保存前检查文件权限,或使用try-except捕获异常。另外,频繁保存大场景可能导致性能下降,建议在关键节点手动保存。
4.3 遍历场景图节点
判断方法:遍历场景图是查找和操作特定物体的基础。USD提供了多种遍历方式,你可以根据需求选择最合适的方法。
- 全量遍历:使用
stage.Traverse()遍历所有Prim,适合小型场景或调试。 - 按路径访问:使用
stage.GetPrimAtPath(path)直接获取指定路径的Prim,效率最高。 - 按类型过滤:结合
GetAllPrimsOfType()或遍历时判断类型,适合查找特定类型的物体。 - 层级遍历:使用
prim.GetChildren()逐层访问子Prim,适合处理层级关系明确的场景。
# 遍历场景图的所有Prim
from pxr import Usd, UsdGeom
stage = omni.usd.get_context().get_stage()
# 方法1:全量遍历
print("=== 全量遍历 ===")
for prim in stage.Traverse():
print(f"Prim路径: {prim.GetPath()}, 类型: {prim.GetTypeName()}")
# 方法2:按路径直接访问
robot_prim = stage.GetPrimAtPath("/World/Robot")
if robot_prim.IsValid():
print(f"找到Robot: {robot_prim.GetPath()}")
# 方法3:按类型过滤(查找所有Mesh)
print("=== 所有Mesh物体 ===")
for prim in stage.Traverse():
if prim.IsA(UsdGeom.Mesh):
print(f"Mesh: {prim.GetPath()}")
# 方法4:层级遍历
world_prim = stage.GetPrimAtPath("/World")
if world_prim.IsValid():
print("=== World的子节点 ===")
for child in world_prim.GetChildren():
print(f"子节点: {child.GetPath()}")
stage.Traverse()可能会遍历数千个Prim,导致性能下降。建议优先使用GetPrimAtPath()直接访问已知路径,或使用GetAllPrimsOfType()按类型过滤,避免不必要的全量遍历。
4.4 获取与设置物体属性(位置、姿态、缩放)
核心原理:物体的位置、姿态(旋转)和缩放统称为变换(Transform),在USD中通过UsdGeom.XformableAPI管理。每个可变换的Prim都包含一个Xform操作列表,这些操作按顺序叠加,最终形成物体的世界变换矩阵。
在Isaac Sim中,操作变换有三种主要方式:
- 直接操作属性:通过
GetAttribute()和SetAttribute()读写translate、rotate、scale属性。 - 使用Xformable API:通过
UsdGeom.Xformable封装的方法,更安全、更便捷。 - 使用Isaac Sim的TransformPrim:提供更高层的封装,支持世界坐标和局部坐标的转换。
# 获取和设置物体变换的完整示例
from pxr import Usd, UsdGeom, Gf
stage = omni.usd.get_context().get_stage()
robot_prim = stage.GetPrimAtPath("/World/Robot")
if robot_prim.IsValid():
# 方法1:使用Xformable API(推荐)
xformable = UsdGeom.Xformable(robot_prim)
# 获取世界变换矩阵
world_transform = xformable.ComputeLocalToWorldTransform(Usd.TimeCode.Default())
print(f"世界变换矩阵: {world_transform}")
# 设置局部位置
xformable.ClearXformOpOrder()
translate_op = xformable.AddTranslateOp()
translate_op.Set(Gf.Vec3d(1.0, 2.0, 0.5))
# 设置旋转(欧拉角)
rotate_op = xformable.AddRotateXYZOp()
rotate_op.Set(Gf.Vec3d(0, 0, 90)) # 绕Z轴旋转90度
# 设置缩放
scale_op = xformable.AddScaleOp()
scale_op.Set(Gf.Vec3d(1.5, 1.5, 1.5))
# 方法2:直接操作属性(更底层)
translate_attr = robot_prim.GetAttribute("xformOp:translate")
if translate_attr.IsValid():
translate_attr.Set(Gf.Vec3d(2.0, 3.0, 1.0))
# 读取位置
current_translate = translate_attr.Get()
print(f"当前位置: {current_translate}")
对于姿态操作,USD支持多种旋转表示方式:欧拉角(XYZ)、四元数、旋转矩阵。在Isaac Sim中,最常用的是欧拉角和四元数。四元数可以避免万向锁问题,适合复杂的旋转插值。
# 使用四元数设置旋转
from pxr import Gf
# 创建四元数(w, x, y, z)
quat = Gf.Quatd(1.0, 0.0, 0.0, 0.0) # 单位四元数,无旋转
rotate_op = xformable.AddRotateOp(UsdGeom.XformOp.TypeRotateXYZ)
rotate_op.Set(Gf.Vec3d(45, 30, 60)) # 欧拉角方式
# 或者直接设置四元数
quat_attr = robot_prim.GetAttribute("xformOp:orient")
if quat_attr.IsValid():
quat_attr.Set(Gf.Quatf(0.707, 0.0, 0.707, 0.0)) # 绕Y轴旋转90度
4.5 实战案例:搭建并操作一个简单的机器人场景
案例背景:假设你需要为一个机器人抓取任务搭建测试环境。场景中包含一个机器人模型(位于/World/Robot)、一个目标物体(位于/World/Target)和一个地面(/World/Ground)。你需要完成以下任务:加载场景、调整机器人位置、旋转目标物体、缩放地面,并验证所有变换是否正确。
初始状态:场景文件robot_env.usd已存在,但所有物体的变换均为默认值(位置原点、无旋转、缩放1.0)。
操作步骤:
import omni.usd
from pxr import Usd, UsdGeom, Gf
# 1. 加载场景
usd_context = omni.usd.get_context()
usd_context.open_stage("/home/user/scenes/robot_env.usd")
stage = usd_context.get_stage()
# 2. 调整机器人位置到 (1.0, 0.5, 0.0)
robot_prim = stage.GetPrimAtPath("/World/Robot")
robot_xform = UsdGeom.Xformable(robot_prim)
robot_xform.ClearXformOpOrder()
translate_op = robot_xform.AddTranslateOp()
translate_op.Set(Gf.Vec3d(1.0, 0.5, 0.0))
# 3. 旋转目标物体绕Z轴45度
target_prim = stage.GetPrimAtPath("/World/Target")
target_xform = UsdGeom.Xformable(target_prim)
target_xform.ClearXformOpOrder()
rotate_op = target_xform.AddRotateXYZOp()
rotate_op.Set(Gf.Vec3d(0, 0, 45))
# 4. 缩放地面到 (10, 10, 1)
ground_prim = stage.GetPrimAtPath("/World/Ground")
ground_xform = UsdGeom.Xformable(ground_prim)
ground_xform.ClearXformOpOrder()
scale_op = ground_xform.AddScaleOp()
scale_op.Set(Gf.Vec3d(10, 10, 1))
# 5. 验证变换结果
print("=== 变换验证 ===")
for prim_path in ["/World/Robot", "/World/Target", "/World/Ground"]:
prim = stage.GetPrimAtPath(prim_path)
xform = UsdGeom.Xformable(prim)
world_transform = xform.ComputeLocalToWorldTransform(Usd.TimeCode.Default())
translation = world_transform.ExtractTranslation()
print(f"{prim_path}: 位置 = {translation}")
# 6. 保存场景
usd_context.save_stage()
print("场景已保存")
结果对比:执行上述代码后,机器人移动到(1.0, 0.5, 0.0),目标物体绕Z轴旋转45度,地面在X和Y方向放大10倍。通过ComputeLocalToWorldTransform()验证,所有变换均正确应用。
1. 每次修改变换前,先调用
ClearXformOpOrder()清除旧的变换操作,避免叠加冲突。2. 使用
ComputeLocalToWorldTransform()验证结果,确保变换符合预期。3. 保存前检查所有Prim路径是否正确,避免无效引用。
4.6 常见误区与总结复盘
常见误区:
- 误区1:混淆局部坐标与世界坐标。默认情况下,
AddTranslateOp()设置的是局部坐标。如果需要设置世界坐标,必须先获取父Prim的世界变换,进行坐标转换。 - 误区2:忽略XformOpOrder。如果Prim已有变换操作,直接添加新的操作会导致变换叠加,产生意外结果。务必先
ClearXformOpOrder()。 - 误区3:使用无效的Prim路径。在操作前未检查
IsValid(),导致运行时异常。 - 误区4:忘记保存场景。修改内存中的Stage后,未调用
save_stage(),导致修改丢失。
总结复盘:本章围绕Stage操作建立了完整的知识体系。核心要点包括:Stage是USD场景图的根容器,所有操作都通过Usd.Stage对象完成;加载/保存场景使用open_stage()和save_stage();遍历场景图时优先使用路径直接访问,避免全量遍历;变换操作推荐使用UsdGeom.XformableAPI,注意局部坐标与世界坐标的区别。
ComputeLocalToWorldTransform()会消耗较多计算资源。如果需要批量获取多个物体的变换,建议先缓存Stage引用,并尽量减少不必要的矩阵计算。