Files
UsdLayerManager/src/ui/ViewportTile.cpp
T
indigo 1c55907573 Add renderer purpose display toggles (Guide, Proxy, Render)
Exposes UsdGeomImageable purpose visibility as three per-viewport toggles
in the gear popup (Purpose: Guide / Proxy / Render), wired to
showGuides/showProxy/showRender on UsdImagingGLRenderParams. Defaults match
the USD render params defaults (Proxy on, Guide and Render off). Settings
persist per-viewport to viewport_settings.ini.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-30 07:02:35 +08:00

1487 lines
64 KiB
C++

#include "ViewportTile.h"
#include "../utils/Logger.h"
#include <pxr/usd/usdGeom/camera.h>
#include <pxr/usd/usdGeom/xformCommonAPI.h>
#include <pxr/usd/usdGeom/xformable.h>
#include <pxr/usd/usdGeom/bboxCache.h>
#include <pxr/usd/usdGeom/xformCache.h>
#include <pxr/usd/usdGeom/tokens.h>
#include <pxr/usd/usd/primRange.h>
#include <pxr/base/gf/bbox3d.h>
#include <pxr/base/gf/frustum.h>
#include <pxr/base/gf/rotation.h>
#include <pxr/imaging/cameraUtil/conformWindow.h>
#include <imgui.h>
#include <algorithm>
#include <cmath>
#include <map>
#include <string>
namespace UsdLayerManager {
// ---------------------------------------------------------------------------
ViewportTile::ViewportTile() = default;
ViewportTile::~ViewportTile() = default;
// ---------------------------------------------------------------------------
void ViewportTile::SetCommandHistory(CommandHistory* h)
{
m_commandHistory = h;
// The TransformManipulator's command history is set by the container
// (ViewportPanel), not by individual tiles.
}
void ViewportTile::SetTimeCodes(pxr::UsdTimeCode displayTime,
pxr::UsdTimeCode editTime)
{
m_displayTime = displayTime;
m_editTime = editTime;
m_renderer.SetCurrentTimeCode(displayTime);
m_renderer.SetForceRefresh(true);
}
void ViewportTile::SetStage(pxr::UsdStageRefPtr stage)
{
m_stage = stage;
m_renderer.SetStage(stage);
m_camera.SetStage(stage);
m_selectedCameraIndex = 0;
m_cameraListDirty = true;
m_hasLastGfCamera = false;
m_hasSavedFreeCameraState = false;
m_isDrivingUsdCamPrim = false;
m_drivenUsdCamPath = pxr::SdfPath();
m_camera.SwitchToFreeCamera();
if (stage) {
pxr::TfTokenVector purposes = {
pxr::UsdGeomTokens->default_, pxr::UsdGeomTokens->proxy };
pxr::UsdGeomBBoxCache bboxCache(m_displayTime, purposes, true);
pxr::GfBBox3d stageBBox = bboxCache.ComputeWorldBound(stage->GetPseudoRoot());
if (!stageBBox.GetRange().IsEmpty())
m_camera.FrameSelection(stageBBox, 1.1);
}
}
// ---------------------------------------------------------------------------
void ViewportTile::SetSelectedPaths(const pxr::SdfPathVector& paths,
const std::string& primaryPath)
{
m_selectedSdfPaths = paths;
m_selectedPrimPath = primaryPath;
m_renderer.SetSelectedPaths(paths);
}
// ---------------------------------------------------------------------------
void ViewportTile::FrameScene()
{
if (!m_stage) return;
pxr::TfTokenVector purposes = {
pxr::UsdGeomTokens->default_, pxr::UsdGeomTokens->proxy };
pxr::UsdGeomBBoxCache bboxCache(m_displayTime, purposes, true);
pxr::GfBBox3d stageBBox = bboxCache.ComputeWorldBound(m_stage->GetPseudoRoot());
if (!stageBBox.GetRange().IsEmpty()) {
InitCameraNavigation();
m_camera.FrameSelection(stageBBox, 1.1);
}
}
// ---------------------------------------------------------------------------
ViewportTileSettings ViewportTile::GetSettings() const
{
ViewportTileSettings s;
s.renderDelegate = m_renderer.GetCurrentRendererId().GetString();
s.showGrid = m_renderer.ShowGrid();
s.showCameraGuide = m_renderer.ShowCameraGuide();
s.aov = m_renderer.GetCurrentAov().GetString();
s.showGuides = m_renderer.ShowGuides();
s.showProxy = m_renderer.ShowProxy();
s.showRender = m_renderer.ShowRender();
s.aaEnabled = m_renderer.GetAAEnabled();
const pxr::GfVec3f& bg = m_renderer.GetBackgroundColor();
s.bgColorR = bg[0]; s.bgColorG = bg[1]; s.bgColorB = bg[2];
s.bboxMode = static_cast<int>(m_renderer.GetBBoxMode());
s.ambientLightOnly = m_renderer.GetAmbientLightOnly();
s.domeLightEnabled = m_renderer.GetDomeLightEnabled();
s.shadingMode = static_cast<int>(m_renderer.GetShadingMode());
return s;
}
void ViewportTile::ApplySettings(const ViewportTileSettings& s)
{
if (!s.renderDelegate.empty())
m_renderer.SetRendererPlugin(pxr::TfToken(s.renderDelegate));
m_renderer.SetShowGrid(s.showGrid);
m_renderer.SetShowCameraGuide(s.showCameraGuide);
if (!s.aov.empty())
m_renderer.SetCurrentAov(pxr::TfToken(s.aov));
m_renderer.SetShowGuides(s.showGuides);
m_renderer.SetShowProxy(s.showProxy);
m_renderer.SetShowRender(s.showRender);
m_renderer.SetAAEnabled(s.aaEnabled);
m_renderer.SetBackgroundColor(pxr::GfVec3f(s.bgColorR, s.bgColorG, s.bgColorB));
m_renderer.SetBBoxMode(static_cast<BBoxMode>(s.bboxMode));
m_renderer.SetAmbientLightOnly(s.ambientLightOnly);
m_renderer.SetDomeLightEnabled(s.domeLightEnabled);
m_renderer.SetShadingMode(static_cast<ShadingMode>(s.shadingMode));
}
// ---------------------------------------------------------------------------
void ViewportTile::RefreshCameraList()
{
m_cameraPaths.clear();
if (m_stage) {
for (pxr::UsdPrim prim : m_stage->Traverse())
if (prim.IsA<pxr::UsdGeomCamera>())
m_cameraPaths.push_back(prim.GetPath());
}
m_cameraListDirty = false;
}
void ViewportTile::TrySwitchToFreeCamera()
{
if (m_camera.GetMode() != ViewportCamera::CameraMode::UsdCamera) return;
m_selectedCameraIndex = 0;
m_isDrivingUsdCamPrim = false;
if (m_hasSavedFreeCameraState)
m_camera.SwitchToFreeCamera(&m_savedFreeCameraState);
else
m_camera.SwitchToFreeCamera(m_hasLastGfCamera ? &m_lastComputedGfCamera : nullptr);
}
void ViewportTile::InitCameraNavigation()
{
if (m_camera.GetMode() != ViewportCamera::CameraMode::UsdCamera) return;
pxr::SdfPath camPath = m_camera.GetUsdCameraPath();
if (!m_stage || camPath.IsEmpty()) { TrySwitchToFreeCamera(); return; }
pxr::UsdPrim prim = m_stage->GetPrimAtPath(camPath);
if (!prim || !prim.IsA<pxr::UsdGeomCamera>()) { TrySwitchToFreeCamera(); return; }
pxr::UsdGeomCamera usdCam(prim);
pxr::GfCamera gfCam = usdCam.GetCamera(m_displayTime);
m_camera.SwitchToFreeCamera(&gfCam);
// SwitchToFreeCamera calls PullFromCameraTransform, which extracts orbital
// theta/phi via Euler decomposition and uses GetFocusDistance() as m_dist.
// Both are wrong for a USD camera prim: the focus distance is a DOF attribute
// (defaults to 5 units) and the Euler decomposition produces gimbal-affected
// angles when the camera has roll or an unusual orientation.
// Fix: compute orbit center from the scene bbox, then call
// InitOrbitFromEyeAndCenter to derive theta/phi from geometry with zero roll.
{
pxr::TfTokenVector purposes = {
pxr::UsdGeomTokens->default_, pxr::UsdGeomTokens->proxy };
pxr::UsdGeomBBoxCache bboxCache(m_displayTime, purposes, true);
pxr::GfBBox3d stageBBox =
bboxCache.ComputeWorldBound(m_stage->GetPseudoRoot());
if (!stageBBox.GetRange().IsEmpty()) {
pxr::GfVec3d sceneCenter = stageBBox.ComputeCentroid();
pxr::GfVec3d camEye = gfCam.GetFrustum().GetPosition();
pxr::GfVec3d viewDir = gfCam.GetFrustum().ComputeViewDirection();
// Project scene centre onto the view ray for the orbit distance;
// keeps the camera at its authored position.
double dist = pxr::GfDot(sceneCenter - camEye, viewDir);
if (dist > 0.01)
m_camera.InitOrbitFromEyeAndCenter(
camEye, camEye + dist * viewDir, dist);
}
}
m_isDrivingUsdCamPrim = true;
m_drivenUsdCamPath = camPath;
}
pxr::GfVec3d ViewportTile::ComputeGizmoPivot() const
{
if (m_selectedSdfPaths.empty() || !m_stage) return pxr::GfVec3d(0.0);
pxr::UsdPrim prim = m_stage->GetPrimAtPath(m_selectedSdfPaths.front());
if (!prim) return pxr::GfVec3d(0.0);
pxr::UsdGeomXformCache xformCache(m_displayTime);
// Primary: use XformCommonAPI to read the authored translate and pivot op.
// The manipulator must sit at the pivot's world-space position:
// worldPivot = parentToWorld.Transform(translate + pivot)
// NOT at the bounding-box center (geometry centroid) and NOT at
// worldMatrix.ExtractTranslation() which folds in the pivot-inverse op
// and gives the geometry origin, not the authored pivot point.
pxr::UsdGeomXformCommonAPI api(prim);
pxr::GfVec3d translate;
pxr::GfVec3f rotation, scale, pivot;
pxr::UsdGeomXformCommonAPI::RotationOrder rotOrder;
if (api.GetXformVectors(&translate, &rotation, &scale, &pivot,
&rotOrder, m_displayTime))
{
pxr::GfVec3d localPivot = translate + pxr::GfVec3d(pivot[0], pivot[1], pivot[2]);
pxr::UsdPrim parent = prim.GetParent();
if (parent && !parent.IsPseudoRoot()) {
pxr::GfMatrix4d p2w = xformCache.GetLocalToWorldTransform(parent);
return p2w.Transform(localPivot);
}
return localPivot; // root-level prim: parent space == world space
}
// Fallback for incompatible op stacks (e.g. xformOp:transform matrix ops):
// use the prim's world-space origin from the local-to-world matrix.
pxr::GfMatrix4d worldXform = xformCache.GetLocalToWorldTransform(prim);
return worldXform.ExtractTranslation();
}
// ---------------------------------------------------------------------------
// BuildOrthoCamera
// ---------------------------------------------------------------------------
// Camera-to-world rotation matrices (row-major GfMatrix4d: rows are cam axes).
// Right-hand rule verified: cross(cam_X, cam_Y) == cam_Z for every entry.
//
// Y-up (isZUp=false)
// Top cam_X=(1,0,0) cam_Y=(0,0,-1) cam_Z=(0,1,0) eye=c+(0,+d,0)
// Bottom cam_X=(1,0,0) cam_Y=(0,0,+1) cam_Z=(0,-1,0) eye=c+(0,-d,0)
// Front cam_X=(1,0,0) cam_Y=(0,1,0) cam_Z=(0,0,+1) eye=c+(0,0,+d)
// Back cam_X=(-1,0,0) cam_Y=(0,1,0) cam_Z=(0,0,-1) eye=c+(0,0,-d)
// Left cam_X=(0,0,+1) cam_Y=(0,1,0) cam_Z=(-1,0,0) eye=c+(-d,0,0)
// Right cam_X=(0,0,-1) cam_Y=(0,1,0) cam_Z=(+1,0,0) eye=c+(+d,0,0)
//
// Z-up (isZUp=true)
// Top cam_X=(1,0,0) cam_Y=(0,1,0) cam_Z=(0,0,+1) eye=c+(0,0,+d)
// Bottom cam_X=(1,0,0) cam_Y=(0,-1,0) cam_Z=(0,0,-1) eye=c+(0,0,-d)
// Front cam_X=(1,0,0) cam_Y=(0,0,+1) cam_Z=(0,-1,0) eye=c+(0,-d,0)
// Back cam_X=(-1,0,0) cam_Y=(0,0,+1) cam_Z=(0,+1,0) eye=c+(0,+d,0)
// Left cam_X=(0,-1,0) cam_Y=(0,0,+1) cam_Z=(-1,0,0) eye=c+(-d,0,0)
// Right cam_X=(0,+1,0) cam_Y=(0,0,+1) cam_Z=(+1,0,0) eye=c+(+d,0,0)
// ---------------------------------------------------------------------------
pxr::GfCamera ViewportTile::BuildOrthoCamera() const
{
const bool zUp = m_camera.IsZUp();
const double d = m_camera.GetDist();
const pxr::GfVec3d c = m_camera.GetFocalPoint();
const double aspect = (m_viewWidth > 0 && m_viewHeight > 0)
? static_cast<double>(m_viewWidth) / static_cast<double>(m_viewHeight)
: 1.0;
pxr::GfCamera cam;
// Visible vertical world span = d*2. Scales with zoom/framing naturally.
cam.SetOrthographicFromAspectRatioAndSize(
static_cast<float>(aspect),
static_cast<float>(d * 2.0),
pxr::GfCamera::FOVVertical);
// Large symmetric range: ortho cameras may need to see geometry behind
// the nominal camera position (e.g., geometry above a Top-view camera).
cam.SetClippingRange(pxr::GfRange1f(
static_cast<float>(-d * 10.0),
static_cast<float>( d * 100.0)));
// Build camera-to-world matrix from three cam-space axes + eye position.
auto M = [](
double r00, double r01, double r02,
double r10, double r11, double r12,
double r20, double r21, double r22,
const pxr::GfVec3d& e) -> pxr::GfMatrix4d
{
return pxr::GfMatrix4d(
r00, r01, r02, 0,
r10, r11, r12, 0,
r20, r21, r22, 0,
e[0], e[1], e[2], 1);
};
pxr::GfMatrix4d xform(1.0);
if (!zUp) {
switch (m_orthoView) {
case OrthoView::Top: xform = M( 1,0, 0, 0,0,-1, 0, 1, 0, c+pxr::GfVec3d( 0, d, 0)); break;
case OrthoView::Bottom: xform = M( 1,0, 0, 0,0, 1, 0,-1, 0, c+pxr::GfVec3d( 0,-d, 0)); break;
case OrthoView::Front: xform = M( 1,0, 0, 0,1, 0, 0, 0, 1, c+pxr::GfVec3d( 0, 0, d)); break;
case OrthoView::Back: xform = M(-1,0, 0, 0,1, 0, 0, 0,-1, c+pxr::GfVec3d( 0, 0,-d)); break;
case OrthoView::Left: xform = M( 0,0, 1, 0,1, 0, -1, 0, 0, c+pxr::GfVec3d(-d, 0, 0)); break;
case OrthoView::Right: xform = M( 0,0,-1, 0,1, 0, 1, 0, 0, c+pxr::GfVec3d( d, 0, 0)); break;
default: break;
}
} else {
switch (m_orthoView) {
case OrthoView::Top: xform = M( 1,0, 0, 0, 1, 0, 0, 0, 1, c+pxr::GfVec3d( 0, 0, d)); break;
case OrthoView::Bottom: xform = M( 1,0, 0, 0,-1, 0, 0, 0,-1, c+pxr::GfVec3d( 0, 0,-d)); break;
case OrthoView::Front: xform = M( 1,0, 0, 0, 0, 1, 0,-1, 0, c+pxr::GfVec3d( 0,-d, 0)); break;
case OrthoView::Back: xform = M(-1,0, 0, 0, 0, 1, 0, 1, 0, c+pxr::GfVec3d( 0, d, 0)); break;
case OrthoView::Left: xform = M( 0,-1,0, 0, 0, 1, -1, 0, 0, c+pxr::GfVec3d(-d, 0, 0)); break;
case OrthoView::Right: xform = M( 0, 1,0, 0, 0, 1, 1, 0, 0, c+pxr::GfVec3d( d, 0, 0)); break;
default: break;
}
}
cam.SetTransform(xform);
return cam;
}
// ---------------------------------------------------------------------------
// ResolveCamera
// ---------------------------------------------------------------------------
pxr::GfCamera ViewportTile::ResolveCamera()
{
double aspect = static_cast<double>(m_viewWidth) /
static_cast<double>(m_viewHeight);
pxr::GfCamera gfCamera;
// -- Orthographic preset (short-circuit) ---------------------------------
if (m_orthoView != OrthoView::None) {
gfCamera = BuildOrthoCamera();
m_lastComputedGfCamera = gfCamera;
m_hasLastGfCamera = true;
m_renderer.SetCameraStateFromGfCamera(gfCamera);
return gfCamera;
}
if (m_camera.GetMode() == ViewportCamera::CameraMode::Free ||
m_isDrivingUsdCamPrim)
{
m_camera.SetAspectRatio(aspect);
pxr::GfBBox3d emptyBBox;
gfCamera = m_camera.ComputeGfCamera(emptyBBox, false);
pxr::CameraUtilConformWindow(
&gfCamera, pxr::CameraUtilMatchVertically, aspect);
m_lastComputedGfCamera = gfCamera;
m_hasLastGfCamera = true;
m_renderer.SetCameraStateFromGfCamera(gfCamera);
if (m_isDrivingUsdCamPrim) {
pxr::UsdPrim prim = m_stage->GetPrimAtPath(m_drivenUsdCamPath);
if (prim && prim.IsA<pxr::UsdGeomCamera>()) {
// Author the full camera-to-world matrix as a single transform
// op rather than decomposing to XYZ-euler translate/rotate.
// The euler round-trip (Decompose(X,Y,Z) -> rotateXYZ op) is
// lossy: the read-back orientation differs from what was
// written, which made the view jump every time a drag finished
// and the mode flipped back to reading the prim. A matrix op
// round-trips exactly through UsdGeomCamera::GetCamera().
pxr::UsdGeomXformable xformable(prim);
bool resets = false;
std::vector<pxr::UsdGeomXformOp> ops =
xformable.GetOrderedXformOps(&resets);
pxr::UsdGeomXformOp matrixOp;
if (ops.size() == 1 &&
ops[0].GetOpType() == pxr::UsdGeomXformOp::TypeTransform)
matrixOp = ops[0];
else
matrixOp = xformable.MakeMatrixXform();
if (matrixOp)
matrixOp.Set(gfCamera.GetTransform(), m_editTime);
}
if (!m_isOrbiting && !m_isPanning && !m_isDollying) {
m_isDrivingUsdCamPrim = false;
m_camera.SetUsdCamera(m_drivenUsdCamPath);
m_renderer.SetCameraPath(m_drivenUsdCamPath);
}
}
}
else
{
pxr::SdfPath camPath = m_camera.GetUsdCameraPath();
pxr::UsdPrim camPrim = m_stage->GetPrimAtPath(camPath);
if (camPrim && camPrim.IsA<pxr::UsdGeomCamera>()) {
pxr::UsdGeomCamera usdCam(camPrim);
gfCamera = usdCam.GetCamera(m_displayTime);
pxr::CameraUtilConformWindow(
&gfCamera, pxr::CameraUtilMatchVertically, aspect);
m_lastComputedGfCamera = gfCamera;
m_hasLastGfCamera = true;
m_renderer.SetCameraStateFromGfCamera(gfCamera);
}
}
return gfCamera;
}
// ---------------------------------------------------------------------------
// HandleInput
// ---------------------------------------------------------------------------
void ViewportTile::HandleInput(bool isFocused, TransformManipulator& manipulator,
bool dividerActive)
{
ImGuiIO& io = ImGui::GetIO();
ImVec2 mousePos = ImGui::GetMousePos();
// IsWindowHovered scopes to this child window
bool hovered = ImGui::IsWindowHovered(ImGuiHoveredFlags_ChildWindows);
bool altHeld = io.KeyAlt;
// Track click for focus update ??any mouse button press in this tile
// makes it the focused (active) viewport.
if (hovered && (ImGui::IsMouseClicked(ImGuiMouseButton_Left) ||
ImGui::IsMouseClicked(ImGuiMouseButton_Middle) ||
ImGui::IsMouseClicked(ImGuiMouseButton_Right)))
m_wasClickedThisFrame = true;
// --- Manipulator input (only in focused tile) ---
bool manipConsumed = false;
if (isFocused &&
manipulator.GetMode() != ManipulatorMode::Select &&
!m_selectedSdfPaths.empty() && m_stage &&
m_viewWidth > 0 && m_viewHeight > 0 && m_hasLastGfCamera)
{
pxr::GfVec3d pivot = ComputeGizmoPivot();
pxr::GfVec3d camEye = m_lastComputedGfCamera.GetFrustum().GetPosition();
pxr::GfFrustum frustum = m_lastComputedGfCamera.GetFrustum();
pxr::GfMatrix4d viewProj =
frustum.ComputeViewMatrix() * frustum.ComputeProjectionMatrix();
manipConsumed = manipulator.HandleInput(
viewProj, pivot, camEye,
m_imageScreenPos,
m_viewWidth, m_viewHeight,
hovered);
}
// --- Start camera drags ---
if (hovered && !manipConsumed) {
if (altHeld && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
if (m_orthoView == OrthoView::None) {
// Perspective free camera: orbit
InitCameraNavigation();
m_isOrbiting = true;
} else {
// Ortho view: Alt+LMB pans (same as Alt+MMB in Maya)
m_isPanning = true;
}
m_lastMouseX = mousePos.x; m_lastMouseY = mousePos.y;
}
if (altHeld && ImGui::IsMouseClicked(ImGuiMouseButton_Middle)) {
InitCameraNavigation();
m_isPanning = true;
m_lastMouseX = mousePos.x; m_lastMouseY = mousePos.y;
}
if (altHeld && ImGui::IsMouseClicked(ImGuiMouseButton_Right)) {
InitCameraNavigation();
m_isDollying = true;
m_lastMouseX = mousePos.x; m_lastMouseY = mousePos.y;
}
if (!altHeld && ImGui::IsMouseClicked(ImGuiMouseButton_Left) &&
!dividerActive && // don't start rect-select while a split handle is active
m_stage && m_viewWidth > 0 && m_viewHeight > 0)
{
m_rectAnchor = mousePos;
m_rectCurrent = mousePos;
m_rectDragStarted = true;
m_isRectSelecting = false;
}
}
// --- Stop Alt-based camera drags when Alt is released ---
// Alt+LMB / Alt+MMB / Alt+RMB navigation should end as soon as Alt is
// released, even if the mouse button is still held.
if (!altHeld) {
m_isOrbiting = false;
m_isPanning = false;
m_isDollying = false;
}
// --- Process camera drags ---
if (m_isOrbiting) {
float dx = mousePos.x - m_lastMouseX;
float dy = mousePos.y - m_lastMouseY;
m_camera.Tumble(0.25 * dx, 0.25 * dy);
m_lastMouseX = mousePos.x; m_lastMouseY = mousePos.y;
}
if (m_isPanning) {
float dx = mousePos.x - m_lastMouseX;
float dy = mousePos.y - m_lastMouseY;
if (m_orthoView != OrthoView::None) {
// Ortho pan: derive right/up from the ortho camera matrix so the
// movement direction always matches the view, regardless of whatever
// stale theta/phi the underlying free-camera orbital state carries.
// Scale: visible world height = dist*2, so 1 pixel = (dist*2)/viewH.
pxr::GfCamera orthoCam = BuildOrthoCamera();
pxr::GfMatrix4d xform = orthoCam.GetTransform();
// row0 = cam_X (screen right), row1 = cam_Y (screen up) in world space
pxr::GfVec3d screenRight(xform[0][0], xform[0][1], xform[0][2]);
pxr::GfVec3d screenUp (xform[1][0], xform[1][1], xform[1][2]);
double factor = m_camera.GetDist() * 2.0
/ static_cast<double>(std::max(m_viewHeight, 1));
pxr::GfVec3d newCenter = m_camera.GetFocalPoint()
- screenRight * (static_cast<double>(dx) * factor)
+ screenUp * (static_cast<double>(dy) * factor);
m_camera.SetFocalPoint(newCenter);
} else {
double factor = m_camera.ComputePixelsToWorldFactor(
static_cast<double>(std::max(m_viewHeight, 1)));
m_camera.Truck(static_cast<double>(-dx) * factor,
static_cast<double>( dy) * factor);
}
m_lastMouseX = mousePos.x; m_lastMouseY = mousePos.y;
}
if (m_isDollying) {
float dx = mousePos.x - m_lastMouseX;
float dy = mousePos.y - m_lastMouseY;
double zoomDelta = -0.002 * (dx + dy);
m_camera.AdjustDistance(1.0 + zoomDelta);
m_lastMouseX = mousePos.x; m_lastMouseY = mousePos.y;
}
// --- Update rect selection while LMB held ---
if (m_rectDragStarted && !altHeld &&
ImGui::IsMouseDown(ImGuiMouseButton_Left) &&
!m_isOrbiting && !m_isPanning && !m_isDollying &&
!manipulator.IsDragging())
{
m_rectCurrent = mousePos;
float dx = m_rectCurrent.x - m_rectAnchor.x;
float dy = m_rectCurrent.y - m_rectAnchor.y;
if (std::sqrt(dx * dx + dy * dy) > kRectDragThreshold)
m_isRectSelecting = true;
}
// --- LMB released ---
if (ImGui::IsMouseReleased(ImGuiMouseButton_Left)) {
m_isOrbiting = false;
if (m_rectDragStarted && !altHeld &&
m_stage && m_viewWidth > 0 && m_viewHeight > 0)
{
if (hovered) {
int ax = static_cast<int>(m_rectAnchor.x - m_imageScreenPos.x);
int ay = static_cast<int>(m_rectAnchor.y - m_imageScreenPos.y);
int cx = static_cast<int>(m_rectCurrent.x - m_imageScreenPos.x);
int cy = static_cast<int>(m_rectCurrent.y - m_imageScreenPos.y);
if (m_isRectSelecting) {
pxr::SdfPathVector hitPaths;
if (m_renderer.PickObjectsInRect(
ax, ay, cx, cy,
m_viewWidth, m_viewHeight, &hitPaths))
{
if (io.KeyShift) {
for (const auto& hp : hitPaths)
if (std::find(m_selectedSdfPaths.begin(),
m_selectedSdfPaths.end(), hp)
== m_selectedSdfPaths.end())
m_selectedSdfPaths.push_back(hp);
} else {
m_selectedSdfPaths = hitPaths;
}
m_selectedPrimPath = m_selectedSdfPaths.empty()
? "" : m_selectedSdfPaths.front().GetString();
m_renderer.SetSelectedPaths(m_selectedSdfPaths);
if (OnPrimsPickedRect) {
std::vector<std::string> pathStrs;
pathStrs.reserve(m_selectedSdfPaths.size());
for (const auto& p : m_selectedSdfPaths)
pathStrs.push_back(p.GetString());
OnPrimsPickedRect(pathStrs);
}
} else if (!io.KeyShift) {
m_renderer.ClearSelected();
m_selectedPrimPath.clear();
m_selectedSdfPaths.clear();
if (OnPrimsPickedRect) OnPrimsPickedRect({});
}
} else {
// Single click pick
if (cx >= 0 && cy >= 0 && cx < m_viewWidth && cy < m_viewHeight) {
pxr::SdfPath camPickPath;
bool hitCamera = false;
if (m_hasLastGfCamera) {
pxr::GfFrustum fr = m_lastComputedGfCamera.GetFrustum();
pxr::GfMatrix4d vp = fr.ComputeViewMatrix()
* fr.ComputeProjectionMatrix();
hitCamera = m_renderer.PickCameraAtPoint(
m_stage, mousePos.x, mousePos.y, vp,
m_imageScreenPos.x, m_imageScreenPos.y,
m_viewWidth, m_viewHeight,
m_camera.GetDist(), &camPickPath);
}
pxr::SdfPath hitPath = hitCamera ? camPickPath : pxr::SdfPath();
bool hitGeom = false;
pxr::GfVec3d hitPoint;
if (!hitCamera)
hitGeom = m_renderer.PickObject(
cx, cy, m_viewWidth, m_viewHeight, &hitPoint, &hitPath);
if (hitCamera || hitGeom) {
if (io.KeyShift) {
auto it = std::find(m_selectedSdfPaths.begin(),
m_selectedSdfPaths.end(), hitPath);
if (it != m_selectedSdfPaths.end())
m_selectedSdfPaths.erase(it);
else
m_selectedSdfPaths.push_back(hitPath);
m_selectedPrimPath = m_selectedSdfPaths.empty()
? "" : m_selectedSdfPaths.back().GetString();
m_renderer.ClearSelected();
for (const auto& p : m_selectedSdfPaths)
m_renderer.AddSelected(p);
if (OnPrimsPickedRect) {
std::vector<std::string> pathStrs;
pathStrs.reserve(m_selectedSdfPaths.size());
for (const auto& p : m_selectedSdfPaths)
pathStrs.push_back(p.GetString());
OnPrimsPickedRect(pathStrs);
}
} else {
m_selectedPrimPath = hitPath.GetString();
m_selectedSdfPaths = { hitPath };
m_renderer.ClearSelected();
m_renderer.AddSelected(hitPath);
if (OnPrimPicked) OnPrimPicked(m_selectedPrimPath);
}
} else {
if (!io.KeyShift) {
m_renderer.ClearSelected();
m_selectedPrimPath.clear();
m_selectedSdfPaths.clear();
if (OnPrimPicked) OnPrimPicked("");
}
}
}
}
}
}
m_rectDragStarted = false;
m_isRectSelecting = false;
}
if (ImGui::IsMouseReleased(ImGuiMouseButton_Middle)) m_isPanning = false;
if (ImGui::IsMouseReleased(ImGuiMouseButton_Right)) m_isDollying = false;
// --- Mouse wheel zoom ---
if (hovered && io.MouseWheel != 0.0f) {
InitCameraNavigation();
double delta = std::max(-0.5, std::min(0.5,
static_cast<double>(io.MouseWheel) * 0.12));
m_camera.AdjustDistance(1.0 - delta);
}
// --- Keyboard shortcuts ---
// All shortcuts are gated on `hovered` (mouse is over this tile) so that
// exactly one viewport responds per key press. This matches standard DCC
// behaviour: the viewport under the cursor is the "active" one for both
// mouse and keyboard interaction.
if (!io.WantTextInput && hovered) {
if (ImGui::IsKeyPressed(ImGuiKey_F)) {
if (!m_selectedSdfPaths.empty() && m_stage) {
pxr::TfTokenVector purposes = {
pxr::UsdGeomTokens->default_, pxr::UsdGeomTokens->proxy };
pxr::UsdGeomBBoxCache bboxCache(
m_displayTime, purposes, true);
pxr::GfRange3d combined;
for (const auto& path : m_selectedSdfPaths) {
pxr::UsdPrim prim = m_stage->GetPrimAtPath(path);
if (!prim) continue;
pxr::GfBBox3d bbox = bboxCache.ComputeWorldBound(prim);
combined.UnionWith(bbox.ComputeAlignedRange());
}
if (combined.IsEmpty()) {
pxr::UsdGeomXformCache xformCache(m_displayTime);
for (const auto& path : m_selectedSdfPaths) {
pxr::UsdPrim prim = m_stage->GetPrimAtPath(path);
if (!prim) continue;
pxr::GfMatrix4d worldXform =
xformCache.GetLocalToWorldTransform(prim);
pxr::GfVec3d pos = worldXform.ExtractTranslation();
combined.UnionWith(pos - pxr::GfVec3d(1.0));
combined.UnionWith(pos + pxr::GfVec3d(1.0));
}
}
if (!combined.IsEmpty()) {
InitCameraNavigation();
m_camera.FrameSelection(pxr::GfBBox3d(combined), 1.1);
m_renderer.SetForceRefresh(true);
}
}
}
if (ImGui::IsKeyPressed(ImGuiKey_A)) {
FrameScene();
m_renderer.SetForceRefresh(true);
}
// Q/W/E/R tool mode and World/Object space are handled globally by
// ViewportPanel so they always work regardless of which tile is hovered.
}
}
// ---------------------------------------------------------------------------
// DrawSelectionRect
// ---------------------------------------------------------------------------
void ViewportTile::DrawSelectionRect()
{
if (!m_isRectSelecting || !m_rectDragStarted) return;
float vpMinX = m_imageScreenPos.x;
float vpMinY = m_imageScreenPos.y;
float vpMaxX = vpMinX + static_cast<float>(m_viewWidth);
float vpMaxY = vpMinY + static_cast<float>(m_viewHeight);
ImVec2 drawMin(
std::max(vpMinX, std::min(m_rectAnchor.x, m_rectCurrent.x)),
std::max(vpMinY, std::min(m_rectAnchor.y, m_rectCurrent.y)));
ImVec2 drawMax(
std::min(vpMaxX, std::max(m_rectAnchor.x, m_rectCurrent.x)),
std::min(vpMaxY, std::max(m_rectAnchor.y, m_rectCurrent.y)));
if (drawMin.x >= drawMax.x || drawMin.y >= drawMax.y) return;
ImDrawList* dl = ImGui::GetWindowDrawList();
dl->AddRectFilled(drawMin, drawMax, IM_COL32(100, 160, 255, 40));
dl->AddRect (drawMin, drawMax, IM_COL32(100, 160, 255, 220), 0.0f, 0, 1.5f);
}
// ---------------------------------------------------------------------------
// RenderContextMenu
// ---------------------------------------------------------------------------
void ViewportTile::RenderContextMenu(int tileIndex)
{
if (ImGui::IsWindowHovered(ImGuiHoveredFlags_ChildWindows) &&
ImGui::IsMouseReleased(ImGuiMouseButton_Right) &&
!ImGui::GetIO().KeyAlt)
{
std::string popupId = "VPCtxMenu_" + std::to_string(tileIndex);
ImGui::OpenPopup(popupId.c_str());
}
std::string popupId = "VPCtxMenu_" + std::to_string(tileIndex);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(8, 8));
if (ImGui::BeginPopup(popupId.c_str())) {
bool showGrid = m_renderer.ShowGrid();
if (ImGui::MenuItem("Show Grid", nullptr, &showGrid))
m_renderer.SetShowGrid(showGrid);
bool showGuide = m_renderer.ShowCameraGuide();
if (ImGui::MenuItem("Camera Guide", nullptr, &showGuide))
m_renderer.SetShowCameraGuide(showGuide);
ImGui::Separator();
bool ambientOnly = m_renderer.GetAmbientLightOnly();
if (ImGui::MenuItem("Camera Light", nullptr, &ambientOnly))
m_renderer.SetAmbientLightOnly(ambientOnly);
bool domeLight = m_renderer.GetDomeLightEnabled();
if (ImGui::MenuItem("Dome Light", nullptr, &domeLight))
m_renderer.SetDomeLightEnabled(domeLight);
ImGui::Separator();
if (ImGui::BeginMenu("Bounding Box")) {
BBoxMode cur = m_renderer.GetBBoxMode();
if (ImGui::MenuItem("None", nullptr, cur == BBoxMode::None))
m_renderer.SetBBoxMode(BBoxMode::None);
if (ImGui::MenuItem("Per Object", nullptr, cur == BBoxMode::PerObject))
m_renderer.SetBBoxMode(BBoxMode::PerObject);
if (ImGui::MenuItem("All Selection",nullptr, cur == BBoxMode::AllSelection))
m_renderer.SetBBoxMode(BBoxMode::AllSelection);
ImGui::EndMenu();
}
ImGui::Separator();
if (ImGui::BeginMenu("Background")) {
auto& bg = m_renderer.GetBackgroundColor();
if (ImGui::MenuItem("Dark Gray", nullptr,
bg == pxr::GfVec3f(0.15f, 0.15f, 0.15f)))
m_renderer.SetBackgroundColor(pxr::GfVec3f(0.15f, 0.15f, 0.15f));
if (ImGui::MenuItem("Black", nullptr,
bg == pxr::GfVec3f(0.0f, 0.0f, 0.0f)))
m_renderer.SetBackgroundColor(pxr::GfVec3f(0.0f, 0.0f, 0.0f));
if (ImGui::MenuItem("Light Gray", nullptr,
bg == pxr::GfVec3f(0.45f, 0.45f, 0.45f)))
m_renderer.SetBackgroundColor(pxr::GfVec3f(0.45f, 0.45f, 0.45f));
if (ImGui::MenuItem("Midnight Blue", nullptr,
bg == pxr::GfVec3f(0.1f, 0.1f, 0.2f)))
m_renderer.SetBackgroundColor(pxr::GfVec3f(0.1f, 0.1f, 0.2f));
ImGui::EndMenu();
}
ImGui::EndPopup();
}
ImGui::PopStyleVar();
}
// ---------------------------------------------------------------------------
// RenderCompactToolbar -- all per-tile controls in one compact row
// ---------------------------------------------------------------------------
void ViewportTile::RenderCompactToolbar(int tileIndex)
{
if (m_cameraListDirty) RefreshCameraList();
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(3, 3));
// -- Camera selector -----------------------------------------------------
ImGui::PushItemWidth(140.0f);
// Helper: ortho view display name
auto OrthoName = [](OrthoView v) -> const char* {
switch (v) {
case OrthoView::Top: return "Top";
case OrthoView::Bottom: return "Bottom";
case OrthoView::Front: return "Front";
case OrthoView::Back: return "Back";
case OrthoView::Left: return "Left";
case OrthoView::Right: return "Right";
default: return nullptr;
}
};
// Determine display label: ortho view name > USD cam prim path > "Free Camera"
const char* currentLabel = "Free Camera";
if (m_orthoView != OrthoView::None) {
currentLabel = OrthoName(m_orthoView);
} else if (m_selectedCameraIndex > 0 &&
static_cast<size_t>(m_selectedCameraIndex - 1) < m_cameraPaths.size()) {
currentLabel = m_cameraPaths[m_selectedCameraIndex - 1].GetText();
}
std::string camComboId = "##Cam_" + std::to_string(tileIndex);
if (ImGui::BeginCombo(camComboId.c_str(), currentLabel)) {
RefreshCameraList();
// -- Perspective free camera -----------------------------------------
bool freeSel = (m_orthoView == OrthoView::None && m_selectedCameraIndex == 0);
if (ImGui::Selectable("Free Camera", freeSel)) {
m_orthoView = OrthoView::None;
m_selectedCameraIndex = 0;
m_isDrivingUsdCamPrim = false;
if (m_hasSavedFreeCameraState)
m_camera.SwitchToFreeCamera(&m_savedFreeCameraState);
else
m_camera.SwitchToFreeCamera(
m_hasLastGfCamera ? &m_lastComputedGfCamera : nullptr);
}
// -- Orthographic presets -------------------------------------------
ImGui::Separator();
static const struct { OrthoView view; const char* label; } kOrtho[] = {
{ OrthoView::Top, "Top" },
{ OrthoView::Bottom, "Bottom" },
{ OrthoView::Front, "Front" },
{ OrthoView::Back, "Back" },
{ OrthoView::Left, "Left" },
{ OrthoView::Right, "Right" },
};
for (const auto& ov : kOrtho) {
bool isSel = (m_orthoView == ov.view);
if (ImGui::Selectable(ov.label, isSel)) {
m_orthoView = ov.view;
m_selectedCameraIndex = 0;
m_isDrivingUsdCamPrim = false;
// Ensure free-camera mode so center/dist are maintained
if (m_camera.GetMode() == ViewportCamera::CameraMode::UsdCamera) {
m_camera.SwitchToFreeCamera(
m_hasLastGfCamera ? &m_lastComputedGfCamera : nullptr);
}
}
if (isSel) ImGui::SetItemDefaultFocus();
}
// -- USD camera prims -----------------------------------------------
if (!m_cameraPaths.empty()) ImGui::Separator();
for (size_t i = 0; i < m_cameraPaths.size(); ++i) {
bool isSelected = (m_orthoView == OrthoView::None &&
m_selectedCameraIndex == static_cast<int>(i + 1));
if (ImGui::Selectable(m_cameraPaths[i].GetText(), isSelected)) {
if (m_camera.GetMode() == ViewportCamera::CameraMode::Free
&& m_hasLastGfCamera)
{
m_savedFreeCameraState = m_lastComputedGfCamera;
m_hasSavedFreeCameraState = true;
}
m_orthoView = OrthoView::None;
m_selectedCameraIndex = static_cast<int>(i + 1);
m_camera.SetUsdCamera(m_cameraPaths[i]);
m_renderer.SetCameraPath(m_cameraPaths[i]);
}
if (isSelected) ImGui::SetItemDefaultFocus();
}
ImGui::EndCombo();
}
if (ImGui::IsItemHovered()) {
if (m_orthoView != OrthoView::None)
ImGui::SetTooltip("Orthographic: %s\n"
"Alt+LMB / Alt+MMB: Pan\n"
"Alt+RMB / Scroll: Zoom\n"
"F: Frame selection A: Frame all",
OrthoName(m_orthoView));
else
ImGui::SetTooltip("Camera\n"
"Alt+LMB: Orbit Alt+MMB: Pan Alt+RMB: Dolly\n"
"Scroll: Zoom F: Frame sel A: Frame all");
}
ImGui::PopItemWidth();
ImGui::SameLine();
ImGui::TextDisabled("|");
ImGui::SameLine();
// -- Render delegate (global) --------------------------------------------
{
pxr::TfToken currentId = m_renderer.GetCurrentRendererId();
std::string displayName = currentId.IsEmpty()
? "Rdr"
: UsdSceneRenderer::GetRendererDisplayName(currentId);
if (displayName.size() > 8) displayName = displayName.substr(0, 8);
std::string rdrPopupId = "RdrPopup_" + std::to_string(tileIndex);
ImGui::Button(displayName.c_str(), ImVec2(72.0f, 0.0f));
if (ImGui::IsItemHovered())
ImGui::SetTooltip("Render delegate");
if (ImGui::BeginPopupContextItem(rdrPopupId.c_str(),
ImGuiPopupFlags_MouseButtonLeft))
{
for (const auto& pluginId : UsdSceneRenderer::GetRendererPlugins()) {
std::string name = UsdSceneRenderer::GetRendererDisplayName(pluginId);
if (name.empty()) name = pluginId.GetString();
bool selected = (pluginId == currentId);
if (ImGui::MenuItem(name.c_str(), nullptr, selected) && !selected)
m_renderer.SetRendererPlugin(pluginId);
}
ImGui::EndPopup();
}
}
ImGui::SameLine();
ImGui::TextDisabled("|");
ImGui::SameLine();
// -- View option toggles (icon buttons) ----------------------------------
auto iconToggle = [this](const char* id, const char* fallback,
Icon iconEnum, bool active,
const char* tooltip) -> bool
{
const ImVec4 kActive (0.26f, 0.59f, 0.98f, 1.00f);
const ImVec4 kActiveHv(0.36f, 0.69f, 1.00f, 1.00f);
if (active) {
ImGui::PushStyleColor(ImGuiCol_Button, kActive);
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, kActiveHv);
}
bool clicked = false;
if (m_iconManager) {
ImTextureID tex = m_iconManager->Get(iconEnum);
clicked = ImGui::ImageButton(id, ImTextureRef(tex), ImVec2(16.f, 16.f));
} else {
clicked = ImGui::Button(fallback);
}
if (active) ImGui::PopStyleColor(2);
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", tooltip);
return clicked;
};
std::string gridId = "##grd_" + std::to_string(tileIndex);
std::string aaId = "##aa_" + std::to_string(tileIndex);
bool showGrid = m_renderer.ShowGrid();
if (iconToggle(gridId.c_str(), "Grd", Icon::Grid, showGrid, "Toggle grid"))
m_renderer.SetShowGrid(!showGrid);
ImGui::SameLine();
bool aa = m_renderer.GetAAEnabled();
if (iconToggle(aaId.c_str(), "AA", Icon::Antialias, aa, "Toggle anti-aliasing"))
m_renderer.SetAAEnabled(!aa);
ImGui::SameLine();
ImGui::TextDisabled("|");
ImGui::SameLine();
// -- Gear / viewport options popup ---------------------------------------
{
std::string gearId = "##gear_" + std::to_string(tileIndex);
std::string gearPopupId = "GearPopup_" + std::to_string(tileIndex);
if (m_iconManager) {
ImTextureID tex = m_iconManager->Get(Icon::Settings);
ImGui::ImageButton(gearId.c_str(), ImTextureRef(tex), ImVec2(16.f, 16.f));
} else {
ImGui::Button("*");
}
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Viewport options");
if (ImGui::BeginPopupContextItem(gearPopupId.c_str(),
ImGuiPopupFlags_MouseButtonLeft))
{
// -- Shading mode ------------------------------------------------
if (ImGui::BeginMenu("Shading")) {
ShadingMode cur = m_renderer.GetShadingMode();
auto shadingItem = [&](const char* label, ShadingMode mode) {
if (ImGui::MenuItem(label, nullptr, cur == mode))
m_renderer.SetShadingMode(mode);
};
shadingItem("Smooth Shaded", ShadingMode::SmoothShaded);
shadingItem("Flat Shaded", ShadingMode::FlatShaded);
shadingItem("Wireframe on Surface", ShadingMode::WireframeOnSurface);
shadingItem("Wireframe", ShadingMode::Wireframe);
shadingItem("Unlit", ShadingMode::Unlit);
ImGui::EndMenu();
}
// -- AOV ---------------------------------------------------------
{
pxr::TfTokenVector aovs = m_renderer.GetRendererAovs();
if (ImGui::BeginMenu("AOV", !aovs.empty())) {
pxr::TfToken currentAov = m_renderer.GetCurrentAov();
for (const auto& aov : aovs) {
bool checked = (aov == currentAov);
if (ImGui::MenuItem(aov.GetText(), nullptr, checked) && !checked)
m_renderer.SetCurrentAov(aov);
}
ImGui::EndMenu();
}
}
// -- Lighting ----------------------------------------------------
if (ImGui::BeginMenu("Lighting")) {
bool ambient = m_renderer.GetAmbientLightOnly();
if (ImGui::MenuItem("Camera Headlight", nullptr, ambient))
m_renderer.SetAmbientLightOnly(true);
if (ImGui::MenuItem("Scene Lights", nullptr, !ambient))
m_renderer.SetAmbientLightOnly(false);
ImGui::Separator();
bool dome = m_renderer.GetDomeLightEnabled();
if (ImGui::MenuItem("Dome Light", nullptr, dome))
m_renderer.SetDomeLightEnabled(!dome);
ImGui::EndMenu();
}
ImGui::Separator();
// -- Display toggles ---------------------------------------------
{
bool grid = m_renderer.ShowGrid();
if (ImGui::MenuItem("Show Grid", nullptr, grid))
m_renderer.SetShowGrid(!grid);
bool guide = m_renderer.ShowCameraGuide();
if (ImGui::MenuItem("Camera Guide", nullptr, guide))
m_renderer.SetShowCameraGuide(!guide);
ImGui::Separator();
bool purGuide = m_renderer.ShowGuides();
if (ImGui::MenuItem("Purpose: Guide", nullptr, purGuide))
m_renderer.SetShowGuides(!purGuide);
bool purProxy = m_renderer.ShowProxy();
if (ImGui::MenuItem("Purpose: Proxy", nullptr, purProxy))
m_renderer.SetShowProxy(!purProxy);
bool purRender = m_renderer.ShowRender();
if (ImGui::MenuItem("Purpose: Render", nullptr, purRender))
m_renderer.SetShowRender(!purRender);
bool aaItem = m_renderer.GetAAEnabled();
if (ImGui::MenuItem("Anti-Aliasing", nullptr, aaItem))
m_renderer.SetAAEnabled(!aaItem);
}
ImGui::Separator();
// -- BBox --------------------------------------------------------
if (ImGui::BeginMenu("Bounding Box")) {
BBoxMode bbox = m_renderer.GetBBoxMode();
if (ImGui::MenuItem("None", nullptr, bbox == BBoxMode::None))
m_renderer.SetBBoxMode(BBoxMode::None);
if (ImGui::MenuItem("Per Object", nullptr, bbox == BBoxMode::PerObject))
m_renderer.SetBBoxMode(BBoxMode::PerObject);
if (ImGui::MenuItem("All Selection", nullptr, bbox == BBoxMode::AllSelection))
m_renderer.SetBBoxMode(BBoxMode::AllSelection);
ImGui::EndMenu();
}
// -- Background --------------------------------------------------
if (ImGui::BeginMenu("Background")) {
auto& bg = m_renderer.GetBackgroundColor();
if (ImGui::MenuItem("Dark Gray", nullptr, bg == pxr::GfVec3f(0.15f, 0.15f, 0.15f)))
m_renderer.SetBackgroundColor(pxr::GfVec3f(0.15f, 0.15f, 0.15f));
if (ImGui::MenuItem("Black", nullptr, bg == pxr::GfVec3f(0.0f, 0.0f, 0.0f)))
m_renderer.SetBackgroundColor(pxr::GfVec3f(0.0f, 0.0f, 0.0f));
if (ImGui::MenuItem("Light Gray", nullptr, bg == pxr::GfVec3f(0.45f, 0.45f, 0.45f)))
m_renderer.SetBackgroundColor(pxr::GfVec3f(0.45f, 0.45f, 0.45f));
if (ImGui::MenuItem("Midnight Blue", nullptr, bg == pxr::GfVec3f(0.1f, 0.1f, 0.2f)))
m_renderer.SetBackgroundColor(pxr::GfVec3f(0.1f, 0.1f, 0.2f));
ImGui::EndMenu();
}
ImGui::Separator();
if (ImGui::MenuItem("Renderer Settings..."))
m_showRendererSettings = true;
ImGui::EndPopup();
}
}
ImGui::PopStyleVar(); // ItemSpacing
}
// ---------------------------------------------------------------------------
// RenderManipulatorOverlay -- vertical Q/W/E/R + space buttons on the image
// ---------------------------------------------------------------------------
void ViewportTile::RenderManipulatorOverlay(TransformManipulator& manipulator)
{
const float kButtonSize = 32.0f;
const float kIconPad = 4.0f;
const float kRounding = 4.0f;
const float kPadX = 10.0f;
const float kPadY = 10.0f;
const float kSpacing = 4.0f;
const ImVec2 kBtnSz(kButtonSize, kButtonSize);
ImVec2 origin(m_imageScreenPos.x + kPadX, m_imageScreenPos.y + kPadY);
struct ToolInfo {
ManipulatorMode mode;
Icon icon;
const char* fallbackLabel;
const char* tooltip;
const char* id;
};
static const ToolInfo tools[] = {
{ ManipulatorMode::Select, Icon::ToolSelect, "Q", "Select (Q)", "##ovl_q" },
{ ManipulatorMode::Move, Icon::ToolMove, "W", "Move (W)", "##ovl_w" },
{ ManipulatorMode::Rotate, Icon::ToolRotate, "E", "Rotate (E)", "##ovl_e" },
{ ManipulatorMode::Scale, Icon::ToolScale, "R", "Scale (R)", "##ovl_r" },
};
ManipulatorMode current = manipulator.GetMode();
ImDrawList* dl = ImGui::GetWindowDrawList();
for (int i = 0; i < 4; ++i) {
const auto& t = tools[i];
bool active = (current == t.mode);
ImVec2 btnMin(origin.x, origin.y + i * (kButtonSize + kSpacing));
ImVec2 btnMax(btnMin.x + kButtonSize, btnMin.y + kButtonSize);
ImGui::SetCursorScreenPos(btnMin);
bool clicked = ImGui::InvisibleButton(t.id, kBtnSz);
bool hovBtn = ImGui::IsItemHovered();
ImU32 bgCol = active ? IM_COL32( 66, 150, 250, 230) :
hovBtn ? IM_COL32( 64, 64, 64, 220) :
IM_COL32( 26, 26, 26, 178);
dl->AddRectFilled(btnMin, btnMax, bgCol, kRounding);
if (m_iconManager) {
ImTextureID texId = m_iconManager->Get(t.icon);
dl->AddImage(ImTextureRef(texId),
ImVec2(btnMin.x + kIconPad, btnMin.y + kIconPad),
ImVec2(btnMax.x - kIconPad, btnMax.y - kIconPad));
} else {
float tx = btnMin.x + (kButtonSize - ImGui::CalcTextSize(t.fallbackLabel).x) * 0.5f;
float ty = btnMin.y + (kButtonSize - ImGui::GetTextLineHeight()) * 0.5f;
dl->AddText(ImVec2(tx, ty), IM_COL32(255,255,255,255), t.fallbackLabel);
}
if (active)
dl->AddRect(btnMin, btnMax, IM_COL32(100, 180, 255, 200), kRounding, 0, 1.5f);
if (clicked) manipulator.SetMode(t.mode);
if (hovBtn) ImGui::SetTooltip("%s", t.tooltip);
}
// World / Object space toggle
float spaceY = origin.y + 4 * (kButtonSize + kSpacing) + 4.0f;
ImVec2 spBtnMin(origin.x, spaceY);
ImVec2 spBtnMax(spBtnMin.x + kButtonSize, spBtnMin.y + kButtonSize);
bool isWorld = (manipulator.GetTransformSpace() == TransformSpace::World);
ImGui::SetCursorScreenPos(spBtnMin);
bool spClicked = ImGui::InvisibleButton("##ovl_space", kBtnSz);
bool spHovered = ImGui::IsItemHovered();
ImU32 spBgCol = isWorld ? IM_COL32( 66, 150, 250, 230) :
spHovered ? IM_COL32( 64, 64, 64, 220) :
IM_COL32( 26, 26, 26, 178);
dl->AddRectFilled(spBtnMin, spBtnMax, spBgCol, kRounding);
if (m_iconManager) {
ImTextureID spTex = m_iconManager->Get(isWorld ? Icon::WorldSpace : Icon::LocalSpace);
dl->AddImage(ImTextureRef(spTex),
ImVec2(spBtnMin.x + kIconPad, spBtnMin.y + kIconPad),
ImVec2(spBtnMax.x - kIconPad, spBtnMax.y - kIconPad));
} else {
const char* spLabel = isWorld ? "W" : "O";
float spTx = spBtnMin.x + (kButtonSize - ImGui::CalcTextSize(spLabel).x) * 0.5f;
float spTy = spBtnMin.y + (kButtonSize - ImGui::GetTextLineHeight()) * 0.5f;
dl->AddText(ImVec2(spTx, spTy), IM_COL32(255,255,255,255), spLabel);
}
if (isWorld)
dl->AddRect(spBtnMin, spBtnMax, IM_COL32(100,180,255,200), kRounding, 0, 1.5f);
if (spClicked)
manipulator.SetTransformSpace(isWorld ? TransformSpace::Object : TransformSpace::World);
if (spHovered)
ImGui::SetTooltip(isWorld
? "World space\nClick to switch to Object space"
: "Object space\nClick to switch to World space");
}
// ---------------------------------------------------------------------------
// Render
// ---------------------------------------------------------------------------
void ViewportTile::Render(int tileIndex, ImVec2 pos, ImVec2 size,
bool isFocused, TransformManipulator& manipulator,
bool dividerActive)
{
m_wasClickedThisFrame = false;
m_wasHoveredThisFrame = false;
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0));
ImGui::SetCursorScreenPos(pos);
std::string childId = "##vptile_" + std::to_string(tileIndex);
ImGuiWindowFlags flags = ImGuiWindowFlags_NoScrollbar
| ImGuiWindowFlags_NoScrollWithMouse
| ImGuiWindowFlags_NoBackground;
bool childOpen = ImGui::BeginChild(childId.c_str(), size, false, flags);
ImGui::PopStyleVar(); // WindowPadding
if (!childOpen) {
ImGui::EndChild();
return;
}
// Hover detection for the whole child (used by Space maximize in container)
m_wasHoveredThisFrame = ImGui::IsWindowHovered(ImGuiHoveredFlags_ChildWindows);
// NOTE: Border is drawn AFTER EndChild (see below) so it is not clipped
// by this child window's scissor rect.
// -- Compact toolbar ------------------------------------------------------
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(4, 2));
ImGui::SetCursorPos(ImVec2(4, 2));
RenderCompactToolbar(tileIndex);
ImGui::PopStyleVar();
ImGui::Separator();
// -- Scene image area -----------------------------------------------------
ImVec2 avail = ImGui::GetContentRegionAvail();
m_viewWidth = static_cast<int>(avail.x);
m_viewHeight = static_cast<int>(avail.y);
if (m_viewWidth > 0 && m_viewHeight > 0 && m_stage) {
m_imageScreenPos = ImGui::GetCursorScreenPos();
HandleInput(isFocused, manipulator, dividerActive);
pxr::GfCamera gfCamera = ResolveCamera();
// Render scene into FBO
m_renderer.Render(m_viewWidth, m_viewHeight);
// Overlays into FBO
if (m_hasLastGfCamera) {
pxr::GfFrustum frustum = gfCamera.GetFrustum();
pxr::GfMatrix4d viewProj =
frustum.ComputeViewMatrix() * frustum.ComputeProjectionMatrix();
m_renderer.DrawAxis(viewProj, m_camera.GetDist());
m_renderer.DrawBoundingBoxes(m_selectedSdfPaths, viewProj);
m_renderer.DrawCameraWireframes(
m_stage, m_selectedSdfPaths,
m_camera.GetUsdCameraPath(),
viewProj, m_camera.GetDist());
m_renderer.DrawLightWireframes(
m_stage, m_selectedSdfPaths,
viewProj, m_camera.GetDist());
}
// Display FBO texture (flip UV Y: OpenGL is bottom-up)
uint32_t texID = m_renderer.GetColorTextureID();
if (texID != 0) {
ImGui::Image(
ImTextureID(static_cast<ImU64>(texID)),
ImVec2(static_cast<float>(m_viewWidth),
static_cast<float>(m_viewHeight)),
ImVec2(0, 1), ImVec2(1, 0));
} else {
ImGui::TextColored(ImVec4(1,0,0,1),
"Viewport: No texture (renderer not initialised)");
}
// -- 2-D ImGui overlays on top of the image --------------------------
DrawSelectionRect();
// Gizmo -- only in focused tile
if (isFocused && m_hasLastGfCamera &&
manipulator.GetMode() != ManipulatorMode::Select &&
!m_selectedSdfPaths.empty())
{
pxr::GfFrustum frustum = gfCamera.GetFrustum();
pxr::GfMatrix4d viewProj =
frustum.ComputeViewMatrix() * frustum.ComputeProjectionMatrix();
pxr::GfVec3d pivot = ComputeGizmoPivot();
pxr::GfVec3d camEye = frustum.GetPosition();
manipulator.Render(
ImGui::GetWindowDrawList(),
viewProj, pivot, camEye,
m_imageScreenPos,
m_viewWidth, m_viewHeight);
}
} else {
if (!m_stage)
ImGui::TextDisabled("No stage loaded");
else
ImGui::TextDisabled("Viewport too small (%dx%d)", m_viewWidth, m_viewHeight);
}
// -- Focus / hover border -------------------------------------------------
// Drawn as the LAST item inside the child window so it sits above the scene
// texture in the draw list sequence. Because it is inside the child window
// it is automatically clipped to this tile's bounds and cannot bleed over
// floating panels or menus positioned outside this tile.
// A 1 px inset ensures the full 2 px focused line stays within the clip rect.
{
ImVec2 bMin(pos.x + 1.f, pos.y + 1.f);
ImVec2 bMax(pos.x + size.x - 1.f, pos.y + size.y - 1.f);
ImDrawList* dl = ImGui::GetWindowDrawList();
if (isFocused) {
dl->AddRect(bMin, bMax, IM_COL32(66, 150, 250, 230), 0.f, 0, 2.0f);
} else if (m_wasHoveredThisFrame) {
dl->AddRect(bMin, bMax, IM_COL32(220, 220, 220, 110), 0.f, 0, 1.0f);
}
}
RenderContextMenu(tileIndex);
ImGui::EndChild();
if (m_showRendererSettings)
RenderRendererSettingsDialog(tileIndex);
}
// ---------------------------------------------------------------------------
// RenderRendererSettingsDialog
// ---------------------------------------------------------------------------
void ViewportTile::RenderRendererSettingsDialog(int tileIndex)
{
using namespace pxr;
std::string title = "Renderer Settings [" +
UsdSceneRenderer::GetRendererDisplayName(m_renderer.GetCurrentRendererId()) +
"]###RendererSettingsDlg_" + std::to_string(tileIndex);
ImGui::SetNextWindowSize(ImVec2(520, 460), ImGuiCond_FirstUseEver);
// WindowPadding is captured by Begin(); pop it immediately after.
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(12.f, 12.f));
bool open = ImGui::Begin(title.c_str(), &m_showRendererSettings,
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoDocking);
ImGui::PopStyleVar();
if (!open) {
ImGui::End();
return;
}
// ItemSpacing and FramePadding are read dynamically; push for content, pop before End.
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(8.f, 6.f));
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(6.f, 4.f));
// Cache the settings list per delegate — GetRendererSettingsList() calls TF_WARN
// for every unsupported setting type, so querying it every frame floods the log.
pxr::TfToken curId = m_renderer.GetCurrentRendererId();
if (curId != m_cachedSettingsRendererId) {
m_cachedRendererSettings = m_renderer.GetRendererSettingsList();
m_cachedSettingsRendererId = curId;
}
const auto& settings = m_cachedRendererSettings;
if (settings.empty()) {
ImVec2 sz = ImGui::GetContentRegionAvail();
ImGui::SetCursorPos(ImVec2(sz.x * 0.5f - 120.f, sz.y * 0.5f - 8.f));
ImGui::TextDisabled("This renderer exposes no settings.");
ImGui::PopStyleVar(2);
ImGui::End();
return;
}
// Group settings by namespace prefix (split key at first ':').
// Keys without ':' go into "General". std::map keeps alphabetical order.
std::map<std::string, std::vector<const UsdImagingGLRendererSetting*>> groups;
for (const auto& s : settings) {
std::string k = s.key.GetString();
auto colon = k.find(':');
std::string cat = (colon != std::string::npos) ? k.substr(0, colon) : "General";
if (!cat.empty()) cat[0] = static_cast<char>(toupper(static_cast<unsigned char>(cat[0])));
groups[cat].push_back(&s);
}
bool singleGroup = (groups.size() == 1);
bool tabBarOpen = false;
if (!singleGroup)
tabBarOpen = ImGui::BeginTabBar("##rdrsettings_tabs");
auto renderGroup = [&](const std::vector<const UsdImagingGLRendererSetting*>& items) {
float tableH = ImGui::GetContentRegionAvail().y - 40.f;
if (tableH < 20.f) tableH = 20.f;
if (ImGui::BeginTable("##srows", 2,
ImGuiTableFlags_SizingStretchProp | ImGuiTableFlags_BordersInnerH |
ImGuiTableFlags_ScrollY, ImVec2(0, tableH)))
{
ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthStretch, 0.55f);
ImGui::TableSetupColumn("Value", ImGuiTableColumnFlags_WidthStretch, 0.45f);
for (const auto* sp : items) {
const auto& s = *sp;
VtValue cur = m_renderer.GetRendererSetting(s.key);
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0);
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted(s.name.c_str());
ImGui::TableSetColumnIndex(1);
ImGui::PushItemWidth(-1.f);
std::string wid = "##" + s.key.GetString();
if (s.type == UsdImagingGLRendererSetting::TYPE_FLAG) {
bool v = cur.GetWithDefault<bool>(false);
if (ImGui::Checkbox(wid.c_str(), &v))
m_renderer.SetRendererSetting(s.key, VtValue(v));
} else if (s.type == UsdImagingGLRendererSetting::TYPE_INT) {
int v = cur.GetWithDefault<int>(0);
if (ImGui::DragInt(wid.c_str(), &v))
m_renderer.SetRendererSetting(s.key, VtValue(v));
} else if (s.type == UsdImagingGLRendererSetting::TYPE_FLOAT) {
float v = cur.GetWithDefault<float>(0.f);
if (ImGui::DragFloat(wid.c_str(), &v, 0.01f))
m_renderer.SetRendererSetting(s.key, VtValue(v));
} else if (s.type == UsdImagingGLRendererSetting::TYPE_STRING) {
std::string sv = cur.GetWithDefault<std::string>("");
char buf[256];
strncpy(buf, sv.c_str(), sizeof(buf) - 1);
buf[sizeof(buf) - 1] = '\0';
if (ImGui::InputText(wid.c_str(), buf, sizeof(buf)) &&
ImGui::IsItemDeactivatedAfterEdit())
m_renderer.SetRendererSetting(s.key, VtValue(std::string(buf)));
}
ImGui::PopItemWidth();
}
ImGui::EndTable();
}
};
if (singleGroup) {
renderGroup(groups.begin()->second);
} else if (tabBarOpen) {
// Non-"General" tabs first (alphabetical via std::map), then "General" last
for (const auto& [cat, items] : groups) {
if (cat == "General") continue;
if (ImGui::BeginTabItem(cat.c_str())) {
renderGroup(items);
ImGui::EndTabItem();
}
}
auto genIt = groups.find("General");
if (genIt != groups.end() && ImGui::BeginTabItem("General")) {
renderGroup(genIt->second);
ImGui::EndTabItem();
}
ImGui::EndTabBar();
}
ImGui::Separator();
ImGui::Spacing();
if (ImGui::Button("Reset Defaults")) {
for (const auto& s : settings)
m_renderer.SetRendererSetting(s.key, s.defValue);
}
ImGui::PopStyleVar(2);
ImGui::End();
}
} // namespace UsdLayerManager