⚠️ Alpha 内测版本:课程内容尚未完成,可能发生变更,欢迎通过 Issue 提交反馈。
跳到正文

L02 · 场景、实体与仿真生命周期 ​

课程状态: 讲义和可执行 notebook 均已提供。notebook 已在 CPU 和参考 AMD ROCm 平台上通过干净 kernel 验证,其中包括离屏渲染;L02 状态为 cpu-verified。

本讲定位 ​

L01 已经说明,“环境可用”并不是一个简单的是非判断:同一台机器对文档阅读、 核心仿真、图像渲染和完整训练流程的支持程度可能不同。L02 接下来回答所有后续 仿真程序都会遇到的第一个结构问题:

一段 Python 场景描述在什么时候才会变成可以推进和测量的仿真状态?

答案是 Genesis 的生命周期:

text
gs.init
  → 配置 Scene
  → 添加实体和相机
  → scene.build()
  → scene.step()
  → 读取状态和渲染观测

这套模式会反复出现在后续课程中:L03 的刚体实验、L04 的关节机器人、L05 的 逆运动学与相机,以及 L07 的完整抓取场景都会沿用它。本讲刻意只使用 Genesis 内置的 Plane 和 Box primitive(基本几何体),让生命周期本身清楚可见,而不是 被任务专用的高层封装遮住。

开始前,你应当能够:

  • 从干净 kernel 启动并运行 notebook;
  • 识别当前环境实际选择的后端;
  • 知道 gs.init() 是进程级初始化;
  • 区分“函数没有报错”和“结果已经得到验证”。

学习目标 ​

完成 L02 后,你应当能够:

  1. 在 Genesis 程序中标出初始化、拓扑声明、构建和运行四类操作,并识别错误的 调用顺序;
  2. 将 scene.add_entity(...) 的参数归入 Morph、Material 或 Surface,并解释 三者各自负责什么;
  3. 解释刚体对象的 Scene → RigidEntity → RigidLink → RigidGeom 层级,并在 一个 primitive 上检查实际结构;
  4. 在自动选择的已验证后端上构建并推进最小场景,读取位置、四元数和线速度, 报告它们的 shape、device、数值有限性和实际变化;
  5. 用受控的预期异常证明 build() 边界;
  6. 在启用渲染时验证离屏 RGB 观测,或在未启用时明确报告 SKIP,并用基于 状态数据的示意图完成核心实验。

从配置到运行时状态 ​

阅读仿真代码时,可以先把它分为三个阶段:

阶段需要回答的问题典型操作
声明这个世界里有哪些对象?创建 Scene,添加实体和相机
构建描述如何转化为可执行状态?scene.build()
运行接下来发生了什么,能够观测到什么?scene.step()、状态 getter、相机渲染

声明阶段创建的是用于描述场景的 Python 对象。例如,调用 scene.add_entity(...) 后会立即得到一个实体 handle(句柄),但这并不代表 全部运行时状态已经存在。求解器缓冲区和编译后的 kernel 要到构建边界才准备好。

构建成功后,运行阶段的操作才能推进和读取这些状态。这个区别解释了两个初看 有些意外的错误:

  • 在构建前读取刚体实体的位置,会得到包含 is not built yet 的错误;
  • 在构建后继续添加实体,会得到 Scene is already built. 错误。

这两条是生命周期约束,并非任意设置的限制。Genesis 只有先确定场景拓扑,才能 分配求解器状态,并准备操作这些状态所需的 kernel。

Scene 与 Entity ​

Scene 负责什么 ​

gs.Scene 是一个仿真世界的顶层容器,它把多类配置和状态组织在一起:

  • dt、substeps 等仿真时间配置;
  • 求解器专用选项;
  • 实体及其物理表示;
  • 相机和可视化配置;
  • 构建后由 scene.step() 推进的运行时状态。

L02 使用时间配置只是为了得到一个具体可运行的场景。L03 才会研究时间步长、 子步、接触和摩擦如何改变物理结果,不要从本讲的单条轨迹推导这些参数的效果。

Entity 是什么 ​

Entity(实体)是 scene.add_entity(...) 返回的运行时操作对象。代码会保留这个 handle,以便随后检查或修改它所代表的对象:

