Fix rotate manipulator axis and gizmo pivot placement
Rotate manipulator — correct axis bug: - Object-space delta axis was set to initRot.GetRow3(axis) (a parent-space vector) then applied in the prim's local frame via deltaM * initRot. These frames are mismatched: the axis must be the canonical e_a so that Rot(e_a) fixes e_a and the prim rotates purely about its own axis. Fixed to use the canonical axis for both spaces; only composition order differs (deltaM * initRot for object, initRot * deltaM for world). Gizmo pivot placement: - ComputeGizmoPivot() used the bounding-box centre (geometry centroid), placing the gizmo at the wrong location for any non-centred prim. Replaced with parentToWorld.Transform(translate + pivot) from XformCommonAPI::GetXformVectors — the authored pivot point in world space. Fallback for incompatible op stacks uses worldMatrix.ExtractTranslation(). Property panel: - Show pivot (X/Y/Z) as read-only row below Scale, populated from XformCommonAPI::GetXformVectors each frame. - Removed active-rotate-axis display (was added and then removed per request). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
#include "Application.h"
|
||||
#include "Application.h"
|
||||
#include "../utils/Logger.h"
|
||||
#include "../utils/FileDialog.h"
|
||||
#include "../utils/PathUtils.h"
|
||||
|
||||
@@ -116,6 +116,7 @@ void PropertyPanel::ReadTransform() {
|
||||
static_cast<float>(translation[2]));
|
||||
m_rotate = rotation;
|
||||
m_scale = scale;
|
||||
m_pivot = pivot;
|
||||
m_rotOrder = rotOrder;
|
||||
}
|
||||
else
|
||||
@@ -163,6 +164,7 @@ void PropertyPanel::ReadTransform() {
|
||||
static_cast<float>(eulerDeg[1]),
|
||||
static_cast<float>(eulerDeg[2]));
|
||||
m_rotOrder = UsdGeomXformCommonAPI::RotationOrderXYZ;
|
||||
m_pivot = GfVec3f(0.f, 0.f, 0.f);
|
||||
|
||||
m_xformFallback = true;
|
||||
}
|
||||
@@ -500,6 +502,23 @@ void PropertyPanel::RenderTransformSection() {
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Pivot row (read-only) ----
|
||||
ImGui::TableNextRow();
|
||||
ImGui::TableSetColumnIndex(0);
|
||||
ImGui::TextColored(kColorLabel, "Pivot");
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
ImGui::TableSetColumnIndex(i + 1);
|
||||
ImGui::SetNextItemWidth(-FLT_MIN);
|
||||
// Use InputFloat with ReadOnly so the value is visible but not editable.
|
||||
// Pivot changes require a dedicated pivot-editing mode (not yet supported).
|
||||
char id[8]; snprintf(id, sizeof(id), "##pv%d", i);
|
||||
float v = m_pivot[i];
|
||||
ImGui::PushStyleColor(ImGuiCol_FrameBg,
|
||||
ImVec4(0.15f, 0.15f, 0.15f, 1.f)); // dimmer background signals read-only
|
||||
ImGui::InputFloat(id, &v, 0.f, 0.f, "%.3f", ImGuiInputTextFlags_ReadOnly);
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
|
||||
ImGui::EndTable();
|
||||
|
||||
// Record whether any field was active this frame so Render() can decide
|
||||
|
||||
@@ -59,10 +59,11 @@ private:
|
||||
pxr::UsdStageRefPtr m_stage;
|
||||
std::string m_selectedPrimPath;
|
||||
|
||||
// Cached TRS values (float matches DragFloat precision)
|
||||
// Cached TRS + pivot values (float matches DragFloat precision)
|
||||
pxr::GfVec3f m_translate{ 0.f, 0.f, 0.f };
|
||||
pxr::GfVec3f m_rotate { 0.f, 0.f, 0.f };
|
||||
pxr::GfVec3f m_scale { 1.f, 1.f, 1.f };
|
||||
pxr::GfVec3f m_pivot { 0.f, 0.f, 0.f };
|
||||
pxr::UsdGeomXformCommonAPI::RotationOrder
|
||||
m_rotOrder{ pxr::UsdGeomXformCommonAPI::RotationOrderXYZ };
|
||||
|
||||
|
||||
@@ -403,13 +403,11 @@ void TransformManipulator::Render(ImDrawList* dl, const pxr::GfMatrix4d& vp,
|
||||
// Clock-hand: intersect mouse ray with ring plane (GfPlane) -> rotateTo vector.
|
||||
// worldRotation = GfRotation(dragStartClockHand, currentClockHand).
|
||||
// axisSign = dot(planeNormal, worldRotation.GetAxis()) > 0 ? 1 : -1.
|
||||
// localDeltaAxis:
|
||||
// Object => initRot.GetRow3(dragAxis) [usdtweak exact: local axis in parent space]
|
||||
// World => world unit vector
|
||||
// deltaRotation = GfRotation(localDeltaAxis * axisSign, angle).
|
||||
// deltaAxis = canonical axis e_a for the dragged ring (both spaces).
|
||||
// deltaRotation = GfRotation(deltaAxis * axisSign, angle).
|
||||
// resultingRotation:
|
||||
// Object => GfMatrix4d(1).SetRotate(delta) * initRot [usdtweak exact]
|
||||
// World => initRot * GfMatrix4d(1).SetRotate(delta) [USD row-vector world delta]
|
||||
// Object => GfMatrix4d(1).SetRotate(delta) * initRot [delta in prim local frame]
|
||||
// World => initRot * GfMatrix4d(1).SetRotate(delta) [delta about world axis]
|
||||
// DecomposeRotation with FIXED initRot rows as reference axes + current hints.
|
||||
//
|
||||
// SCALE (ScaleManipulator::OnUpdate)
|
||||
@@ -494,16 +492,24 @@ bool TransformManipulator::HandleInput(const pxr::GfMatrix4d& vp,
|
||||
pxr::UsdGeomXformOp::GetOpTransform(opType,pxr::VtValue(rot));
|
||||
|
||||
// Capture delta axis and space mode at drag start.
|
||||
// Object: initRot.GetRow3(axis) -- local axis in parent space.
|
||||
// World : world unit vector.
|
||||
if (m_transformSpace==TransformSpace::Object) {
|
||||
m_dragRotateDeltaAxis=m_dragRotateInitialRotMat.GetRow3(m_dragAxis);
|
||||
double n=m_dragRotateDeltaAxis.GetLength();
|
||||
if (n>1e-9) m_dragRotateDeltaAxis/=n;
|
||||
} else {
|
||||
static const pxr::GfVec3d kW[3]={{1,0,0},{0,1,0},{0,0,1}};
|
||||
m_dragRotateDeltaAxis=kW[m_dragAxis];
|
||||
}
|
||||
//
|
||||
// The delta axis is the CANONICAL axis e_a for the dragged ring
|
||||
// in BOTH spaces; only the composition order (below) differs.
|
||||
//
|
||||
// Object: e_a applied in the prim's own local frame via
|
||||
// deltaM * initRot. Because Rot(e_a) fixes e_a, the
|
||||
// prim's current local axis (e_a * initRot) stays put,
|
||||
// so the object rotates purely about its own axis
|
||||
// (Maya local mode).
|
||||
// World : e_a applied after initRot via initRot * deltaM, i.e.
|
||||
// rotation about the fixed world axis.
|
||||
//
|
||||
// NOTE: using initRot.GetRow3(axis) (a parent-space vector) with
|
||||
// deltaM * initRot is WRONG -- the axis is then applied in the
|
||||
// local frame it does not belong to, which makes the object
|
||||
// tumble once it already carries a rotation.
|
||||
static const pxr::GfVec3d kAxis[3]={{1,0,0},{0,1,0},{0,0,1}};
|
||||
m_dragRotateDeltaAxis=kAxis[m_dragAxis];
|
||||
m_dragRotateObjectSpace=(m_transformSpace==TransformSpace::Object);
|
||||
}
|
||||
else if (m_mode==ManipulatorMode::Scale) {
|
||||
@@ -584,13 +590,14 @@ bool TransformManipulator::HandleInput(const pxr::GfMatrix4d& vp,
|
||||
?1.0:-1.0;
|
||||
|
||||
// usdtweak: const GfRotation deltaRotation(localPlaneNormal*axisSign, angle)
|
||||
// deltaAxis is the canonical axis e_a for the dragged ring.
|
||||
pxr::GfRotation deltaRot(m_dragRotateDeltaAxis*axisSign,
|
||||
worldRotation.GetAngle());
|
||||
pxr::GfMatrix4d deltaM=pxr::GfMatrix4d(1.0).SetRotate(deltaRot);
|
||||
|
||||
// usdtweak: resultingRotation = GfMatrix4d(1).SetRotate(delta)*initRot
|
||||
// Object (usdtweak exact): deltaM * initRot
|
||||
// World (USD row-vector) : initRot * deltaM
|
||||
// resultingRotation:
|
||||
// Object (rotate about prim's own axis): deltaM * initRot
|
||||
// World (rotate about fixed world axis): initRot * deltaM
|
||||
pxr::GfMatrix4d result=
|
||||
m_dragRotateObjectSpace
|
||||
? deltaM * m_dragRotateInitialRotMat
|
||||
|
||||
@@ -108,19 +108,15 @@ private:
|
||||
//
|
||||
// Ring plane: world-space ring normal (for mouse-ray intersection + sign).
|
||||
//
|
||||
// Delta-rotation axis differs by TransformSpace:
|
||||
// Object => initRot.GetRow3(axis) -- local axis in PARENT space
|
||||
// (exact usdtweak localPlaneNormal convention)
|
||||
// World => world unit vector
|
||||
// (correct for root-level / simple hierarchies)
|
||||
//
|
||||
// resultingRotation formula:
|
||||
// Object => GfMatrix4d(1).SetRotate(delta) * initRot
|
||||
// (usdtweak: delta applied in local frame before initRot)
|
||||
// World => initRot * GfMatrix4d(1).SetRotate(delta)
|
||||
// (USD row-vector: initRot maps local->parent, then world delta)
|
||||
// Delta-rotation axis is the canonical axis e_a for the dragged ring in
|
||||
// BOTH spaces; only the composition order differs:
|
||||
// Object => SetRotate(delta) * initRot
|
||||
// delta applied in the prim's own local frame, so the dragged
|
||||
// local axis stays fixed -> rotates purely about its own axis.
|
||||
// World => initRot * SetRotate(delta)
|
||||
// initRot maps local->parent, then delta about the world axis.
|
||||
pxr::GfVec3d m_dragRotatePlaneNormal; // world-space
|
||||
pxr::GfVec3d m_dragRotateDeltaAxis; // space-dependent (see above)
|
||||
pxr::GfVec3d m_dragRotateDeltaAxis; // canonical axis e_a for ring
|
||||
pxr::GfVec3d m_dragRotateFrom; // clock-hand at drag start
|
||||
pxr::GfMatrix4d m_dragRotateInitialRotMat; // local->parent rot at drag start
|
||||
bool m_dragRotateObjectSpace = true;
|
||||
|
||||
@@ -65,6 +65,7 @@ public:
|
||||
LayoutMode GetLayout() const { return m_layout; }
|
||||
int GetFocusedTileIndex() const { return m_focusedTileIndex; }
|
||||
|
||||
|
||||
private:
|
||||
// ── Tile rect helper ─────────────────────────────────────────────────────
|
||||
struct TileRect { ImVec2 pos; ImVec2 size; };
|
||||
|
||||
+25
-9
@@ -130,16 +130,32 @@ pxr::GfVec3d ViewportTile::ComputeGizmoPivot() const
|
||||
pxr::UsdPrim prim = m_stage->GetPrimAtPath(m_selectedSdfPaths.front());
|
||||
if (!prim) return pxr::GfVec3d(0.0);
|
||||
|
||||
pxr::TfTokenVector purposes = {
|
||||
pxr::UsdGeomTokens->default_, pxr::UsdGeomTokens->proxy };
|
||||
pxr::UsdGeomBBoxCache bboxCache(
|
||||
m_displayTime, purposes, true);
|
||||
pxr::GfBBox3d bbox = bboxCache.ComputeWorldBound(prim);
|
||||
pxr::GfRange3d range = bbox.ComputeAlignedRange();
|
||||
if (!range.IsEmpty())
|
||||
return (range.GetMin() + range.GetMax()) * 0.5;
|
||||
|
||||
pxr::UsdGeomXformCache xformCache(m_displayTime);
|
||||
|
||||
// Primary: use XformCommonAPI to read the authored translate and pivot op.
|
||||
// The manipulator must sit at the pivot's world-space position:
|
||||
// worldPivot = parentToWorld.Transform(translate + pivot)
|
||||
// NOT at the bounding-box center (geometry centroid) and NOT at
|
||||
// worldMatrix.ExtractTranslation() which folds in the pivot-inverse op
|
||||
// and gives the geometry origin, not the authored pivot point.
|
||||
pxr::UsdGeomXformCommonAPI api(prim);
|
||||
pxr::GfVec3d translate;
|
||||
pxr::GfVec3f rotation, scale, pivot;
|
||||
pxr::UsdGeomXformCommonAPI::RotationOrder rotOrder;
|
||||
if (api.GetXformVectors(&translate, &rotation, &scale, &pivot,
|
||||
&rotOrder, m_displayTime))
|
||||
{
|
||||
pxr::GfVec3d localPivot = translate + pxr::GfVec3d(pivot[0], pivot[1], pivot[2]);
|
||||
pxr::UsdPrim parent = prim.GetParent();
|
||||
if (parent && !parent.IsPseudoRoot()) {
|
||||
pxr::GfMatrix4d p2w = xformCache.GetLocalToWorldTransform(parent);
|
||||
return p2w.Transform(localPivot);
|
||||
}
|
||||
return localPivot; // root-level prim: parent space == world space
|
||||
}
|
||||
|
||||
// Fallback for incompatible op stacks (e.g. xformOp:transform matrix ops):
|
||||
// use the prim's world-space origin from the local-to-world matrix.
|
||||
pxr::GfMatrix4d worldXform = xformCache.GetLocalToWorldTransform(prim);
|
||||
return worldXform.ExtractTranslation();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user