## 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.