查看器交互与插件#
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() 之后,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() 注册任何模式切换或退出按键,使它们与默认项一起显示在说明叠加层中。
另请参阅#
交互式 GUI 与调试:ImGui 叠加层、关节滑块和仿真控制。
渲染:相机、渲染以及录制视频。