Skip to content

feat(viewer): animated orbit / trajectory GIF mode for the 3D scene view (closes #88) - #106

Open
dchaudhari7177 wants to merge 1 commit into
bamdadd:mainfrom
dchaudhari7177:feat/viewer-orbit-animation
Open

feat(viewer): animated orbit / trajectory GIF mode for the 3D scene view (closes #88)#106
dchaudhari7177 wants to merge 1 commit into
bamdadd:mainfrom
dchaudhari7177:feat/viewer-orbit-animation

Conversation

@dchaudhari7177

Copy link
Copy Markdown
Contributor

Closes #88.

The 3D scene viewer rendered a single static PNG, so coverage could only be judged from one fixed angle. --animate exports a GIF instead:

uv run --with matplotlib python scripts/view_scene_3d.py --animate --out scene.gif
uv run --with matplotlib python scripts/view_scene_3d.py --animate \
    --animate-mode trajectory --out walk.gif
  • orbit (default) — sweeps the viewpoint right round the scene, geometry fixed, so frustum overlap reads from any angle.
  • trajectory — fixed viewpoint, object walks its path as a growing polyline with the leading sample marked.

--frames and --frame-ms tune it. Verified headless (Agg) on this machine: 36-frame orbit GIF ≈ 750 KB in ~8 s; 15-frame trajectory GIF ≈ 82 KB.

Static path is provably unchanged

Rather than assert this, I checked it: the bundled MTMC fixture renders byte-identical before and after the refactor (md5 f151c33e3b15ce78989ecd16d6272727, 93,434 bytes), re-verified after ruff format.

render() is split into _draw_scene() (cameras, trajectories, ground, axes) plus a thin save step, so both paths share one drawing implementation instead of duplicating it. Trajectory bounds always span the full path even when the drawn polyline is truncated, so the view doesn't drift between frames.

GIFs use Pillow, matching record_multiview.py, so no imageio/ffmpeg encoder is required (worth noting: imageio isn't installed in this repo's venv, so the Pillow path is also the only one that works out of the box). matplotlib and PIL stay lazily imported inside the render functions.

Two bugs found while testing

  1. Animation frames must not use bbox_inches="tight". The tight box is recomputed per frame, and as the view rotates the artists' extent changes — frames came out 354–436 px wide for the bundled fixture. A GIF pastes every frame onto the first frame's canvas, so this showed up as jitter and clipped edges. Animation frames now render to the full fixed canvas (560×440); the static PNG keeps its tight box. test_orbit_frames_all_share_one_canvas_size pins it.
  2. Trajectory mode over-rendered. Asking for more frames than trajectory samples re-rendered figures that were pixel-identical (Pillow collapses them on write anyway). Now capped at the sample count.

Also: ax.legend() is only called when something is labelled — a camera-only manifest has no trajectory to list, and matplotlib emitted No artists with labels found rather than drawing nothing.

Tests

tests/test_view_scene_3d_animation.py — 7 headless tests: non-empty multi-frame GIF for both modes, uniform frame size, the frame cap, the static PNG path still emitting a PNG, rejected mode/frame-count, and a camera-only manifest (orbit works, trajectory raises a clear error). All skip cleanly without matplotlib/Pillow.

Kept in its own module rather than added to tests/test_view_scene_3d.py deliberately — see below.

Note on overlap with #105

My open PR #105 (issue #89, coverage colouring) also edits scripts/view_scene_3d.py, changing render()'s signature and the camera-drawing loop, and adds tests/test_view_scene_3d.py. These two will conflict textually. Both are off main and independent, and I put the animation tests in a separate module so at least the test files don't collide. Happy to rebase whichever you'd like to land second — just say which.

Local results

ruff check     All checks passed!
ruff format    2 files already formatted
mypy           Success: no issues found in 2 source files
pytest tests/test_view_scene_3d_animation.py    7 passed

Full-suite note: tests/test_overlay.py::test_importing_package_does_not_import_pillow fails, but it fails identically on pristine upstream/main (its "PIL" not in sys.modules check is order-dependent and something earlier in the run imports Pillow). Passes when that file is run alone. Left alone as out of scope — though note steerbench's equivalent check moved to a subprocess for exactly this reason, if you want the same fix here.

🤖 Generated with Claude Code

The 3D scene viewer rendered a single static PNG, so coverage could only be
judged from one fixed angle. --animate exports a GIF instead:

  --animate-mode orbit       sweep the viewpoint right round the scene
  --animate-mode trajectory  fixed viewpoint, object walks its path

The static-PNG default is untouched, and verified so: the bundled fixture
renders byte-identical (md5 f151c33e...) before and after the refactor.

render() is split into _draw_scene() (cameras, trajectories, ground, axes) plus
a thin save step, so both paths share one drawing implementation instead of
duplicating it. Trajectory bounds are always computed from the full path even
when the polyline is truncated, so the view does not drift between frames.

GIFs are written with Pillow, matching the animation path in
record_multiview.py, so no imageio/ffmpeg encoder is needed - matplotlib and
PIL stay lazily imported inside the render functions.

Two things found while testing:

- Animation frames must NOT use bbox_inches="tight". The tight box is
  recomputed per frame, and as the view rotates the artists' extent changes, so
  frames came out 354-436 px wide for the bundled fixture. A GIF pastes every
  frame onto the first frame's canvas, so that showed as jitter and clipped
  edges. Animation frames now use the full fixed canvas; the static PNG keeps
  its tight box. A test pins that all frames share one size.
- Trajectory mode caps frames at the trajectory sample count, since asking for
  more re-renders figures that are pixel-identical.

Also only calls ax.legend() when something is labelled: a camera-only manifest
has no trajectory to list, and matplotlib warned instead of drawing nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.

3D viewer: add an animated / orbiting mode (rotate + play trajectory)

2 participants