python
box = scene.add_entity(
    morph=gs.morphs.Box(
        size=(0.10, 0.10, 0.10),
        pos=(0.0, 0.0, 0.50),
    ),
    material=gs.materials.Rigid(rho=500.0),
    surface=gs.surfaces.Default(color=(0.20, 0.60, 0.90, 1.0)),
    name="falling_box",
)

这里的赋值并不是把一个位置复制到独立的 Python 数据结构中。box 指向的是由 scene 管理的对象;构建完成后,它的 getter 会读取场景当前的求解器状态。

刚体层级 ​

Entity 是 Genesis 中的通用概念。本讲的 Box 使用 gs.materials.Rigid,因此 这里只讨论刚体实体:

text
Scene
└── RigidEntity
    └── RigidLink
        └── RigidGeom

每一层的职责不同:

  • RigidEntity 是提供给用户的对象级 handle;
  • RigidLink 是实体中的一个刚体链节;
  • RigidGeom 是隶属于某个 link 的碰撞几何体。

一个 Box primitive 通常会产生一个刚体实体,其中包含一个 link 和一个碰撞 geom。关节机器人仍然可以是一个实体,但内部会有多个 link 和 joint。L04 将 详细解释关节结构、关节状态和自由度;L02 只建立容器层级的基本认识。

不要只相信示意图,应当用 API 检查实际对象:

python
print(type(box).__name__)
print(box.n_links, len(box.links))
print(box.n_geoms, len(box.geoms))

对于本课程锁定的 Genesis 1.3.3 和这里使用的 primitive,预期结果是一个 link 和一个碰撞 geom。这个结果只描述当前 primitive,不能推广成所有 Entity 或 所有导入资产的通用基数。

视觉几何体和碰撞几何体也不一定相同。导入机器人和物体资产后,这个区别会很 重要;本讲先不展开视觉 geom,以免偏离生命周期主线。

Morph、Material 与 Surface ​

scene.add_entity(...) 会组合三类配置。把它们区分清楚,代码审查和故障诊断 都会容易很多。

输入在 L02 中的职责示例常见混淆
Morph形状、尺寸、初始位姿,以及 fixed 等导入标志gs.morphs.Box(size=..., pos=...)Morph 是创建描述,不是运行时 Entity
Material物理模型和物理参数gs.materials.Rigid(rho=...)这里的“材质”不是物体显示出来的颜色
Surface颜色、纹理等渲染外观gs.surfaces.Default(color=...)红色物体不会因此自动变得更滑或更涩

Morph 描述创建什么 ​

Morph(形态描述)告诉 Genesis 要创建什么几何形状,以及把它放在什么初始 位置。内置 primitive Morph 包括 Plane、Box 和 Sphere;基于文件的 Morph 还可以描述 mesh 和关节资产,后续课程会使用这些形式。

有些决定对象如何进入仿真的标志也属于 Morph。例如,fixed=True 会把 primitive 固定住,而不是允许它自由运动。但这不会让 Morph 变成运行时状态; add_entity 返回的 Entity 才是提供运行时方法的对象。

Material 决定物理行为 ​

Material(物理材质)选择实体所采用的物理模型。本讲中, gs.materials.Rigid(...) 选择刚体求解路径,并提供密度。刚体 Material 还可以 携带摩擦等其他物理参数。

L02 固定使用一组参数,只为了让这个配置类别在代码中可见。本讲不会通过改变 密度或摩擦得出物理结论;L03 会在受控条件下改变物理参数并测量其影响。

Surface 决定外观 ​

Surface(表面外观)描述实体如何被渲染。gs.surfaces.Default(color=...) 中的 RGBA 元组只改变外观,不改变接触摩擦。如果希望“让蓝色方块变得更滑”,修改 颜色不是正确操作;对应的物理参数属于 Material。

这是一个重要的调试习惯:先判断问题属于几何、物理还是外观,再检查对应的 输入类别。

build() 边界 ​

从概念上看,build 做了什么 ​

只有在声明完本次运行需要的实体和相机后,才能调用 scene.build()。在锁定的 Genesis 1.3.3 实现中,build 会固定已经声明的结构、分配求解器状态、准备所选的 环境布局、编译所需的仿真 kernel、重置初始状态,并构建可视化组件,使场景进入 可执行状态。

这些实现细节解释了为什么第一次 build 可能比之后的单次 step 更耗时,但它们 不意味着课程代码应该依赖私有属性或精确的内部调用顺序。需要记住的公开约定很 简单:

  • build 前声明拓扑;
  • 只 build 一次;
  • build 后再进行步进、控制、状态读取和渲染。

