Init Repo
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-05-25
|
||||
@@ -0,0 +1,89 @@
|
||||
## Context
|
||||
|
||||
The renderer (`UsdSceneRenderer`) already has a working pattern for GL line overlays drawn into the FBO: a shared GLSL program (`#version 130`, `in vec3 position; uniform mat4 mvpMatrix; uniform vec4 color`) is compiled once and used by both `DrawAxis` and `DrawBoundingBoxes`. The FBO can be re-bound at any time via `BindDrawTarget()`/`UnbindDrawTarget()`. The `TransformManipulator` already selects and edits any `Xformable` prim — no changes to it are needed.
|
||||
|
||||
`UsdGeomCamera` inherits from `UsdGeomXformable`. Every camera prim carries lens data (`focalLength`, `horizontalAperture`, `verticalAperture`, `clippingRange`) and an `Xformable` world transform. `GfFrustum::ComputeCorners()` returns the 8 world-space corners of a frustum given a `GfCamera`.
|
||||
|
||||
`ViewportPanel::Render` already has two FBO overlay call sites (`DrawAxis`, `DrawBoundingBoxes`) and two ImGui DrawList overlay call sites (gizmo, selection rect). The camera wireframe follows the FBO overlay pattern.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- Frustum wireframe drawn in 3D world space inside the FBO for every `UsdGeomCamera` prim present in the current stage at time `UsdTimeCode::Default()`.
|
||||
- Three visual states: inactive (muted grey), active/viewport-driving (cyan), selected (yellow/orange matching selection accent).
|
||||
- Click in the viewport selects a camera prim when the mouse is within a pick threshold of a projected frustum segment (tested before the geometry pick).
|
||||
- Selected camera integrates with the existing `TransformManipulator` (translate/rotate).
|
||||
|
||||
**Non-Goals:**
|
||||
- No near/far clip plane rectangle beyond a fixed display depth (far clip could be millions of units; showing a visualisation cap is acceptable).
|
||||
- No billboard label/text annotation.
|
||||
- No custom manipulator handles specific to camera FOV adjustment.
|
||||
- No frustum change when dragging the manipulator (camera shape updates next frame via normal USD dirty propagation).
|
||||
|
||||
## Decisions
|
||||
|
||||
### D1 — GL overlay into FBO, not ImGui DrawList
|
||||
|
||||
**Decision**: Draw camera wireframes using the existing GLSL line program and `BindDrawTarget`/`UnbindDrawTarget`, the same as `DrawAxis` and `DrawBoundingBoxes`.
|
||||
|
||||
**Rationale**: 3D GL lines drawn into the FBO participate in the depth buffer and are occluded by geometry correctly. ImGui screen-space lines would always appear on top of everything. The existing infrastructure (VAO, VBO, GLSL program, FBO helpers) already supports this pattern at zero additional complexity.
|
||||
|
||||
---
|
||||
|
||||
### D2 — Frustum geometry: near-plane pyramid + body box
|
||||
|
||||
**Decision**: The camera wireframe consists of two parts:
|
||||
1. **Frustum pyramid**: the camera apex (position) connected by 4 lines to the 4 corners of a display near-plane quad. The near clip value from the `UsdGeomCamera` is used but clamped to a minimum of `0.01` to keep it visible.
|
||||
2. **Body box**: a small fixed-size world-space box (side ≈ `frustumScale * 0.15`) centred at the camera position, representing the camera body. This gives a tangible handle at the camera origin that is easy to click.
|
||||
3. **Up arrow**: a short line from the body box top face indicating the camera's up direction, matching the camera local Y axis.
|
||||
|
||||
The far frustum rectangle is **not drawn** — far clip distances are often enormous and would produce confusing off-screen lines.
|
||||
|
||||
**Rationale**: Matches Houdini/Blender's standard camera shape. The body box ensures a reliable click target even at distance.
|
||||
|
||||
---
|
||||
|
||||
### D3 — Frustum scale proportional to camera distance
|
||||
|
||||
**Decision**: The overall wireframe scale is `dist * 0.12` where `dist` is the current viewport camera's look-at distance (from `ViewportCamera::GetDist()`). This matches the `screenFactor` approach used by the `TransformManipulator`.
|
||||
|
||||
**Rationale**: A camera at distance 100 should not appear tiny compared to one at distance 5. Constant apparent size improves usability.
|
||||
|
||||
---
|
||||
|
||||
### D4 — Camera picking via screen-space segment proximity
|
||||
|
||||
**Decision**: `PickCameraAtPoint` iterates all `UsdGeomCamera` prims, projects each wireframe segment to screen space, and returns the prim path of the closest camera whose minimum segment distance to the mouse is `< kCameraPickRadius` (10 pixels). If multiple cameras qualify, the nearest in screen space wins.
|
||||
|
||||
**Rationale**: `UsdImagingGLEngine::TestIntersection` only hits rendered hydra geometry — cameras have no rendered prims in Storm. Custom 2D proximity testing is the only option. 10 px matches the manipulator's pick radius for consistency.
|
||||
|
||||
---
|
||||
|
||||
### D5 — Camera pick tested before geometry pick
|
||||
|
||||
**Decision**: In `ViewportPanel::HandleInput`, the single-click path calls `PickCameraAtPoint` first. If a camera path is returned, that prim is selected and the geometry pick is skipped. If no camera is hit, the existing `PickObject` path runs as before.
|
||||
|
||||
**Rationale**: Camera wireframes are thin and can overlap rendered geometry behind them. Giving camera wireframes priority matches Houdini/Maya behaviour where camera icons are always selectable.
|
||||
|
||||
---
|
||||
|
||||
### D6 — Active camera path passed through from ViewportPanel
|
||||
|
||||
**Decision**: `DrawCameraWireframes` and `PickCameraAtPoint` receive `m_selectedCameraPath` from `ViewportPanel` (the path of whichever USD camera is currently active in the toolbar, or `SdfPath()` for Free Camera). This allows the cyan highlight without coupling `UsdSceneRenderer` to `ViewportCamera`.
|
||||
|
||||
**Rationale**: `UsdSceneRenderer` should not hold a reference to `ViewportCamera`. The path is a cheap `SdfPath` copy per frame.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- **[Risk] Frustum scale is view-dependent** — when the viewport camera is inside a USD camera's frustum and zoomed in close, the scale might get very large.
|
||||
→ Mitigation: clamp `frustumScale` to a maximum of 50 world units.
|
||||
|
||||
- **[Risk] Performance** — iterating all `UsdGeomCamera` prims every frame could be slow for large scenes.
|
||||
→ Mitigation: cache the list of camera prim paths; invalidate on stage change via `SetForceRefresh`.
|
||||
|
||||
- **[Risk] Camera behind the viewer** — `WorldToScreen` returns false for behind-near-plane points; those line endpoints are simply skipped.
|
||||
→ Acceptable; only the visible portion of the wireframe is drawn.
|
||||
|
||||
## Migration Plan
|
||||
|
||||
Additive changes only — no existing API is removed or broken. `DrawCameraWireframes` and `PickCameraAtPoint` are new methods; their call sites in `ViewportPanel` are inside existing per-frame code blocks.
|
||||
@@ -0,0 +1,28 @@
|
||||
## Why
|
||||
|
||||
Camera prims exist in the USD scene but are invisible in the viewport — there is no visual indication of a camera's position, orientation, or field of view. Users cannot tell where cameras are in the scene, cannot click to select them, and cannot use the transform manipulator to reposition them. This is a critical gap for any scene with multiple cameras (rendering, animation, layout).
|
||||
|
||||
## What Changes
|
||||
|
||||
- Draw a 3D frustum wireframe for every `UsdGeomCamera` prim in the viewport, rendered into the FBO using the existing GL overlay infrastructure (`BindDrawTarget`/`UnbindDrawTarget`).
|
||||
- Highlight the **active camera** (the one currently driving the viewport via the camera toolbar) with a distinct colour; all other cameras use the standard camera wire colour.
|
||||
- Highlight the **selected camera** with the selection accent colour.
|
||||
- Support **viewport click-selection** of camera prims by screen-space hit-testing mouse clicks against projected frustum segments.
|
||||
- Camera prims, once selected, can be translated/rotated/scaled through the existing `TransformManipulator` (they are `Xformable`; no manipulator changes are required).
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `camera-frustum-overlay`: Per-frame GL wireframe rendering of all `UsdGeomCamera` prims' frustums into the viewport FBO, with colour-coding for active vs. selected vs. inactive cameras.
|
||||
- `camera-viewport-picking`: Screen-space hit-testing that maps a viewport mouse click to a camera prim when the click is within a threshold distance of any projected frustum edge; integrates with the existing prim-selection callbacks.
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
_(none — no existing spec-level requirements are changing)_
|
||||
|
||||
## Impact
|
||||
|
||||
- **Modified**: `src/core/UsdSceneRenderer.h/.cpp` — add `DrawCameraWireframes(stage, selectedPaths, activeCameraPath, viewProjMatrix)` and `PickCameraAtPoint(stage, mouseScreenPos, viewProjMatrix, imagePos, viewW, viewH, outPath)`.
|
||||
- **Modified**: `src/ui/ViewportPanel.cpp` — call `DrawCameraWireframes` each frame after `DrawBoundingBoxes`; call `PickCameraAtPoint` inside the single-click pick path before the existing `PickObject` geometry pick so camera hits take priority.
|
||||
- No new external dependencies. Uses existing GLSL shader program already compiled for `DrawAxis`/`DrawBoundingBoxes`.
|
||||
@@ -0,0 +1,42 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Camera frustum wireframe drawn every frame
|
||||
For every `UsdGeomCamera` prim present in the current USD stage, the system SHALL draw a 3D wireframe representation into the viewport FBO each frame. The wireframe SHALL be drawn at `UsdTimeCode::Default()`. The wireframe SHALL consist of: (a) a small body box centred at the camera origin, (b) four lines from the camera origin to the corners of a near-clip display quad, and (c) a short up-arrow line indicating the camera's local Y axis.
|
||||
|
||||
#### Scenario: Camera wireframe appears when camera prim exists
|
||||
- **WHEN** a `UsdGeomCamera` prim exists in the stage
|
||||
- **THEN** a frustum wireframe is visible in the viewport at the camera prim's world-space position and orientation
|
||||
|
||||
#### Scenario: No wireframe when stage has no cameras
|
||||
- **WHEN** no `UsdGeomCamera` prims exist in the stage
|
||||
- **THEN** no camera wireframe is drawn
|
||||
|
||||
### Requirement: Wireframe size scales with viewport camera distance
|
||||
The wireframe scale SHALL be proportional to the current viewport camera's look-at distance (`ViewportCamera::GetDist()`), clamped to a maximum of 50 world units, so that camera shapes maintain a consistent apparent screen size regardless of zoom level.
|
||||
|
||||
#### Scenario: Camera wireframe stays visible when zooming out
|
||||
- **WHEN** the viewport camera is zoomed out (increasing distance)
|
||||
- **THEN** the camera wireframe grows proportionally so it remains visible at roughly the same screen size
|
||||
|
||||
### Requirement: Three distinct visual states
|
||||
The system SHALL draw camera wireframes using three colour states:
|
||||
- **Inactive** (default): muted grey `(0.55, 0.55, 0.55, 0.85)`
|
||||
- **Active** (the camera currently driving the viewport via the camera toolbar): cyan `(0.2, 0.9, 1.0, 1.0)`
|
||||
- **Selected** (the camera prim is the current primary selection): yellow-orange `(1.0, 0.75, 0.1, 1.0)`
|
||||
|
||||
A camera that is both active and selected SHALL use the selected colour (selected takes priority).
|
||||
|
||||
#### Scenario: Active camera shown in cyan
|
||||
- **WHEN** a USD camera prim is set as the active viewport camera via the camera toolbar
|
||||
- **THEN** its wireframe is drawn in cyan
|
||||
|
||||
#### Scenario: Selected camera shown in yellow-orange
|
||||
- **WHEN** a camera prim is the primary selected prim in the scene hierarchy
|
||||
- **THEN** its wireframe is drawn in yellow-orange, overriding the cyan active colour
|
||||
|
||||
### Requirement: Camera list cached and refreshed on stage change
|
||||
The system SHALL cache the list of `UsdGeomCamera` prim paths to avoid re-traversing the stage every frame. The cache SHALL be invalidated whenever `SetForceRefresh(true)` is called or the stage pointer changes.
|
||||
|
||||
#### Scenario: Cache invalidated on stage reload
|
||||
- **WHEN** a new USD file is opened
|
||||
- **THEN** the camera wireframe list reflects the new stage's cameras in the next frame
|
||||
@@ -0,0 +1,27 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Camera prim selectable by clicking its wireframe
|
||||
The system SHALL test each viewport left-mouse-button click against all camera wireframe segments before running the existing geometry pick. If the click position is within 10 screen pixels of any projected wireframe edge, the corresponding `UsdGeomCamera` prim SHALL be selected. Among multiple qualifying cameras the nearest one (smallest minimum screen distance) SHALL be chosen.
|
||||
|
||||
#### Scenario: Clicking camera wireframe selects the camera prim
|
||||
- **WHEN** the user left-clicks within 10 pixels of a camera wireframe segment
|
||||
- **THEN** the camera prim appears selected in the Scene Hierarchy panel and the Property panel shows its attributes
|
||||
|
||||
#### Scenario: Camera pick takes priority over geometry behind it
|
||||
- **WHEN** the user clicks on a camera wireframe that visually overlaps rendered geometry
|
||||
- **THEN** the camera prim is selected, not the geometry behind it
|
||||
|
||||
#### Scenario: Click misses all cameras — falls through to geometry pick
|
||||
- **WHEN** the user left-clicks on an area with no camera wireframe within 10 pixels
|
||||
- **THEN** the normal geometry pick runs as before
|
||||
|
||||
### Requirement: Selected camera is editable with the transform manipulator
|
||||
After a camera prim is selected via viewport click, the system SHALL allow the `TransformManipulator` to translate and rotate it using the existing W/E/R tool modes. The camera SHALL implement `UsdGeomXformCommonAPI` for the manipulator to write to. No additional manipulator changes are required.
|
||||
|
||||
#### Scenario: Move manipulator repositions selected camera
|
||||
- **WHEN** a camera prim is selected and the Move tool (W) is active
|
||||
- **THEN** the translate gizmo appears at the camera's world position and dragging it moves the camera prim
|
||||
|
||||
#### Scenario: Rotate manipulator reorients selected camera
|
||||
- **WHEN** a camera prim is selected and the Rotate tool (E) is active
|
||||
- **THEN** the rotate rings appear at the camera's world position and dragging them rotates the camera prim
|
||||
@@ -0,0 +1,55 @@
|
||||
## 1. UsdSceneRenderer — DrawCameraWireframes
|
||||
|
||||
- [ ] 1.1 Add to `UsdSceneRenderer.h`: `void DrawCameraWireframes(pxr::UsdStageRefPtr stage, const pxr::SdfPathVector& selectedPaths, const pxr::SdfPath& activeCameraPath, const pxr::GfMatrix4d& viewProjMatrix, double viewportCameraDist)` and private helpers `BuildCameraWireframeLines(const pxr::GfCamera& gfCam, double scale, std::vector<float>& outVerts)` and `pxr::SdfPathVector m_cachedCameraPaths` + `bool m_cameraCacheDirty = true`
|
||||
- [ ] 1.2 In `UsdSceneRenderer.cpp` implement `DrawCameraWireframes`:
|
||||
- If `m_cameraCacheDirty`, traverse stage for `UsdGeomCamera` prims and cache paths; clear dirty flag
|
||||
- For each cached camera path, resolve world transform + lens params via `UsdGeomCamera::GetCamera(UsdTimeCode::Default())` → `GfCamera`
|
||||
- Compute `frustumScale = std::min(viewportCameraDist * 0.12, 50.0)`
|
||||
- Call `BuildCameraWireframeLines` to produce a `std::vector<float>` of XYZ vertex pairs (GL_LINES)
|
||||
- Determine colour: selected → `(1.0,0.75,0.1,1.0)`, active → `(0.2,0.9,1.0,1.0)`, inactive → `(0.55,0.55,0.55,0.85)`
|
||||
- `BindDrawTarget()`, draw all segments using the existing axis GLSL program + a dynamic VBO (same pattern as `DrawBoundingBoxes`), `UnbindDrawTarget()`
|
||||
- [ ] 1.3 Implement `BuildCameraWireframeLines`: given `GfCamera` and scale, produce:
|
||||
- **Body box** — 12 edges of a small box (width × height × depth = scale×0.15 each) centred at camera origin in local space, transformed to world space by the camera's transform matrix
|
||||
- **Frustum pyramid** — 4 lines from camera origin to each corner of a near-display quad at depth `max(nearClip, 0.01)`, scaled so the quad width/height match `horizontalAperture/focalLength * nearDepth` (perspective divide)
|
||||
- **Up arrow** — one line from body-box top-centre upward by `scale * 0.2` along camera local Y
|
||||
- [ ] 1.4 Set `m_cameraCacheDirty = true` inside `SetStage` (already exists) and inside the `SetForceRefresh(true)` path
|
||||
|
||||
## 2. UsdSceneRenderer — PickCameraAtPoint
|
||||
|
||||
- [ ] 2.1 Add to `UsdSceneRenderer.h`: `bool PickCameraAtPoint(pxr::UsdStageRefPtr stage, const ImVec2& mousePosAbsolute, const pxr::GfMatrix4d& viewProjMatrix, const ImVec2& imagePos, int viewW, int viewH, double viewportCameraDist, pxr::SdfPath* outCameraPath)`
|
||||
- [ ] 2.2 In `UsdSceneRenderer.cpp` implement `PickCameraAtPoint`:
|
||||
- Iterate `m_cachedCameraPaths` (build cache if needed)
|
||||
- For each camera: build wireframe vertex list via `BuildCameraWireframeLines`
|
||||
- Project each vertex pair to screen space using the same `WorldToScreen` math as `TransformManipulator`
|
||||
- Compute `PointToSegmentDist` for each segment against `mousePosAbsolute`
|
||||
- Track minimum distance and corresponding path; if `< kCameraPickRadius (10 px)` return true with that path
|
||||
- If tie between multiple cameras, return the one with the smallest minimum distance
|
||||
|
||||
## 3. ViewportPanel — Integration
|
||||
|
||||
- [ ] 3.1 In `ViewportPanel.h`: add `pxr::SdfPath GetActiveCameraPath() const` (returns `m_camera.GetUsdCameraPath()` when in UsdCamera mode, else `SdfPath()`) — check `ViewportCamera` API for the getter name
|
||||
- [ ] 3.2 In `ViewportPanel.cpp` `Render()`: after the `DrawBoundingBoxes` call, add:
|
||||
```cpp
|
||||
m_renderer.DrawCameraWireframes(m_stage, m_selectedSdfPaths,
|
||||
m_camera.GetUsdCameraPath(), viewProj, m_camera.GetDist());
|
||||
```
|
||||
(Use the local `viewProj` already computed in that scope)
|
||||
- [ ] 3.3 In `ViewportPanel.cpp` `HandleInput()`, single-click path (before the existing `PickObject` call): add camera pick:
|
||||
```cpp
|
||||
pxr::SdfPath camPath;
|
||||
if (m_renderer.PickCameraAtPoint(m_stage, mouse, viewProj,
|
||||
m_imageScreenPos, m_viewWidth, m_viewHeight,
|
||||
m_camera.GetDist(), &camPath)) {
|
||||
// fire selection callbacks with camPath
|
||||
}
|
||||
```
|
||||
Only fall through to `PickObject` if `PickCameraAtPoint` returns false
|
||||
|
||||
## 4. Build and Verification
|
||||
|
||||
- [ ] 4.1 Run `cmake --preset default` (only needed if new `.cpp` files were added; all changes here are in existing files so this may be skipped) then `cmake --build build --config Release` — resolve any compilation errors
|
||||
- [ ] 4.2 Install and launch `install/bin/App.exe`; create a `Camera` prim via Stage menu, verify its frustum wireframe appears in the viewport at the world origin
|
||||
- [ ] 4.3 Verify clicking the camera wireframe selects the prim in the Scene Hierarchy panel
|
||||
- [ ] 4.4 Verify the Move (W) gizmo appears on the selected camera and dragging repositions it; the wireframe moves to match
|
||||
- [ ] 4.5 Switch the viewport to that camera via the camera toolbar; verify the wireframe turns cyan
|
||||
- [ ] 4.6 With the camera active in the toolbar, click it in the viewport; verify it turns yellow-orange (selected overrides active colour)
|
||||
Reference in New Issue
Block a user