Files
UsdLayerManager/src/core/ViewportCamera.h
T
indigo 7d53eb18d4 Fix USD camera tumble: lossless matrix write-back + gimbal-free orbit init
Two issues caused the camera orientation to flip when tumbling a custom
USD camera:

1. Write-back decomposed the camera matrix to XYZ-euler via
   Decompose(X,Y,Z) and re-authored it as a rotateXYZ op. That round-trip
   is lossy — the angles Decompose returns do not reconstruct the same
   matrix as a rotateXYZ op, so the orientation read back from the prim
   differed from the free-camera view shown during the drag. The view
   jumped every time a drag finished and snapped back on the next drag.
   Now author the full camera-to-world transform as a single matrix op,
   which round-trips exactly through UsdGeomCamera::GetCamera().

2. Orbit init relied on PullFromCameraTransform's Euler decomposition for
   theta/phi, which is gimbal-affected. Added
   ViewportCamera::InitOrbitFromEyeAndCenter to derive theta/phi directly
   from the eye->center vector (zero roll), respecting the Z-up matrix.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 07:28:28 +08:00

192 lines
7.8 KiB
C++

#pragma once
#include <pxr/base/gf/camera.h>
#include <pxr/base/gf/matrix4d.h>
#include <pxr/base/gf/vec3d.h>
#include <pxr/base/gf/bbox3d.h>
#include <pxr/base/gf/range3d.h>
#include <pxr/base/gf/ray.h>
#include <pxr/usd/sdf/path.h>
#include <pxr/usd/usd/stage.h>
#include <utility>
namespace UsdLayerManager {
/// Free camera ported from Pixar's usdview FreeCamera (freeCamera.py).
/// Supports Tumble/Truck/AdjustDistance with Z-up stage handling,
/// automatic near/far clipping plane computation from scene bbox,
/// and USD prim-camera passthrough mode.
class ViewportCamera {
public:
enum class CameraMode { Free, UsdCamera };
static constexpr double kDefaultNear = 1.0;
static constexpr double kDefaultFar = 2000000.0;
static constexpr double kMaxSafeZResolution = 1e6;
static constexpr double kMaxGoodZResolution = 5e4;
ViewportCamera();
// -----------------------------------------------------------------------
// Stage (sets Z-up flag and YZUp matrices)
// -----------------------------------------------------------------------
void SetStage(pxr::UsdStageRefPtr stage);
// -----------------------------------------------------------------------
// Free Camera Operations (mirrors FreeCamera.py)
// -----------------------------------------------------------------------
/// Orbit (tumble) around center by dTheta (horizontal) and dPhi (vertical) degrees.
void Tumble(double dTheta, double dPhi);
/// Scale distance from center by scaleFactor. Prevents getting stuck near zero.
void AdjustDistance(double scaleFactor);
/// Pan (truck) in camera-local right/up directions, world-unit deltas.
void Truck(double deltaRight, double deltaUp);
/// Returns pixels-to-world factor for correct Truck() scaling.
double ComputePixelsToWorldFactor(double viewportHeight);
/// Frame a bounding box. frameFit=1.1 gives ~10% margin (usdview default).
void FrameSelection(const pxr::GfBBox3d& selBBox, double frameFit = 1.1);
// -----------------------------------------------------------------------
// Camera Resolution: returns GfCamera with updated transform + clipping
// -----------------------------------------------------------------------
/// Returns the GfCamera with up-to-date transform.
/// If autoClip=true, near/far are computed from stageBBox for best precision.
pxr::GfCamera ComputeGfCamera(const pxr::GfBBox3d& stageBBox,
bool autoClip = false);
// -----------------------------------------------------------------------
// Convenience Matrix Accessors
// -----------------------------------------------------------------------
pxr::GfMatrix4d GetViewMatrix();
pxr::GfMatrix4d GetProjectionMatrix();
// -----------------------------------------------------------------------
// Camera Mode
// -----------------------------------------------------------------------
CameraMode GetMode() const { return m_mode; }
const pxr::SdfPath& GetUsdCameraPath() const { return m_usdCameraPath; }
/// Switch to USD prim camera mode (renderer will call SetCameraPath).
void SetUsdCamera(const pxr::SdfPath& cameraPath);
/// Switch back to free camera, optionally initializing state from lastGfCamera.
void SwitchToFreeCamera(const pxr::GfCamera* lastGfCamera = nullptr);
// -----------------------------------------------------------------------
// Camera Settings
// -----------------------------------------------------------------------
double GetFOV() const; ///< Vertical FOV in degrees
void SetFOV(double fov);
double GetAspectRatio() const;
void SetAspectRatio(double aspect);
/// Hint for autoClip: the closest visible geometry point from pick result.
void SetClosestVisibleDistFromPoint(const pxr::GfVec3d& point);
// -----------------------------------------------------------------------
// State Queries
// -----------------------------------------------------------------------
double GetDist() const { return m_dist; }
bool IsZUp() const { return m_isZUp; }
// -----------------------------------------------------------------------
// Compatibility accessors (for tests and legacy callers)
// -----------------------------------------------------------------------
/// Returns camera position in world space.
pxr::GfVec3d GetEye();
/// Returns the look-at focal point (center of orbit).
const pxr::GfVec3d& GetFocalPoint() const { return m_center; }
/// Set the look-at focal point directly.
/// Used by orthographic view panning to move the center in correct
/// view-space directions without going through the perspective Truck() path.
void SetFocalPoint(const pxr::GfVec3d& center) {
m_center = center;
m_cameraTransformDirty = true;
}
/// Set the orbit distance directly.
/// Used when initialising from a USD camera prim to override the camera's
/// authored focus distance (which is a DOF attribute, not an orbit radius).
void SetDist(double d) {
m_dist = d;
m_cameraTransformDirty = true;
}
/// Set orbital state from eye position and orbit center without Euler decomposition.
/// Computes theta/phi directly from the eye→center vector in the Y-up orbital frame;
/// zeroes roll. Use this after SwitchToFreeCamera() when initialising from a USD
/// camera prim to avoid gimbal-lock artifacts from the Decompose() path.
void InitOrbitFromEyeAndCenter(const pxr::GfVec3d& eye,
const pxr::GfVec3d& center,
double dist);
/// Returns the current near-clip distance.
double GetNearClip() const {
return static_cast<double>(m_camera.GetClippingRange().GetMin());
}
/// Returns the current far-clip distance.
double GetFarClip() const {
return static_cast<double>(m_camera.GetClippingRange().GetMax());
}
/// Legacy: frame a GfRange3d (wraps it in a unit-matrix GfBBox3d).
void FrameBoundingBox(const pxr::GfRange3d& range, double frameFit = 1.1) {
FrameSelection(pxr::GfBBox3d(range), frameFit);
}
/// Legacy: orbit alias (matches old Orbit(deltaYaw, deltaPitch) signature).
void Orbit(double deltaYaw, double deltaPitch) {
Tumble(deltaYaw, deltaPitch);
}
private:
void PushToCameraTransform();
void PullFromCameraTransform();
void SetClippingPlanes(const pxr::GfBBox3d& stageBBox);
void ResetClippingPlanes();
std::pair<double,double> RangeOfBoxAlongRay(
const pxr::GfRay& camRay, const pxr::GfBBox3d& bbox) const;
// Core camera object (owns aperture/projection/clipping/transform state)
pxr::GfCamera m_camera;
bool m_cameraTransformDirty;
// Orbital / tumble state
double m_rotTheta; // horizontal orbit (degrees, around Y)
double m_rotPhi; // vertical orbit (degrees, around X)
double m_rotPsi; // roll (degrees, usually 0)
pxr::GfVec3d m_center; // look-at center in world space
double m_dist; // distance camera → center
double m_selSize; // extent of last framed selection
// Stage up-axis handling
bool m_isZUp;
pxr::GfMatrix4d m_YZUpMatrix; // Y-up → Z-up (rotate -90° around X)
pxr::GfMatrix4d m_YZUpInvMatrix; // Z-up → Y-up (inverse)
// Auto-clip state
bool m_hasClosestVisibleDist;
double m_closestVisibleDist;
double m_lastFramedDist;
double m_lastFramedClosestDist;
double m_overrideNear; // ≤0 means "no override"
double m_overrideFar; // ≤0 means "no override"
// Camera mode
CameraMode m_mode;
pxr::SdfPath m_usdCameraPath;
pxr::UsdStageRefPtr m_stage;
};
} // namespace UsdLayerManager