“构建后拓扑固定”意味着什么、不意味着什么 ​

Genesis 1.3.3 要求调用 add_entity、add_camera 和 build 时 Scene 尚未构建。 Scene 构建完成后,就不能再通过这些声明接口改变该场景的拓扑。

这不代表 build 后所有数值都不可修改。运行时 API 仍然可以修改受支持的 状态、控制目标和相机位姿,后续课程正会这样做。准确的规则是:构建完成后, 不要再通过 build 前的声明 API 添加新的场景成员。

构建前的受控失败 ​

add_entity 返回后,实体 handle 已经存在,但其动态状态尚不可用。实验会明确 检验这条结论:

python
try:
    box.get_pos()
except gs.GenesisException as exc:
    if "not built yet" not in str(exc).lower():
        raise
    print("PASS — state is unavailable before build")
else:
    raise AssertionError("get_pos unexpectedly succeeded before scene.build()")

只有捕获到预期的 Genesis 异常,并且消息内容正确,这个测试才算通过。不同的 异常表示出现了意外故障,必须继续向外抛出。

构建后的受控失败 ​

与之相反的测试在 scene.build() 之后运行:

python
try:
    scene.add_entity(gs.morphs.Sphere(radius=0.05))
except gs.GenesisException as exc:
    if "already built" not in str(exc).lower():
        raise
    print("PASS — topology declaration is closed after build")
else:
    raise AssertionError("add_entity unexpectedly succeeded after scene.build()")

不要在这两个测试外层使用宽泛的 except Exception。否则拼写错误、导入问题或 无关的引擎故障,都可能被伪装成支持生命周期结论的证据。

step()、状态与证据 ​

构建完成后,每次 scene.step() 会把仿真向前推进一个外层时间步。设置 SimOptions(dt=0.01, substeps=2) 时,调用 20 次就表示推进 20 个外层时间步; substeps 是内部细分,不是额外的 Python 调用。L03 会解释这个区别为什么会影响 稳定性。

对于单个、未批处理的刚体实体,本讲使用的 getter 会返回以下 shape 的 PyTorch tensor:

Getter含义未批处理 shape
box.get_pos()base link 的位置(3,)
box.get_quat()base link 的朝向,顺序为 (w, x, y, z)(4,)
box.get_vel()base link 的线速度(3,)

返回 tensor 的 device 跟随运行时后端。在使用 ROCm 的 PyTorch 构建中,AMD 设备也可能通过 PyTorch 的 CUDA 兼容命名空间显示,因此应同时检查 Genesis 实际选择的后端和 tensor.device,不能只看到 cuda 一词就推断硬件平台。

每一份用作证据的状态至少要检查:

  1. Python 对象或 tensor 类型;
  2. 预期 shape;
  3. device;
  4. 所有数值是否有限;
  5. 观测到的变化是否符合定性预测。

L02 的 Box 从 Plane 上方开始运动。推进固定步数后,核心检查要求最终高度低于 初始高度,但不要求某个唯一的最终数值:最终接触位置等绝对数值可能受锁定的 引擎配置影响,而精确落点不是本讲要验证的概念。

图像可以帮助人理解场景,但不能代替状态检查;反过来,一个看似合理的状态 tensor 也不能证明相机图像已经正确渲染。每一条结论都应使用同类证据来支持。

后端选择与渲染能力 ​

下面两个经常被混为一谈的选择其实彼此独立:

  • 仿真后端负责执行物理计算和已编译的 kernel;
  • 渲染路径负责生成相机观测。

仿真后端模式 ​

实验提供两种后端模式:

  • auto 是面向学习者的默认模式。当前课程帮助函数优先选择已经验证的 AMD ROCm 路径;该路径可用时选择 gs.amdgpu,否则选择 gs.cpu;
  • cpu 显式选择 gs.cpu,既支持没有已验证加速器的学习者,也用于回归验证 本讲的 cpu-ok 最低能力声明。

在参考 AMD Radeon AI PRO R9700 机器上,auto 应当选择 gs.amdgpu,课程不会 强制这台机器改用 CPU。同时,cpu-ok 标签要求核心实验还要在另一个强制 CPU 的干净 kernel 中通过。

