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>
This commit is contained in:
@@ -335,6 +335,29 @@ void ViewportCamera::SetUsdCamera(const pxr::SdfPath& cameraPath)
|
||||
m_usdCameraPath = cameraPath;
|
||||
}
|
||||
|
||||
void ViewportCamera::InitOrbitFromEyeAndCenter(
|
||||
const pxr::GfVec3d& eye, const pxr::GfVec3d& center, double dist)
|
||||
{
|
||||
m_center = center;
|
||||
m_dist = std::max(dist, 0.001);
|
||||
m_selSize = m_dist / 10.0;
|
||||
|
||||
// Transform the eye-to-center offset into the Y-up orbital frame.
|
||||
// For Y-up stages m_YZUpMatrix is identity; for Z-up it's -90° around X.
|
||||
pxr::GfVec3d offset = m_YZUpMatrix.TransformDir(eye - center);
|
||||
double len = offset.GetLength();
|
||||
if (len > 1e-6) {
|
||||
offset /= len;
|
||||
// In the Y-up orbital frame PushToCameraTransform places the eye at:
|
||||
// (-sin(theta)*cos(phi), sin(phi), cos(theta)*cos(phi))
|
||||
m_rotPhi = std::asin(std::max(-1.0, std::min(1.0, offset[1])))
|
||||
* (180.0 / M_PI);
|
||||
m_rotTheta = std::atan2(-offset[0], offset[2]) * (180.0 / M_PI);
|
||||
}
|
||||
m_rotPsi = 0.0; // zero roll — avoids gimbal weirdness on switch
|
||||
m_cameraTransformDirty = true;
|
||||
}
|
||||
|
||||
void ViewportCamera::SwitchToFreeCamera(const pxr::GfCamera* lastGfCamera)
|
||||
{
|
||||
if (m_mode == CameraMode::Free) return;
|
||||
|
||||
@@ -121,6 +121,14 @@ public:
|
||||
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());
|
||||
|
||||
Reference in New Issue
Block a user