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:
2026-06-16 07:28:28 +08:00
parent ee1b750f8c
commit 7d53eb18d4
3 changed files with 62 additions and 33 deletions
+31 -33
View File
@@ -121,10 +121,13 @@ void ViewportTile::InitCameraNavigation()
pxr::GfCamera gfCam = usdCam.GetCamera(m_displayTime);
m_camera.SwitchToFreeCamera(&gfCam);
// PullFromCameraTransform() sets m_dist from the USD camera's focus-distance
// attribute, which is a depth-of-field parameter (default 5 scene units) —
// not the distance to scene content. Override both the orbit center and
// distance using the scene bounding box so tumbling pivots on visible geometry.
// 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 };
@@ -136,14 +139,12 @@ void ViewportTile::InitCameraNavigation()
pxr::GfVec3d camEye = gfCam.GetFrustum().GetPosition();
pxr::GfVec3d viewDir = gfCam.GetFrustum().ComputeViewDirection();
// Project the scene centre onto the view ray to get the orbit
// distance; this keeps the camera at its current position while
// placing the pivot at the closest on-axis approach to the scene.
// 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.SetFocalPoint(camEye + dist * viewDir);
m_camera.SetDist(dist);
}
if (dist > 0.01)
m_camera.InitOrbitFromEyeAndCenter(
camEye, camEye + dist * viewDir, dist);
}
}
@@ -308,28 +309,25 @@ pxr::GfCamera ViewportTile::ResolveCamera()
if (m_isDrivingUsdCamPrim) {
pxr::UsdPrim prim = m_stage->GetPrimAtPath(m_drivenUsdCamPath);
if (prim && prim.IsA<pxr::UsdGeomCamera>()) {
pxr::UsdGeomXformCommonAPI xformAPI(prim);
{
pxr::GfVec3d t; pxr::GfVec3f r, s, pv;
pxr::UsdGeomXformCommonAPI::RotationOrder ro;
if (!xformAPI.GetXformVectors(
&t, &r, &s, &pv, &ro, m_displayTime))
pxr::UsdGeomXformable(prim).ClearXformOpOrder();
}
pxr::GfMatrix4d camToWorld = gfCamera.GetTransform();
pxr::GfVec3d translate = camToWorld.ExtractTranslation();
pxr::GfRotation rotation = camToWorld.ExtractRotation();
pxr::GfVec3d eulerDeg = rotation.Decompose(
pxr::GfVec3d::XAxis(),
pxr::GfVec3d::YAxis(),
pxr::GfVec3d::ZAxis());
xformAPI.SetTranslate(translate, m_editTime);
xformAPI.SetRotate(
pxr::GfVec3f(float(eulerDeg[0]),
float(eulerDeg[1]),
float(eulerDeg[2])),
pxr::UsdGeomXformCommonAPI::RotationOrderXYZ,
m_editTime);
// 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;