查看器交互与插件#

Genesis World 的查看器是显示仿真运行过程的交互式窗口。除了内置的相机、录制和可视化控件,你还可以用两种方式扩展它:注册按键绑定,把按键绑定到回调;添加查看器插件,接收鼠标和键盘事件、每帧绘制调试几何体,并在每个仿真步运行逻辑。插件适合构建交互式工具,例如在网格上拾取点,或用鼠标拖拽刚体。

本页假设场景以 show_viewer=True 构建(见 你好,Genesis World)。可运行的示例位于 examples/viewer_plugin/。

最小可运行示例#

这个场景添加了内置的 MouseInteractionPlugin,因此你可以点击并拖拽盒子,并把 Esc 绑定为退出。顺序很重要:插件要在 build() 之前添加,按键绑定则在其后注册。

import genesis as gs
from genesis.vis.keybindings import Key, KeyAction, Keybind

gs.init(backend=gs.cpu)

scene = gs.Scene(show_viewer=True)
scene.add_entity(gs.morphs.Plane())
scene.add_entity(gs.morphs.Box(pos=(0, 0, 0.5), size=(0.2, 0.2, 0.2)))

# Plugins attach to the viewer at build time, so register them first.
scene.viewer.add_plugin(gs.vis.viewer_plugins.MouseInteractionPlugin())

scene.build()

is_running = True

def stop():
    global is_running
    is_running = False

# register_keybinds requires a built scene; call it after build().
scene.viewer.register_keybinds(
    Keybind("quit", Key.ESCAPE, KeyAction.RELEASE, callback=stop),
)

while is_running:
    scene.step()

按键绑定#

按键绑定把按键(可选带修饰键)映射到回调。通过 scene.viewer.register_keybinds() 注册一个或多个,传入 genesis.vis.keybindings.Keybind 对象:

from genesis.vis.keybindings import Key, KeyAction, Keybind, KeyMod

scene.viewer.register_keybinds(
    Keybind("greeting", Key.G, KeyAction.PRESS, callback=lambda: print("Hello!")),
    Keybind("quit", Key.ESCAPE, KeyAction.RELEASE, callback=stop),
)

register_keybinds 需要已构建的场景,因此在 scene.build() 之前调用会抛出异常。

Keybind 接收唯一的 name、来自 Key 枚举的 key,以及 key_action:

  • KeyAction.PRESS 在按键按下时触发一次。

  • KeyAction.HOLD 在按键被按住期间重复触发。

  • KeyAction.RELEASE 在按键释放时触发一次。

传入 key_mods=(KeyMod.CTRL,) 可要求修饰键,args / kwargs 用于向回调转发参数。名称必须唯一;对同一按键复用相同动作会抛出异常,除非传入 overwrite=True。

查看器叠加层列出键盘操作说明,其中包含自定义按键绑定

已注册的按键绑定会出现在查看器的说明叠加层中,用户无需阅读代码即可发现这些控件。若要隐藏该叠加层,或去掉默认的相机与录制快捷键,请在创建场景时设置相应的 ViewerOptions 字段:

scene = gs.Scene(
    viewer_options=gs.options.ViewerOptions(
        enable_help_text=False,         # hide the instructions overlay
        enable_default_keybinds=False,  # drop the built-in viewer shortcuts
    ),
    show_viewer=True,
)

查看器插件#

查看器插件是查看器在事件发生时回调的对象。查看器把窗口事件(鼠标、键盘、窗口大小变化)分发给每个插件,每个仿真步调用它一次,每个渲染帧也调用一次。这样插件就能维护自身状态、响应输入并绘制调试几何体,而无需直接接触渲染循环。

在 scene.build() 之前用 scene.viewer.add_plugin(plugin) 添加插件;场景构建完成后,查看器会调用插件的 build() 钩子。内置的 MouseInteractionPlugin 开箱即用;更具体的功能可以通过继承 ViewerPlugin 来编写。

Note

ImGui 叠加层插件(实体浏览器、关节滑块和仿真控制)通过 ViewerOptions(enable_gui=True) 启用,文档见 交互式 GUI 与调试 页面。

事件处理与交互模型#

每个事件钩子返回两种值之一:EVENT_HANDLED(True 的别名)表示消费该事件,或 None 表示继续向下传递。当插件消费了某个事件,后续插件和查看器的默认控件就看不到它。插件正是靠这种方式把左键点击等事件收归己用,同时把滚轮留给相机。对于你不关心的事件,返回 None 即可。

继承 genesis.vis.viewer_plugins.ViewerPlugin,只重写需要的钩子:

方法

调用时机

build(viewer, camera, scene)

场景构建后调用一次。重写它以保存引用并初始化状态;请先调用 super().build(...)。

on_mouse_motion(x, y, dx, dy)

鼠标移动。

on_mouse_drag(x, y, dx, dy, buttons, modifiers)

按住鼠标按键移动。

on_mouse_press(x, y, button, modifiers)

鼠标按键按下。

on_mouse_release(x, y, button, modifiers)

