Skip to content

Convert all eight showcases to real-time display - #198

Merged
KraHsu merged 13 commits into
mainfrom
dev/showcase-realtime-revamp
May 31, 2026
Merged

KraHsu merged 13 commits into
mainfrom
dev/showcase-realtime-revamp

Conversation

@KraHsu

@KraHsu KraHsu commented May 31, 2026

Copy link
Copy Markdown
Owner

What

Convert all eight bundled genelab_showcase examples from silent disk-dumping to real-time, on-screen display. Every showcase now visualises live (smooth Qt / pyqtgraph) instead of writing files the user didn't ask for and couldn't see.

Global direction applied to all eight:

  • No silent disk dumps — real-time display by default; any file output is opt-in and documented.
  • Prefer Qt/pyqtgraph for smooth live plots, point clouds and images.
  • A shared LazyQtWindows helper and a virtual-spring base class (gravity comp + uprighting + pinning) keep the runners consistent.

The eight showcases

# Showcase Real-time view
1 Sensors Qt camera RGB/depth + sensor curves + view frustum
2 RayCast MeshRayCastSensor hit point cloud (GL scatter)
3 Contact marching G1 + air-time curve + contact viz + virtual spring
4 Terrain spring-toured G1 across five sub-terrains + scan point cloud / profile
5 Curriculum 5×5 difficulty grid (flat→slope) + per-robot level markers + live histogram
6 Actuator joint-1 tracking curves + live kp/kv/amplitude/period sliders
7 MlpResidual two arms side by side (with vs without residual), overlaid curves
8 Recording two live PyQt plots; file dumps opt-in; spurious recorder warning silenced

Library improvements (reused beyond the showcases)

  • MeshRayCastSensor — Genesis-backed BVH mesh ray-caster (coexists with the analytic terrain raycaster); RigidObjectCfg.use_visual_raycasting.
  • MPLImagePlotCfg — live-image output in the recording layer.
  • Recorder bridge filters Genesis's repeated start_thread(): Processor thread already exists warning around save_on_reset flushes (benefits any threaded-recorder env).

Notable fixes found along the way

  • Actuator "moves once then sticks": the Python force-channel PD was buzzing (under-damped) at the coarse dt=0.01; stabilised with dt=0.005 / decimation 4 / arm damping 40.
  • Multi-robot data staleness: env.step only refreshes the primary articulation, so the two-arm runner refreshes every robot it reads.
  • Curriculum terrain: five RandomRoughCfg rows collapse to one shape (Genesis keys terrain params by type) — used five distinct terrain types for a visible difficulty gradient; markers drawn at root_pos + envs_offset to match the multi-env render.

Testing

Full suite: 659 passed (headless, 0 failures). Each showcase was visually reviewed and passed a 3-agent quality review before commit.

🤖 Generated with Claude Code

KraHsu added 13 commits May 31, 2026 17:06
… runner

ShowcaseRunner gains the shared infrastructure the realtime showcase pass needs:

- A SpringCfg "marionette" that suspends a robot link via Genesis solver force/torque
  (gravity feed-forward + PD hold + uprighting), so passive humanoids stay standing.
- Real-time pacing of the play loop (gated on the viewer) so motion reads naturally.
- A _post_step hook for per-step debug drawing (camera frustum, ray casts, ...).
- A guard so the final dump is skipped once the viewer closes (a camera-backed dump
  would otherwise raise "Viewer already closed").
Genesis ships a live Matplotlib image viewer (MPLImagePlot) but the recording layer
did not expose it. Add MPLImagePlotCfg so a CameraSensor source can stream its rgb /
depth frame to a live image window (the image counterpart to the line plotters), wire
it through register/resolve, and require a camera source for it.

Also rename the ambiguous _PLOTTER_CFGS to _LINE_PLOT_CFGS (image plots are plotters
too), only enforce the camera-source rule for sensor-name sources (not callables), and
drop the redundant `from __future__ import annotations` in this package (the forward
refs are already quoted). Docs and tests updated.
GeneLab's analytic RayCastSensor only intersects the ground plane / heightfield (the
fast path for terrain height-scan). MeshRayCastSensor wraps Genesis's native Raycaster
(BVH ray-cast against real meshes) so rays hit arbitrary rigid bodies. The two coexist
with a clear division of responsibility.