如果 auto 已经选择经过验证的 AMD 后端,之后初始化或执行失败,notebook 必须 暴露这个错误。静默改用 CPU 会隐藏兼容性回归。当前兼容性记录尚未验证 NVIDIA CUDA 和其他加速器,因此仅仅检测到设备并不足以声明这些路径受到支持。

渲染是独立开关 ​

相机必须在 build 前添加,因此 notebook 会在确定最终拓扑前决定是否加入相机:

python
camera = None
if render_enabled:
    camera = scene.add_camera(
        res=(640, 360),
        pos=(1.1, -1.1, 0.8),
        lookat=(0.0, 0.0, 0.25),
        fov=40,
        GUI=False,
    )

启用渲染时,相机创建、场景构建、渲染和 RGB 校验共同组成一项测试。渲染异常 必须让这项测试失败,不能触发静默降级。

如果运行前已经关闭渲染,notebook 会明确打印 SKIP: rendering disabled for this run,并根据实测的初始与最终位置绘制简单的 侧视示意图。这个图由状态数据生成,不是 Genesis 相机图像;它让 CPU 核心路径 在没有渲染时仍然有用,同时不会制造虚假的渲染声明。

选择 gs.cpu 本身不能证明渲染不可用,选择 gs.amdgpu 也不能证明渲染正常。 课程会分别验证这两项能力。

实验:一个方块,一次生命周期 ​

配套的可执行 notebook 已经实现下面的实验。运行第一个代码 cell 前,学习者先 预测 build 边界、哪些状态值会变化,以及没有相机时什么证据仍能说明物体发生了 运动;最后再根据对比图检查这些预测。

阶段 1——只初始化一次 ​

notebook 会在不修改 sys.path 的前提下导入已经安装的 robo_genesis 包,选择 auto 或强制 cpu 模式,报告实际后端,并且只调用一次 gs.init()。更换后端 或渲染模式后,需要从干净 kernel 重新运行整本 notebook。

阶段 2——声明场景 ​

场景包含:

  • 一个 Plane;
  • 一个有名称、可运动的 Box,显式配置 Morph、Material 和 Surface;
  • 只在运行前已启用渲染的情况下添加一个离屏相机。

此时 scene.is_built 必须为 False。可以检查 Box handle 及其声明结构,但读取 动态状态的 getter 必须拒绝构建前访问。

阶段 3——跨过一次构建边界 ​

notebook 只调用一次 scene.build(),并验证 scene.is_built 已变为 True。随后 检查 primitive 的层级结构,并读取第一份有效状态。

阶段 4——步进并比较 ​

notebook 先记录初始位置、四元数和线速度。启用渲染时,它还会在推进 20 步前 验证并保存初始相机画面;随后记录最终状态,检查 shape 和有限性,并验证方向性 预测 final_z < initial_z,而不是断言一个编造出来的绝对坐标。

阶段 5——如实观测 ​

启用渲染时,notebook 会验证初始和最终两份 RGB 数组,再把两张相机画面与各自 实测高度并排显示。关闭渲染时,则把基于状态绘制的 Box 初始/最终对比图与高度 轨迹并排显示,并明确标注这不是相机图像。两个分支之后都会用反思问题引导学习者 区分状态证据与渲染证据,并将实际结果和预测进行比较。

阶段 6——证明拓扑已经关闭 ​

最后,notebook 会尝试在 build 后调用一次 add_entity,并且只接受消息中包含 already built 的预期 GenesisException。

验收证据 ​

只有同时满足以下条件,核心实验才算通过:

  • 报告已安装的 Genesis 版本和实际后端;
  • 不使用源仓库、开发机绝对路径或 sys.path 注入;
  • scene.is_built 在一次 build 前后从 False 变为 True;
  • 构建前读取状态产生经过严格检查的预期失败;
  • 在锁定的 Genesis 1.3.3 下,当前 primitive 包含一个 link 和一个碰撞 geom;
  • position、quaternion 和 velocity 具有预期的未批处理 shape,且数值有限;
  • 20 步后 Box 的实际高度下降;
  • 构建后添加拓扑产生经过严格检查的预期失败;
  • 启用渲染时验证初始和最终 RGB 数据;关闭渲染时明确报告 SKIP,并且只生成 有清楚标注的状态对比图和轨迹图。