鼠标按键释放。

on_mouse_scroll(x, y, dx, dy)

滚轮滚动。

on_key_press(symbol, modifiers)

按键按下。

on_key_release(symbol, modifiers)

按键释放。

on_resize(width, height)

窗口大小改变。

update_on_sim_step()

每次 scene.step()。

on_draw()

每个渲染帧;在此发出调试绘制调用。

on_close()

查看器关闭时调用一次。

build() 之后,self.viewer、self.camera 和 self.scene 都已设置好,因此钩子可以读取实体状态,或向场景中绘制:

from genesis.vis.viewer_plugins import ViewerPlugin, EVENT_HANDLED, EVENT_HANDLE_STATE

class MyPlugin(ViewerPlugin):
    def on_key_press(self, symbol: int, modifiers: int) -> EVENT_HANDLE_STATE:
        if symbol == ord("x"):
            # ... handle the key ...
            return EVENT_HANDLED  # consume it; don't let the viewer also act on "x"
        return None

    def on_draw(self) -> None:
        self.scene.clear_debug_objects()  # debug geometry persists, so clear last frame's first
        self.scene.draw_debug_sphere((0.0, 0.0, 1.0), radius=0.05)

拖拽刚体:MouseInteractionPlugin#

gs.vis.viewer_plugins.MouseInteractionPlugin 让你可以点击并拖拽刚体。默认情况下,它用弹簧力把刚体移向光标(use_force=True);传入 use_force=False 则改为直接设置刚体位置。它还接受下文所述的 use_visual_geom 选项。完整脚本(包括 --use-force 和 --use-visual-geom 命令行标志以及多环境构建)见 examples/viewer_plugin/mouse_interaction.py:

scene.viewer.add_plugin(
    gs.vis.viewer_plugins.MouseInteractionPlugin(
        use_force=True,      # spring force; False sets position directly
        spring_slack=0.02,   # distance in m the body hangs below the cursor; only used when use_force=True
        color=(0.1, 0.6, 0.8, 0.6),
    )
)

屏幕到世界的射线:RaycasterViewerPlugin#

点击选择和 3D 拖拽都需要一条从相机穿过光标的射线。请改为继承 RaycasterViewerPlugin 而不是 ViewerPlugin:它在场景的刚体碰撞几何体上构建射线投射器,并为你提供 _screen_position_to_ray(x, y),返回一个 Ray(世界坐标系中的原点和方向)。它还重写了 update_on_sim_step(),在刚体移动时保持射线投射器同步,因此仿真运行期间命中结果始终准确。

传入 use_visual_geom=True 可改为对视觉网格投射,这样拾取会跟随屏幕上实际绘制的样子,并能命中只含视觉几何体的实体。代价是每个步都要重新扫描所有视觉三角形,在几何体精细时开销是碰撞网格的数倍。只有材质设置了 use_visual_raycasting=True 的实体才会对该投射可见,这与 射线投射传感器 采用的选择加入方式相同。

用 self._raycaster.cast(*ray) 投射射线。命中结果是一个 RayHit,包含 distance、position、normal(均在世界坐标系中)以及 geom——被击中的 RigidGeom,或在 use_visual_geom=True 时的 RigidVisGeom;geom.link 是所属连杆:

from genesis.vis.viewer_plugins import RaycasterViewerPlugin, EVENT_HANDLED

class PickerPlugin(RaycasterViewerPlugin):
    def on_mouse_press(self, x: int, y: int, button: int, modifiers: int):
        if button != 1:  # left button only
            return None
        ray = self._screen_position_to_ray(x, y)
        hit = self._raycaster.cast(*ray)
        if hit is not None and hit.geom is not None:
            link = hit.geom.link
            world_pos = hit.position  # shape (3,), world frame
            # ... use the hit ...
            return EVENT_HANDLED
        return None

用这种方式构建的完整插件见 examples/viewer_plugin/mesh_point_selector.py。它的 MeshPointSelectorPlugin(在该脚本中定义,不属于公开 API)在每次点击时做射线投射,以选择刚体网格上的点,在 on_draw() 中把它们绘制成球体,并在关闭时以连杆局部坐标写入 CSV。最后这一步在给实体放置传感器时很有用,可用来查找局部位置。它把每次世界空间命中转换到连杆坐标系,因此选中的点会随刚体移动而跟踪:

# In on_mouse_press, after a hit:
link_pos = tensor_to_array(link.get_pos())    # world position of the link
link_quat = tensor_to_array(link.get_quat())  # (w, x, y, z), scalar-first
local_pos = gu.inv_transform_by_trans_quat(world_pos, link_pos, link_quat)

在每一帧运行的 on_draw() 中绘制调试几何体。在开头调用 self.scene.clear_debug_objects() 并重新发出绘制,避免过时的几何体累积。draw_debug_sphere、draw_debug_spheres 和 draw_debug_arrow 是常用的图元。用 scene.viewer.register_keybinds() 注册任何模式切换或退出按键,使它们与默认项一起显示在说明叠加层中。

另请参阅#