The sensor follows the CameraSensor pattern: it registers the Genesis raycaster in
pre_build_genesis and reads it in _compute_data, flattening the pattern's ray grid to a
uniform (num_envs, num_rays, ...) payload. Thin MeshGridPattern / MeshSphericalPattern
specs mirror Genesis's patterns and defer construction to build time (Genesis patterns
need gs.init()); genesis stays a lazy import. RigidObjectCfg gains use_visual_raycasting
to opt a body into the visual BVH. Public surface (genelab.lab / sensor __all__),
snapshot test, and sensor docs updated.
Replace the per-step disk dumps (PNG frames + frame.log) with live windows: the IMU,
frame-transformer, and joint force/torque sensors stream to live PyQt curves declared
as scene recordings; the wrist camera renders RGB + depth into a pyqtgraph image window
(Qt, smooth) and its view frustum is drawn in the main viewer. Adds table props so the
camera sees real geometry, and fixes the camera offset_quat so it looks along the
gripper-forward axis (it was 90 degrees off). Nothing is written to disk.
Swap the analytic RayCastSensor (which only hit the ground) for MeshRayCastSensor so the
rays hit real box / sphere props placed in front of the robot, scanning along the
gripper-forward axis as joint 1 sweeps. The hit point cloud renders live in a dedicated
GPU pyqtgraph 3D-scatter window (one colour per sensor); the old distances.log dump is
gone. Nothing is written to disk.
The README listed six tasks; the package registers eight. Add the MlpResidual-Actuator
and Recording showcases, fix the Sensors entry (ForceTorqueSensor), and refresh the
layout tree.
Replace the static G1 + air_time.log dump with an in-place march: a virtual pelvis spring
(SpringCfg) holds the passive robot upright while the runner scripts an alternating high-knee
step (hip + knee + ankle flexion per side). Each foot's air-time streams to a live PyQt plot,
and a contact-state sphere is drawn under each foot (green = planted, red = airborne, radius ~
contact force). Nothing is written to disk.
Replace the dropped G1 + terrain.log dump with a guided tour: a virtual pelvis spring holds
the G1 upright while the runner teleports it cell to cell across the 1x5 sub-terrain row
(flat -> stairs -> rough -> slope -> wave), re-anchoring the spring at each cell. The main
viewer follows the robot. A downward pelvis ray-cast scans the surface; the runner draws the
rays in the scene and renders the hit point cloud (pyqtgraph GL, height-coloured) plus a live
height cross-section. The scan grid is densified to 11x11. Nothing is written to disk.
The raycast, sensors, and terrain runners each carried the same lazy-init pyqtgraph window
boilerplate (mkQApp, one-time build, best-effort exception fallback, processEvents). Hoist it
into genelab_showcase._viz.LazyQtWindows: runners now pass a build callback to ``ensure(...)``
and pump events via ``process()``. Behaviour is unchanged; the next showcase that needs a
window reuses the helper instead of copying the guard logic.
…level histogram

Replace the disk-dumping curriculum demo with a real-time scene: 25 passive
Unitree-G1 robots tile a 5x5 grid, one per cell, held upright by the virtual
spring (vertical + uprighting) plus a kinematic planar pin. Each grid row is a
distinct, escalating terrain type -- flat, wave, random-rough, pyramid stairs,
slope -- so the difficulty gradient reads from across the scene. Five
RandomRoughCfg rows would have collapsed to one shape because Genesis keys
terrain parameters by type, so distinct types are required.

The runner evolves a per-robot logical curriculum level (promote/demote),
floats a head marker coloured by level over each robot, and renders the live
level distribution as a pyqtgraph histogram. Markers are drawn at
root_pos + envs_offset because the multi-env render offset, not the local
per-env root position, is where the viewer shows each robot. Nothing is
written to disk.
…ve sliders

Replace the disk-dumping actuator showcases with real-time pyqtgraph windows and
fix the "moves once then sticks" behaviour. The stick was the Python force-channel
PD buzzing (under-damped) at the coarse 0.01 dt: at the finer 0.005 dt (decimation 4,
50 Hz control) with arm damping 40 the reported velocity matches the finite-difference
velocity, so joint 1 genuinely tracks the sine instead of jittering.

Both showcases now drive joint 1 with an accumulated-phase sine and plot the commanded
target, the achieved position and the tracking error live, with four sliders (kp, kv,
amplitude, period) that write straight into the actuator PD gains and reshape the sine.

The MlpResidual showcase becomes a side-by-side comparison: two Frankas via the env's
`robots` dict -- `robot` (MlpResidual) and `robot_ideal` (plain IdealPD) -- driven from
the same scripted target through separate action terms, their joint-1 responses overlaid.
env.step only refreshes the primary articulation's data cache, so the runner refreshes
every robot it reads (the non-primary one otherwise reads stale zeros). The residual net
is retuned to a position-error compensator (80 * pos_error) so the residual visibly
tightens tracking, more so as the kp slider is lowered. Nothing is written to disk.
…recorder warning

Default the Recording showcase to live plots only (both data sources now drive PyQt
plots): it writes nothing to disk unless GENELAB_SHOWCASE_RECORD_FILES is set, in which
case the lin-acc CSV and a per-episode orientation NPZ are added. This stops the default
run from silently filling the disk; the module docstring documents the switch.

Also silence Genesis's repeated "start_thread(): Processor thread already exists"
warning. Its RecorderManager.reset() restarts the still-running processor thread on
every save_on_reset flush, logging the warning each episode; the bridge now filters
that one message on the Genesis logger just around its reset() call. This benefits any
env with threaded save_on_reset recorders, not only the showcase.
@KraHsu
KraHsu merged commit c2e252a into main May 31, 2026
4 checks passed
@KraHsu
KraHsu deleted the dev/showcase-realtime-revamp branch May 31, 2026 13:41
This was referenced May 31, 2026
KraHsu added a commit that referenced this pull request Jun 5, 2026
Convert all eight showcases to real-time display
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant