4. 场景图与Stage操作

在Isaac Sim的Python二次开发中,Stage(场景)是所有虚拟世界内容的根容器。无论是加载一个机器人模型、摆放障碍物,还是查询物体的当前位姿,所有操作都围绕Stage展开。理解Stage的数据结构——即USD(Universal Scene Description)场景图,是进行高效开发的前提。本章将系统讲解Stage的概念、USD场景的加载与保存、场景图节点的遍历方法,以及物体属性(位置、姿态、缩放)的读写技巧,帮助你建立对Isaac Sim场景操作的全局认知。

Stage(场景根节点) 加载/保存USD open_stage / save_stage 支持 .usd / .usda / .usdc 可加载Omniverse服务器URL 遍历场景图节点 GetPrimsInStage / GetChildren 遍历所有Prim节点 按路径访问 / 按类型过滤 获取/设置属性 位置 / 姿态 / 缩放 GetAttribute / SetAttribute XformPrim API 封装 核心:一切操作均通过Usd.Stage对象完成 Stage是USD场景图的根,所有Prim节点挂载其下

上图展示了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),路径的层级关系直接反映了场景图的父子关系。

关键理解:USD场景图与文件系统类似,但更强大。Prim可以包含变换(Transform)、几何体(Geometry)、材质(Material)等多种类型的数据。在Isaac Sim中,/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中,操作变换有三种主要方式:

  1. 直接操作属性:通过GetAttribute()和SetAttribute()读写translate、rotate、scale属性。
  2. 使用Xformable API:通过UsdGeom.Xformable封装的方法,更安全、更便捷。
  3. 使用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()验证,所有变换均正确应用。

案例复盘:本案例展示了Stage操作的核心流程。关键经验包括:
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引用,并尽量减少不必要的矩阵计算。