Init Repo

This commit is contained in:
2026-06-03 09:00:11 +08:00
commit 9be48d8b9e
155 changed files with 14827 additions and 0 deletions
@@ -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)