L02 已记录的验证在参考机的多个独立干净 kernel 中运行同一本 notebook:默认 auto 选择 gs.amdgpu,强制 cpu 选择 gs.cpu。启用渲染的参考运行提供额外 证据,但不会把 GPU 变成核心课程的先决条件。

常见失败与诊断 ​

Genesis hasn't been initialized ​

在 gs.init() 之前创建了 Scene 或其他 Genesis 对象。重启 kernel,只运行一次 初始化,然后按顺序执行后续 cell。

... is not built yet ​

在 scene.build() 之前调用了状态 getter 或 scene.step() 等运行时操作。找到 build 调用的位置,不要压制异常,也不要编造占位状态。

实验中唯一一次构建前 getter 是受控的反例测试,只应使用前文所示的严格异常 检查处理。

Scene is already built ​

代码在跨过拓扑边界后又请求添加实体、添加相机或第二次 build。把声明操作移到 唯一一次 build 之前,并从干净 kernel 重新运行。

实验中唯一一次 build 后 add_entity 同样是受控的反例测试,不是正常构建场景 的示例。

状态 shape 不符合预期 ​

先打印 getter 名称、类型、shape 和 device。本讲使用默认的未批处理场景,因此 不应出现额外的环境维度。L06 会有意引入批量环境,并解释相应的 shape。

状态包含非有限值或明显不合理 ​

先确认初始化和 build 成功,再检查步进前的第一份状态,然后定位从哪一步开始 出现非有限值。不要只根据最后一张图像诊断问题;L03 会加入面向接触和稳定性的 物理诊断方法。

渲染失败 ​

先确认运行前是否启用了渲染。如果本来就关闭了渲染,没有相机图像属于明确的 SKIP;如果已经启用,则应保留真实异常和环境信息,不能宽泛捕获后把它重新 标成成功的降级路径。

在同一个 kernel 中重复运行 gs.init() ​

更换后端或渲染模式需要重新进行进程级初始化。应重启 kernel,再从头到尾运行 notebook,而不是在带有隐藏状态的环境中反复执行初始化 cell。

课堂检查与练习 ​

概念检查 ​

先不回看前文表格,尝试回答:

  1. scene.add_camera(...) 应当放在 scene.build() 的哪一侧?为什么?
  2. 如果希望让蓝色方块变得更滑,应当修改 Surface 颜色,还是 Material 配置?
  3. 为什么实体 handle 已经存在时,get_pos() 仍然可能无效?
  4. “构建后拓扑固定”是否意味着所有运行时状态都不能再变化?
  5. 如果 ROCm 系统中的 tensor 显示为 cuda:0,还需要查看什么证据,才能判断 实际使用的后端?

动手练习 ​

在 build 调用前添加一个固定标记:

python
marker = scene.add_entity(
    morph=gs.morphs.Box(
        size=(0.06, 0.06, 0.06),
        pos=(0.18, 0.0, 0.03),
        fixed=True,
    ),
    surface=gs.surfaces.Default(color=(0.20, 0.80, 0.35, 1.0)),
    name="fixed_marker",
)

然后重启 kernel 并重新运行整本 notebook。通过对象检查验证这个 marker;启用 渲染时,还要在图像中确认它可见。解释哪些参数属于 Morph,哪些属于 Surface。

作为代码阅读扩展,打开 src/robo_genesis/build_scene.py,找出 Scene 创建、实体 声明、相机声明、build 调用和 build 后配置。现在不要运行完整抓取场景;这个练习 只要求识别 L07 将会使用的生命周期模式。

小结与 L03 衔接 ​

  • Scene 描述在 scene.build() 时转化为可执行的运行时状态;
  • 实体和相机在 build 前声明,步进、状态读取和渲染在 build 后进行;
  • 刚体 primitive 呈现 RigidEntity → RigidLink → RigidGeom 层级;
  • Morph、Material 和 Surface 分别描述几何与初始位姿、物理行为和渲染外观;
  • 状态的 shape、device、有限性和变化,比“没有报错”或一张看似合理的图像更能 支持调试结论;
  • cpu-ok 保证 CPU fallback,但不会强制已经验证的 AMD 机器放弃 gs.amdgpu;
  • 仿真后端与渲染能力需要分别验证。

L02 固定了物理配置,使生命周期成为唯一被考察的主题。L03 将主动改变时间步长、 substeps、接触和摩擦,并测量这些选择如何影响刚体仿真的稳定性。

资料来源 ​