Fix rotate issue

This commit is contained in:
indigo 2026-06-06 09:34:39 +08:00
parent d4b46b438c
commit 3b0250b882
3 changed files with 174 additions and 4 deletions

View File

@ -1,9 +1,42 @@
#include "CreatePrimCommand.h" #include "CreatePrimCommand.h"
#include "../../utils/Logger.h" #include "../../utils/Logger.h"
#include <pxr/usd/usd/prim.h> #include <pxr/usd/usd/prim.h>
#include <pxr/usd/usdGeom/xformCommonAPI.h>
#include <pxr/usd/usdGeom/metrics.h>
#include <pxr/usd/usdGeom/tokens.h>
namespace UsdLayerManager { 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, CreatePrimCommand::CreatePrimCommand(pxr::UsdStageRefPtr stage,
const pxr::SdfPath& primPath, const pxr::SdfPath& primPath,
const pxr::TfToken& typeName) const pxr::TfToken& typeName)
@ -18,8 +51,16 @@ void CreatePrimCommand::Execute() {
if (!m_stage) return; if (!m_stage) return;
try { try {
pxr::UsdPrim prim = m_stage->DefinePrim(m_primPath, m_typeName); 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()); 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) { } catch (const std::exception& e) {
LOG_ERROR(std::string("CreatePrimCommand::Execute error: ") + e.what()); LOG_ERROR(std::string("CreatePrimCommand::Execute error: ") + e.what());
} }

View File

@ -13,6 +13,7 @@
#include <pxr/usd/usdGeom/metrics.h> #include <pxr/usd/usdGeom/metrics.h>
#include <pxr/usd/usdGeom/tokens.h> #include <pxr/usd/usdGeom/tokens.h>
#include <pxr/usd/usdGeom/xform.h> #include <pxr/usd/usdGeom/xform.h>
#include <pxr/usd/usdGeom/xformCommonAPI.h>
#include <pxr/usd/sdf/layer.h> #include <pxr/usd/sdf/layer.h>
#include <pxr/usd/sdf/reference.h> #include <pxr/usd/sdf/reference.h>
#include <pxr/usd/sdf/primSpec.h> #include <pxr/usd/sdf/primSpec.h>
@ -202,6 +203,17 @@ void SceneHierarchyPanel::Render() {
try { try {
UsdPrim newPrim = m_stage->DefinePrim(primPath, TfToken(typeName)); UsdPrim newPrim = m_stage->DefinePrim(primPath, TfToken(typeName));
if (newPrim.IsValid()) { 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); LOG_INFO("Created prim '" + primPath.GetString() + "' of type " + baseName);
SetSelectedPathFromClick(primPath.GetString()); SetSelectedPathFromClick(primPath.GetString());
} else { } else {

View File

@ -7,9 +7,13 @@
#include <pxr/usd/usd/editContext.h> #include <pxr/usd/usd/editContext.h>
#include <pxr/usd/usdGeom/xformCommonAPI.h> #include <pxr/usd/usdGeom/xformCommonAPI.h>
#include <pxr/usd/usdGeom/xformCache.h> #include <pxr/usd/usdGeom/xformCache.h>
#include <pxr/usd/usdGeom/xformOp.h>
#include <pxr/base/gf/matrix4d.h> #include <pxr/base/gf/matrix4d.h>
#include <pxr/base/gf/matrix4f.h> #include <pxr/base/gf/matrix4f.h>
#include <pxr/base/gf/vec3d.h> #include <pxr/base/gf/vec3d.h>
#include <pxr/base/gf/rotation.h>
#include <pxr/base/gf/math.h>
#include <pxr/base/vt/value.h>
#include <cmath> #include <cmath>
#include <algorithm> #include <algorithm>
@ -566,6 +570,14 @@ bool TransformManipulator::HandleInput(const pxr::GfMatrix4d& viewProj,
consumed = true; consumed = true;
if (ImGui::IsMouseDown(ImGuiMouseButton_Left)) { if (ImGui::IsMouseDown(ImGuiMouseButton_Left)) {
// Defensive bounds check — m_dragAxis is always set to 02 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, ImVec2 delta = { mouse.x - m_dragLastPos.x,
mouse.y - m_dragLastPos.y }; 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::UsdEditContext ec(m_stage, m_stage->GetEditTarget());
pxr::UsdGeomXformCommonAPI api(prim); pxr::UsdGeomXformCommonAPI api(prim);
m_dragStartRotate[axisIndex] += angleDeg; // ── Correct, gimbal-lock-free rotation (mirrors usdtweak RotationManipulator) ──
api.SetRotate(m_dragStartRotate, //
pxr::UsdGeomXformCommonAPI::RotationOrderXYZ, // 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<double>(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()); pxr::UsdTimeCode::Default());
} }