From c8f79dfb0fbec0f5d576e670a2c6a7d240a83f2e Mon Sep 17 00:00:00 2001 From: ssdfasd <2156608475@qq.com> Date: Mon, 30 Mar 2026 00:47:31 +0800 Subject: [PATCH] Fix scene move gizmo axis projection --- .../src/Viewport/SceneViewportMoveGizmo.cpp | 143 ++++++++++++++++-- .../editor/test_scene_viewport_move_gizmo.cpp | 47 ++++++ 2 files changed, 175 insertions(+), 15 deletions(-) diff --git a/editor/src/Viewport/SceneViewportMoveGizmo.cpp b/editor/src/Viewport/SceneViewportMoveGizmo.cpp index aac3fb4e..8f702252 100644 --- a/editor/src/Viewport/SceneViewportMoveGizmo.cpp +++ b/editor/src/Viewport/SceneViewportMoveGizmo.cpp @@ -5,6 +5,9 @@ #include "SceneViewportPicker.h" #include +#include +#include +#include namespace XCEngine { namespace Editor { @@ -31,17 +34,21 @@ Math::Vector3 GetAxisVector(SceneViewportGizmoAxis axis) { Math::Color GetAxisBaseColor(SceneViewportGizmoAxis axis) { switch (axis) { case SceneViewportGizmoAxis::X: - return Math::Color(0.937f, 0.325f, 0.314f, 1.0f); + return Math::Color(0.91f, 0.09f, 0.05f, 1.0f); case SceneViewportGizmoAxis::Y: - return Math::Color(0.400f, 0.733f, 0.416f, 1.0f); + return Math::Color(0.45f, 1.0f, 0.12f, 1.0f); case SceneViewportGizmoAxis::Z: - return Math::Color(0.259f, 0.647f, 0.961f, 1.0f); + return Math::Color(0.11f, 0.29f, 1.0f, 1.0f); case SceneViewportGizmoAxis::None: default: return Math::Color::White(); } } +Math::Vector3 NormalizeVector3(const Math::Vector3& value, const Math::Vector3& fallback) { + return value.SqrMagnitude() <= Math::EPSILON ? fallback : value.Normalized(); +} + bool IsMouseInsideViewport(const SceneViewportMoveGizmoContext& context) { return context.mousePosition.x >= 0.0f && context.mousePosition.y >= 0.0f && @@ -49,6 +56,87 @@ bool IsMouseInsideViewport(const SceneViewportMoveGizmoContext& context) { context.mousePosition.y <= context.viewportSize.y; } +void EncapsulateTransformedBounds( + const Math::Bounds& localBounds, + const Components::TransformComponent& transform, + Math::Bounds& inOutBounds, + bool& inOutHasBounds) { + const Math::Vector3 localMin = localBounds.GetMin(); + const Math::Vector3 localMax = localBounds.GetMax(); + const Math::Vector3 corners[] = { + Math::Vector3(localMin.x, localMin.y, localMin.z), + Math::Vector3(localMax.x, localMin.y, localMin.z), + Math::Vector3(localMin.x, localMax.y, localMin.z), + Math::Vector3(localMin.x, localMin.y, localMax.z), + Math::Vector3(localMax.x, localMax.y, localMin.z), + Math::Vector3(localMin.x, localMax.y, localMax.z), + Math::Vector3(localMax.x, localMin.y, localMax.z), + Math::Vector3(localMax.x, localMax.y, localMax.z) + }; + + for (const Math::Vector3& localCorner : corners) { + const Math::Vector3 worldCorner = transform.TransformPoint(localCorner); + if (!inOutHasBounds) { + inOutBounds = Math::Bounds(worldCorner, Math::Vector3::Zero()); + inOutHasBounds = true; + } else { + inOutBounds.Encapsulate(worldCorner); + } + } +} + +void CollectRenderableWorldBoundsRecursive( + const Components::GameObject& gameObject, + Math::Bounds& inOutBounds, + bool& inOutHasBounds) { + if (!gameObject.IsActiveInHierarchy()) { + return; + } + + const auto* meshFilter = gameObject.GetComponent(); + const auto* meshRenderer = gameObject.GetComponent(); + if (meshFilter != nullptr && + meshRenderer != nullptr && + meshFilter->IsEnabled() && + meshRenderer->IsEnabled()) { + const auto* mesh = meshFilter->GetMesh(); + if (mesh != nullptr) { + EncapsulateTransformedBounds(mesh->GetBounds(), *gameObject.GetTransform(), inOutBounds, inOutHasBounds); + } + } + + for (size_t childIndex = 0; childIndex < gameObject.GetChildCount(); ++childIndex) { + const Components::GameObject* child = gameObject.GetChild(childIndex); + if (child != nullptr) { + CollectRenderableWorldBoundsRecursive(*child, inOutBounds, inOutHasBounds); + } + } +} + +float ComputeWorldUnitsPerPixel( + const SceneViewportOverlayData& overlay, + const Math::Vector3& worldPoint, + float viewportHeight) { + if (!overlay.valid || viewportHeight <= 1.0f) { + return 0.0f; + } + + const Math::Vector3 cameraForward = NormalizeVector3(overlay.cameraForward, Math::Vector3::Forward()); + const float depth = Math::Vector3::Dot(worldPoint - overlay.cameraPosition, cameraForward); + if (depth <= Math::EPSILON) { + return 0.0f; + } + + return 2.0f * depth * std::tan(overlay.verticalFovDegrees * Math::DEG_TO_RAD * 0.5f) / viewportHeight; +} + +Math::Vector3 GetGizmoWorldOrigin(const Components::GameObject& gameObject) { + Math::Bounds worldBounds = {}; + bool hasBounds = false; + CollectRenderableWorldBoundsRecursive(gameObject, worldBounds, hasBounds); + return hasBounds ? worldBounds.center : gameObject.GetTransform()->GetPosition(); +} + } // namespace void SceneViewportMoveGizmo::Update(const SceneViewportMoveGizmoContext& context) { @@ -88,8 +176,9 @@ bool SceneViewportMoveGizmo::TryBeginDrag(const SceneViewportMoveGizmoContext& c return false; } - const Math::Vector3 worldPosition = context.selectedObject->GetTransform()->GetPosition(); - const Math::Plane dragPlane = BuildSceneViewportPlaneFromPointNormal(worldPosition, dragPlaneNormal); + const Math::Vector3 objectWorldPosition = context.selectedObject->GetTransform()->GetPosition(); + const Math::Vector3 pivotWorldPosition = GetGizmoWorldOrigin(*context.selectedObject); + const Math::Plane dragPlane = BuildSceneViewportPlaneFromPointNormal(pivotWorldPosition, dragPlaneNormal); float hitDistance = 0.0f; if (!worldRay.Intersects(dragPlane, hitDistance)) { return false; @@ -105,8 +194,9 @@ bool SceneViewportMoveGizmo::TryBeginDrag(const SceneViewportMoveGizmoContext& c m_activeEntityId = context.selectedObject->GetID(); m_activeAxisDirection = worldAxis; m_dragPlane = dragPlane; - m_dragStartWorldPosition = worldPosition; - m_dragStartAxisScalar = Math::Vector3::Dot(hitPoint - worldPosition, worldAxis); + m_dragStartObjectWorldPosition = objectWorldPosition; + m_dragStartPivotWorldPosition = pivotWorldPosition; + m_dragStartAxisScalar = Math::Vector3::Dot(hitPoint - pivotWorldPosition, worldAxis); RefreshHandleState(); return true; } @@ -133,10 +223,10 @@ void SceneViewportMoveGizmo::UpdateDrag(const SceneViewportMoveGizmoContext& con } const Math::Vector3 hitPoint = worldRay.GetPoint(hitDistance); - const float currentAxisScalar = Math::Vector3::Dot(hitPoint - m_dragStartWorldPosition, m_activeAxisDirection); + const float currentAxisScalar = Math::Vector3::Dot(hitPoint - m_dragStartPivotWorldPosition, m_activeAxisDirection); const float deltaScalar = currentAxisScalar - m_dragStartAxisScalar; context.selectedObject->GetTransform()->SetPosition( - m_dragStartWorldPosition + m_activeAxisDirection * deltaScalar); + m_dragStartObjectWorldPosition + m_activeAxisDirection * deltaScalar); } void SceneViewportMoveGizmo::EndDrag(IUndoManager& undoManager) { @@ -151,7 +241,8 @@ void SceneViewportMoveGizmo::EndDrag(IUndoManager& undoManager) { m_activeAxis = SceneViewportGizmoAxis::None; m_activeEntityId = 0; m_activeAxisDirection = Math::Vector3::Zero(); - m_dragStartWorldPosition = Math::Vector3::Zero(); + m_dragStartObjectWorldPosition = Math::Vector3::Zero(); + m_dragStartPivotWorldPosition = Math::Vector3::Zero(); m_dragStartAxisScalar = 0.0f; RefreshHandleState(); } @@ -164,7 +255,8 @@ void SceneViewportMoveGizmo::CancelDrag(IUndoManager* undoManager) { m_activeAxis = SceneViewportGizmoAxis::None; m_activeEntityId = 0; m_activeAxisDirection = Math::Vector3::Zero(); - m_dragStartWorldPosition = Math::Vector3::Zero(); + m_dragStartObjectWorldPosition = Math::Vector3::Zero(); + m_dragStartPivotWorldPosition = Math::Vector3::Zero(); m_dragStartAxisScalar = 0.0f; m_hoveredAxis = SceneViewportGizmoAxis::None; RefreshHandleState(); @@ -198,17 +290,28 @@ void SceneViewportMoveGizmo::BuildDrawData(const SceneViewportMoveGizmoContext& return; } + const Math::Vector3 gizmoWorldOrigin = GetGizmoWorldOrigin(*selectedObject); const SceneViewportProjectedPoint projectedPivot = ProjectSceneViewportWorldPoint( context.overlay, context.viewportSize.x, context.viewportSize.y, - selectedObject->GetTransform()->GetPosition()); + gizmoWorldOrigin); if (!projectedPivot.visible) { return; } m_drawData.visible = true; m_drawData.pivot = projectedPivot.screenPosition; + const float worldUnitsPerPixel = ComputeWorldUnitsPerPixel( + context.overlay, + gizmoWorldOrigin, + context.viewportSize.y); + if (worldUnitsPerPixel <= Math::EPSILON) { + m_drawData = {}; + return; + } + + const float axisLengthWorld = worldUnitsPerPixel * kMoveGizmoHandleLengthPixels; const SceneViewportGizmoAxis axes[] = { SceneViewportGizmoAxis::X, @@ -220,12 +323,22 @@ void SceneViewportMoveGizmo::BuildDrawData(const SceneViewportMoveGizmoContext& handle.axis = axes[index]; handle.start = projectedPivot.screenPosition; - Math::Vector2 screenDirection = Math::Vector2::Zero(); - if (!ProjectSceneViewportAxisDirection(context.overlay, GetAxisVector(handle.axis), screenDirection)) { + const Math::Vector3 axisEndWorld = + gizmoWorldOrigin + GetAxisVector(handle.axis) * axisLengthWorld; + const SceneViewportProjectedPoint projectedEnd = ProjectSceneViewportWorldPoint( + context.overlay, + context.viewportSize.x, + context.viewportSize.y, + axisEndWorld); + if (projectedEnd.ndcDepth < 0.0f || projectedEnd.ndcDepth > 1.0f) { continue; } - handle.end = handle.start + screenDirection * kMoveGizmoHandleLengthPixels; + if ((projectedEnd.screenPosition - projectedPivot.screenPosition).SqrMagnitude() <= Math::EPSILON) { + continue; + } + + handle.end = projectedEnd.screenPosition; handle.visible = true; handle.color = GetAxisBaseColor(handle.axis); } diff --git a/tests/editor/test_scene_viewport_move_gizmo.cpp b/tests/editor/test_scene_viewport_move_gizmo.cpp index 0bd5f80f..b90cfba4 100644 --- a/tests/editor/test_scene_viewport_move_gizmo.cpp +++ b/tests/editor/test_scene_viewport_move_gizmo.cpp @@ -7,6 +7,10 @@ namespace XCEngine::Editor { namespace { +float HandleLength(const SceneViewportMoveGizmoHandleDrawData& handle) { + return (handle.end - handle.start).Magnitude(); +} + class SceneViewportMoveGizmoTest : public ::testing::Test { protected: void SetUp() override { @@ -26,6 +30,19 @@ protected: return overlay; } + static SceneViewportOverlayData MakeDownwardTiltedOverlay() { + SceneViewportOverlayData overlay = {}; + overlay.valid = true; + overlay.cameraPosition = Math::Vector3(0.0f, 10.0f, -1.0f); + overlay.cameraForward = (Math::Vector3::Zero() - overlay.cameraPosition).Normalized(); + overlay.cameraRight = Math::Vector3::Cross(Math::Vector3::Up(), overlay.cameraForward).Normalized(); + overlay.cameraUp = Math::Vector3::Cross(overlay.cameraForward, overlay.cameraRight).Normalized(); + overlay.verticalFovDegrees = 60.0f; + overlay.nearClipPlane = 0.03f; + overlay.farClipPlane = 2000.0f; + return overlay; + } + static SceneViewportMoveGizmoContext MakeContext( Components::GameObject* selectedObject, const Math::Vector2& mousePosition) { @@ -37,6 +54,18 @@ protected: return context; } + static SceneViewportMoveGizmoContext MakeContext( + Components::GameObject* selectedObject, + const Math::Vector2& mousePosition, + const SceneViewportOverlayData& overlay) { + SceneViewportMoveGizmoContext context = {}; + context.overlay = overlay; + context.viewportSize = Math::Vector2(800.0f, 600.0f); + context.mousePosition = mousePosition; + context.selectedObject = selectedObject; + return context; + } + SceneManager& GetSceneManager() { return dynamic_cast(m_context.GetSceneManager()); } @@ -92,5 +121,23 @@ TEST_F(SceneViewportMoveGizmoTest, DraggingXAxisOnlyChangesWorldXAndCreatesUndoS EXPECT_NEAR(restoredPosition.z, 0.0f, 1e-4f); } +TEST_F(SceneViewportMoveGizmoTest, AxisNearlyFacingCameraShrinksInsteadOfKeepingFullLength) { + Components::GameObject* target = GetSceneManager().CreateEntity("Target"); + ASSERT_NE(target, nullptr); + + SceneViewportMoveGizmo gizmo; + gizmo.Update(MakeContext(target, Math::Vector2(400.0f, 300.0f), MakeDownwardTiltedOverlay())); + + ASSERT_TRUE(gizmo.GetDrawData().visible); + const float xLength = HandleLength(gizmo.GetDrawData().handles[0]); + const float yLength = HandleLength(gizmo.GetDrawData().handles[1]); + const float zLength = HandleLength(gizmo.GetDrawData().handles[2]); + + EXPECT_GT(xLength, 60.0f); + EXPECT_GT(zLength, 60.0f); + EXPECT_LT(yLength, xLength * 0.5f); + EXPECT_LT(yLength, zLength * 0.5f); +} + } // namespace } // namespace XCEngine::Editor