From 3b0250b882157cbf1107b92425daff0156c47a1b Mon Sep 17 00:00:00 2001 From: indigo Date: Sat, 6 Jun 2026 09:34:39 +0800 Subject: [PATCH] Fix rotate issue --- src/core/commands/CreatePrimCommand.cpp | 43 ++++++++- src/ui/SceneHierarchyPanel.cpp | 12 +++ src/ui/TransformManipulator.cpp | 123 +++++++++++++++++++++++- 3 files changed, 174 insertions(+), 4 deletions(-) diff --git a/src/core/commands/CreatePrimCommand.cpp b/src/core/commands/CreatePrimCommand.cpp index af5907a..a2b9158 100644 --- a/src/core/commands/CreatePrimCommand.cpp +++ b/src/core/commands/CreatePrimCommand.cpp @@ -1,9 +1,42 @@ #include "CreatePrimCommand.h" #include "../../utils/Logger.h" #include +#include +#include +#include namespace UsdLayerManager { +// --------------------------------------------------------------------------- +// ApplyCameraDefaultOrientation +// +// USD cameras always use +Y-up / look-down-−Z in their own local frame. +// For a Y-up stage that is already the standard world forward direction, so +// the identity transform is fine. +// +// For a Z-up stage the identity transform would point the camera straight +// DOWN (world −Z = −up), which produces wrong orientation when the viewport +// switches to the prim, and wrong tumble/roll directions when navigating. +// +// Fix: rotate the camera +90° around its local X axis so that: +// camera local +Y (up) → world +Z (stage up axis) ✓ +// camera local −Z (view) → world +Y (horizontal "forward") ✓ +// --------------------------------------------------------------------------- +static void ApplyCameraDefaultOrientation(pxr::UsdPrim& prim, + pxr::UsdStageRefPtr stage) +{ + if (!prim.IsValid() || !stage) return; + + bool isZUp = (pxr::UsdGeomGetStageUpAxis(stage) == pxr::UsdGeomTokens->z); + if (!isZUp) return; // Y-up default (identity) is already correct + + pxr::UsdGeomXformCommonAPI xformAPI(prim); + // +90° around X: camera up aligns with world Z, view direction becomes +Y + xformAPI.SetRotate(pxr::GfVec3f(90.f, 0.f, 0.f), + pxr::UsdGeomXformCommonAPI::RotationOrderXYZ, + pxr::UsdTimeCode::Default()); +} + CreatePrimCommand::CreatePrimCommand(pxr::UsdStageRefPtr stage, const pxr::SdfPath& primPath, const pxr::TfToken& typeName) @@ -18,8 +51,16 @@ void CreatePrimCommand::Execute() { if (!m_stage) return; try { pxr::UsdPrim prim = m_stage->DefinePrim(m_primPath, m_typeName); - if (!prim.IsValid()) + if (!prim.IsValid()) { LOG_ERROR("CreatePrimCommand: failed to define prim " + m_primPath.GetString()); + return; + } + + // Camera-specific initialisation: orient the prim so the camera looks + // in the correct "forward" direction for the stage's up-axis. + if (m_typeName == pxr::TfToken("Camera")) + ApplyCameraDefaultOrientation(prim, m_stage); + } catch (const std::exception& e) { LOG_ERROR(std::string("CreatePrimCommand::Execute error: ") + e.what()); } diff --git a/src/ui/SceneHierarchyPanel.cpp b/src/ui/SceneHierarchyPanel.cpp index 6426aca..3c119aa 100644 --- a/src/ui/SceneHierarchyPanel.cpp +++ b/src/ui/SceneHierarchyPanel.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -202,6 +203,17 @@ void SceneHierarchyPanel::Render() { try { UsdPrim newPrim = m_stage->DefinePrim(primPath, TfToken(typeName)); if (newPrim.IsValid()) { + // Set correct camera orientation for the current up-axis + if (std::string(typeName) == "Camera") { + bool isZUp = (UsdGeomGetStageUpAxis(m_stage) == UsdGeomTokens->z); + if (isZUp) { + pxr::UsdGeomXformCommonAPI xformAPI(newPrim); + xformAPI.SetRotate( + pxr::GfVec3f(90.f, 0.f, 0.f), + pxr::UsdGeomXformCommonAPI::RotationOrderXYZ, + pxr::UsdTimeCode::Default()); + } + } LOG_INFO("Created prim '" + primPath.GetString() + "' of type " + baseName); SetSelectedPathFromClick(primPath.GetString()); } else { diff --git a/src/ui/TransformManipulator.cpp b/src/ui/TransformManipulator.cpp index edc58be..9b928ae 100644 --- a/src/ui/TransformManipulator.cpp +++ b/src/ui/TransformManipulator.cpp @@ -7,9 +7,13 @@ #include #include #include +#include #include #include #include +#include +#include +#include #include #include @@ -566,6 +570,14 @@ bool TransformManipulator::HandleInput(const pxr::GfMatrix4d& viewProj, consumed = true; if (ImGui::IsMouseDown(ImGuiMouseButton_Left)) { + // Defensive bounds check — m_dragAxis is always set to 0–2 at drag-start, + // but guard here to prevent OOB on axes[3] if the invariant is ever broken. + if (m_dragAxis < 0 || m_dragAxis > 2) { + m_isDragging = false; + m_dragAxis = -1; + return consumed; + } + ImVec2 delta = { mouse.x - m_dragLastPos.x, mouse.y - m_dragLastPos.y }; @@ -729,9 +741,114 @@ void TransformManipulator::ApplyRotateDelta(int axisIndex, float angleDeg) pxr::UsdEditContext ec(m_stage, m_stage->GetEditTarget()); pxr::UsdGeomXformCommonAPI api(prim); - m_dragStartRotate[axisIndex] += angleDeg; - api.SetRotate(m_dragStartRotate, - pxr::UsdGeomXformCommonAPI::RotationOrderXYZ, + // ── Correct, gimbal-lock-free rotation (mirrors usdtweak RotationManipulator) ── + // + // The previous approach manually reconstructed the rotation matrix from + // Euler angles using what turned out to be an incorrect axis-order + // convention (GfRotation quaternion multiplication reverses the matrix + // order relative to row-vector convention). + // + // The correct approach avoids all manual convention handling: + // + // Step 1 Use UsdGeomXformOp::GetOpTransform — USD builds the exact + // matrix that SetRotate would produce, with no guessing. + // + // Step 2 Build the world-space delta as a GfRotation around the + // gizmo axis and pre-multiply in parent space. + // + // Step 3 Decompose the result with GfRotation::DecomposeRotation + // passing the INITIAL rotation axes (from drag start) as the + // reference frame and the current angles as hints. Using the + // same fixed axes every frame (not the current frame's axes) + // keeps hint-based disambiguation stable — same technique as + // usdtweak's RotationManipulator::OnUpdate. + // ───────────────────────────────────────────────────────────────────── + + // Step 1: exact current rotation matrix via USD's own op encoding + pxr::UsdGeomXformOp::Type opType = + pxr::UsdGeomXformCommonAPI::ConvertRotationOrderToOpType(m_dragOriginalRotOrder); + pxr::GfMatrix4d curRotMat = + pxr::UsdGeomXformOp::GetOpTransform(opType, pxr::VtValue(m_dragStartRotate)); + + // Step 2: world-space delta rotation around the gizmo axis + pxr::GfVec3d axes[3]; + GetGizmoAxes(axes); + pxr::GfMatrix4d deltaWorld = + pxr::GfMatrix4d(1.0).SetRotate( + pxr::GfRotation(axes[axisIndex], static_cast(angleDeg))); + + // Convert to parent space: new_local = (P^-1 * deltaWorld * P) * curLocal + pxr::UsdGeomXformCache xformCache(pxr::UsdTimeCode::Default()); + pxr::UsdPrim parent = prim.GetParent(); + pxr::GfMatrix4d parentRotOnly(1.0); + if (parent && !parent.IsPseudoRoot()) { + pxr::GfMatrix4d p2w = xformCache.GetLocalToWorldTransform(parent); + parentRotOnly = pxr::GfMatrix4d(1.0).SetRotate(p2w.ExtractRotation()); + } + double det = 0.0; + pxr::GfMatrix4d parentRotInv = parentRotOnly.GetInverse(&det); + if (std::abs(det) < 1e-9) return; + + pxr::GfMatrix4d newRotMat = + curRotMat * parentRotOnly * deltaWorld * parentRotInv; + newRotMat.Orthonormalize(); + + // Step 3: decompose newRotMat (which is in local-to-parent space) back into + // Euler angles matching m_dragOriginalRotOrder. + // + // IMPORTANT: The reference axes for GfRotation::DecomposeRotation must be the + // canonical parent-space unit vectors in the order dictated by the rotation + // order — NOT rows of initRotMat (which are the rotated local axes in world + // space). Using rotated rows causes the decomposition to solve a completely + // different problem once any rotation has been applied, producing the random + // value jumps observed on the second axis drag. + // + // For each RotationOrder the Euler product is (row-vector convention): + // XYZ: R = Rx * Ry * Rz → axes in order (X,Y,Z) + // XZY: R = Rx * Rz * Ry → axes in order (X,Z,Y) + // YXZ: R = Ry * Rx * Rz → axes in order (Y,X,Z) + // YZX: R = Ry * Rz * Rx → axes in order (Y,Z,X) + // ZXY: R = Rz * Rx * Ry → axes in order (Z,X,Y) + // ZYX: R = Rz * Ry * Rx → axes in order (Z,Y,X) + // + // The hint angles must be ordered the same way (tw, fb, lr). + + // Map rotation order → (twIdx, fbIdx, lrIdx) where 0=X,1=Y,2=Z + int twIdx = 0, fbIdx = 1, lrIdx = 2; + switch (m_dragOriginalRotOrder) { + case pxr::UsdGeomXformCommonAPI::RotationOrderXYZ: twIdx=0; fbIdx=1; lrIdx=2; break; + case pxr::UsdGeomXformCommonAPI::RotationOrderXZY: twIdx=0; fbIdx=2; lrIdx=1; break; + case pxr::UsdGeomXformCommonAPI::RotationOrderYXZ: twIdx=1; fbIdx=0; lrIdx=2; break; + case pxr::UsdGeomXformCommonAPI::RotationOrderYZX: twIdx=1; fbIdx=2; lrIdx=0; break; + case pxr::UsdGeomXformCommonAPI::RotationOrderZXY: twIdx=2; fbIdx=0; lrIdx=1; break; + case pxr::UsdGeomXformCommonAPI::RotationOrderZYX: twIdx=2; fbIdx=1; lrIdx=0; break; + default: break; + } + + static const pxr::GfVec3d kUnitAxes[3] = { + {1.0, 0.0, 0.0}, {0.0, 1.0, 0.0}, {0.0, 0.0, 1.0} + }; + const pxr::GfVec3d& twAxis = kUnitAxes[twIdx]; + const pxr::GfVec3d& fbAxis = kUnitAxes[fbIdx]; + const pxr::GfVec3d& lrAxis = kUnitAxes[lrIdx]; + + // Hint angles in tw/fb/lr order from current accumulated state + double thetaTw = pxr::GfDegreesToRadians(double(m_dragStartRotate[twIdx])); + double thetaFB = pxr::GfDegreesToRadians(double(m_dragStartRotate[fbIdx])); + double thetaLR = pxr::GfDegreesToRadians(double(m_dragStartRotate[lrIdx])); + double thetaSw = 0.0; + + pxr::GfRotation::DecomposeRotation( + newRotMat, twAxis, fbAxis, lrAxis, /*handedness=*/1.0, + &thetaTw, &thetaFB, &thetaLR, &thetaSw, /*useHint=*/true); + + // Write results back into the correct component slots + pxr::GfVec3f newAngles = m_dragStartRotate; + newAngles[twIdx] = float(pxr::GfRadiansToDegrees(thetaTw)); + newAngles[fbIdx] = float(pxr::GfRadiansToDegrees(thetaFB)); + newAngles[lrIdx] = float(pxr::GfRadiansToDegrees(thetaLR)); + m_dragStartRotate = newAngles; + api.SetRotate(m_dragStartRotate, m_dragOriginalRotOrder, pxr::UsdTimeCode::Default()); }