7.6 KiB
Context
The app currently has a single ViewportPanel class that owns one ViewportCamera, one UsdSceneRenderer, and draws a full-window render with toolbars + overlays. Selection propagates through OnPrimPicked / OnPrimsPickedRect callbacks to SceneHierarchyPanel and PropertyPanel. The UsdSceneRenderer owns a GlfDrawTarget FBO; the render loop calls m_renderer.Render() then renders overlays (axis, bbox, gizmo, manipulator toolbar) into the same FBO or directly to the screen via ImGui draw lists.
The ViewportCamera and UsdSceneRenderer classes are already designed as independent units — they can be instantiated per-viewport without modification. The key architectural challenge is sharing the selection state across N viewport tiles while ensuring only the focused tile renders the transform gizmo.
Goals / Non-Goals
Goals:
- Support 1, 2 (horizontal/vertical split), and 4-quadrant viewport layouts
- Each viewport tile has its own camera, renderer, and per-viewport settings (grid, AA, bbox mode, background color, render delegate)
- Single shared selection: picking in any tile updates all tiles and propagates to panels
- Transform manipulator (gizmo) renders only in the focused tile
- Drag-based dividers to resize tiles within a layout
- Backward-compatible
ApplicationAPI:m_viewportPanelstays as the entry point - Space-key maximize/restore: pressing Space over the hovered tile temporarily fills the entire viewport area; pressing Space again restores the previous layout (Maya-style)
Non-Goals:
- Not adding tear-off/undockable viewport windows (ImGui docking handles that separately)
- Not adding per-viewport layer visibility overrides (future feature)
- Not supporting more than 4 tiles (can be extended later)
- Not adding viewport sync (playblasting) or timeline scrubbing across tiles
Decisions
-
Extract
ViewportTilefromViewportPanelrather than subclassing.- The existing 1100-line
ViewportPanel::Render()method contains everything: toolbar rendering, camera resolution, scene rendering, overlay drawing, context menu, and manipulator. Extracting aViewportTileclass that owns a camera + renderer + local per-viewport settings + the subset of overlay logic that is per-tile keeps the container clean. ViewportPanelbecomes the container: it ownsstd::vector<std::unique_ptr<ViewportTile>>, manages layout geometry, and delegatesRender()calls to each tile.- Alternative considered: having
ViewportPanelcontain N inline structs. This was rejected because tiles need their own camera/resolution/overlay lifecycle, which calls for a proper class.
- The existing 1100-line
-
Selection lives on the container (
ViewportPanel), not on any tile.- Each tile still calls
m_renderer.SetSelectedPaths()andm_renderer.AddSelected()so itsUsdImagingGLEnginehighlights the correct prims, but the authoritativem_selectedSdfPaths/m_selectedPrimPathvector lives onViewportPanel. - When a tile receives a pick hit, it calls back into the container to update the shared selection, and the container broadcasts it to all tiles and to the
OnPrimPicked/OnPrimsPickedRectcallbacks. - Alternative considered: each tile owns its selection and syncs via signals. Rejected because it adds complexity for a single-authority selection model.
- Each tile still calls
-
Manipulator ownership moves to
ViewportPanel(container), not per tile.- The
TransformManipulatoris a singleton — there should never be two gizmos active. The container owns it and only passes it to the focused tile for rendering. - The focused tile is the one that last received a
ImGui::IsWindowHovered()click. Tile tracksm_isFocusedset by container duringHandleInput().
- The
-
Layout state stored as simple enum + divider positions, no external dependency.
- Layout enum values:
Single,HSplit(2 tiles side-by-side),VSplit(2 tiles top-bottom),Quad(4 tiles). - Divider positions stored as normalized floats (
0.0–1.0). Dragging a divider updates the normalized position, thenViewportPanel::Render()recomputes tile rectangles. - No XML/JSON config for layout — state is ephemeral (could be persisted via future settings system).
- Alternative considered:
ImGui::Splitter()based approach from ImGui demos. We'll implement a lightweight version directly since we need precise control over tile rectangle subdivision.
- Layout enum values:
-
Render delegate can differ per tile.
- Each tile's
UsdSceneRendererindependently callsSetRendererPlugin(). This means each tile FBO can use e.g. Storm vs HdEmbree independently. - This is useful for comparing renderers side-by-side and is already supported by
UsdSceneRenderer's per-instance design.
- Each tile's
-
Space-key maximize stored as a simple toggle on the container, not on individual tiles.
ViewportPanelstoresm_maximizedTileIndex(-1 = not maximized) andm_layoutBeforeMaximize(theLayoutModeenum value before Space was pressed).- When Space is pressed (with tile hovered and
m_maximizedTileIndex == -1): savem_layoutBeforeMaximize, setm_maximizedTileIndexto the hovered tile, and render only that tile filling the entire viewport area (no dividers, no layout menu). - When Space is pressed again (or Escape): restore
m_maximizedTileIndexto -1 and re-render the saved layout. - Keyboard shortcuts (F, A, Q/W/E/R) continue to target the maximized tile as the de facto focused tile.
- Alternative considered: pushing a separate layout mode
Maximized. Rejected because it conflates layout state with a transient display mode; storing the pre-maximize layout explicitly is cleaner and avoids state machine complexity.
-
Per-tile settings stored on
ViewportTile, not on a settings struct singleton.- Grid/AA/bg-color/bbox-mode/render-delegate are all fields on
ViewportTile. The compact toolbar in each tile reads/writes these directly. - For layout persistence (future), tiles could serialize their settings.
- Grid/AA/bg-color/bbox-mode/render-delegate are all fields on
Risks / Trade-offs
| Risk | Mitigation |
|---|---|
| N renderers means N FBO draw targets, each at full viewport resolution. For a 4K monitor with Quad layout, each tile is ~960×540 → 4× FBO allocations at that size. GPU memory may spike. | Cap layout to 4 tiles. Add a m_renderScale per tile (default 1.0) if perf issues arise. OpenUSD's UsdImagingGLEngine already has renderParams for resolution scaling. |
| Input routing: multiple tiles competing for Alt+LMB, Alt+RMB, scroll, etc. | The container dispatches HandleInput() only to the hovered tile each frame. The focused tile's manipulator is the only one that can consume input. |
| Right-click context menu: which tile owns it? | The context menu appears on the hovered tile. The popup (ViewportContextMenuN) is opened per-tile using unique IDs. |
| Divider drag area is thin — hard to click on high-DPI. | Dividers are 6px wide with a 2px visual line in the centre. Cursor changes to resize cursor on hover. |
| Keyboard shortcuts (F=frame, A=frame all, Q/W/E/R=manipulator) need to target the focused tile. | Shortcuts in the container delegate to the focused tile's camera/manipulator. If no tile is focused (e.g. user just clicked a panel outside the viewport), shortcuts are ignored. |
| Space key might conflict with ImGui docking or text input. | Guard with !io.WantTextInput (same as existing F/A shortcuts). Space is not a default ImGui docking shortcut, so no conflict with docking. |
| User might accidentally press Space when no tile is hovered (e.g. focus is on a panel). | Space is a no-op when no tile is hovered and viewport is not maximized. If already maximized, Space always restores regardless of hover state. |