From af2f30dad643e2cc1530a203b8cc982147043a3e Mon Sep 17 00:00:00 2001 From: ssdfasd <2156608475@qq.com> Date: Sat, 28 Mar 2026 18:21:18 +0800 Subject: [PATCH] refactor: align scene view camera controls with unity --- editor/src/Viewport/IViewportHostService.h | 1 + .../Viewport/SceneViewportCameraController.h | 66 +++++++++++++++---- editor/src/Viewport/ViewportHostService.h | 5 ++ editor/src/panels/SceneViewPanel.cpp | 15 ++++- editor/src/panels/SceneViewPanel.h | 1 + .../test_scene_viewport_camera_controller.cpp | 64 ++++++++++++++---- 6 files changed, 123 insertions(+), 29 deletions(-) diff --git a/editor/src/Viewport/IViewportHostService.h b/editor/src/Viewport/IViewportHostService.h index 5f7a4ce9..131d2745 100644 --- a/editor/src/Viewport/IViewportHostService.h +++ b/editor/src/Viewport/IViewportHostService.h @@ -35,6 +35,7 @@ struct SceneViewportInput { float mouseWheel = 0.0f; bool hovered = false; bool focused = false; + bool looking = false; bool orbiting = false; bool panning = false; bool focusSelectionRequested = false; diff --git a/editor/src/Viewport/SceneViewportCameraController.h b/editor/src/Viewport/SceneViewportCameraController.h index 39ff2959..2f448c47 100644 --- a/editor/src/Viewport/SceneViewportCameraController.h +++ b/editor/src/Viewport/SceneViewportCameraController.h @@ -12,6 +12,8 @@ namespace XCEngine { namespace Editor { struct SceneViewportCameraInputState { + float lookDeltaX = 0.0f; + float lookDeltaY = 0.0f; float orbitDeltaX = 0.0f; float orbitDeltaY = 0.0f; float panDeltaX = 0.0f; @@ -27,6 +29,7 @@ public: m_distance = 6.0f; m_yawDegrees = -35.0f; m_pitchDegrees = -20.0f; + UpdatePositionFromFocalPoint(); } const Math::Vector3& GetFocalPoint() const { @@ -56,11 +59,12 @@ public: } Math::Vector3 GetPosition() const { - return m_focalPoint - GetForward() * m_distance; + return m_position; } void Focus(const Math::Vector3& point) { m_focalPoint = point; + UpdatePositionFromFocalPoint(); } void ApplyInput(const SceneViewportCameraInputState& input) { @@ -68,28 +72,31 @@ public: return; } - if (std::abs(input.zoomDelta) > Math::EPSILON) { - const float zoomFactor = std::pow(0.85f, input.zoomDelta); - m_distance = std::clamp(m_distance * zoomFactor, 0.5f, 500.0f); + if (std::abs(input.lookDeltaX) > Math::EPSILON || + std::abs(input.lookDeltaY) > Math::EPSILON) { + ApplyRotationDelta(input.lookDeltaX, input.lookDeltaY); + UpdateFocalPointFromPosition(); } if (std::abs(input.orbitDeltaX) > Math::EPSILON || std::abs(input.orbitDeltaY) > Math::EPSILON) { - m_yawDegrees += input.orbitDeltaX * 0.30f; - m_pitchDegrees = std::clamp(m_pitchDegrees - input.orbitDeltaY * 0.20f, -89.0f, 89.0f); + ApplyRotationDelta(input.orbitDeltaX, input.orbitDeltaY); + UpdatePositionFromFocalPoint(); } if (std::abs(input.panDeltaX) > Math::EPSILON || std::abs(input.panDeltaY) > Math::EPSILON) { - const Math::Vector3 forward = GetForward(); - const Math::Vector3 right = Math::Vector3::Normalize( - Math::Vector3::Cross(Math::Vector3::Up(), forward)); - const Math::Vector3 up = Math::Vector3::Normalize( - Math::Vector3::Cross(forward, right)); - - const float worldUnitsPerPixel = - 2.0f * m_distance * std::tan(60.0f * Math::DEG_TO_RAD * 0.5f) / input.viewportHeight; + const Math::Vector3 right = GetRight(); + const Math::Vector3 up = GetUp(); + const float worldUnitsPerPixel = ComputeWorldUnitsPerPixel(input.viewportHeight); m_focalPoint += ((right * -input.panDeltaX) + (up * input.panDeltaY)) * worldUnitsPerPixel; + m_position += ((right * -input.panDeltaX) + (up * input.panDeltaY)) * worldUnitsPerPixel; + } + + if (std::abs(input.zoomDelta) > Math::EPSILON) { + const float zoomFactor = std::pow(0.85f, input.zoomDelta); + m_distance = std::clamp(m_distance * zoomFactor, 0.5f, 500.0f); + UpdatePositionFromFocalPoint(); } } @@ -102,6 +109,37 @@ public: } private: + void ApplyRotationDelta(float deltaX, float deltaY) { + m_yawDegrees += deltaX * 0.30f; + m_pitchDegrees = std::clamp(m_pitchDegrees - deltaY * 0.20f, -89.0f, 89.0f); + } + + Math::Vector3 GetRight() const { + const Math::Vector3 right = Math::Vector3::Cross(Math::Vector3::Up(), GetForward()); + if (right.SqrMagnitude() <= Math::EPSILON) { + return Math::Vector3::Right(); + } + return Math::Vector3::Normalize(right); + } + + Math::Vector3 GetUp() const { + return Math::Vector3::Normalize(Math::Vector3::Cross(GetForward(), GetRight())); + } + + float ComputeWorldUnitsPerPixel(float viewportHeight) const { + return 2.0f * m_distance * std::tan(60.0f * Math::DEG_TO_RAD * 0.5f) / viewportHeight; + } + + void UpdatePositionFromFocalPoint() { + m_position = m_focalPoint - GetForward() * m_distance; + } + + void UpdateFocalPointFromPosition() { + m_focalPoint = m_position + GetForward() * m_distance; + } + +private: + Math::Vector3 m_position = Math::Vector3::Zero(); Math::Vector3 m_focalPoint = Math::Vector3::Zero(); float m_distance = 6.0f; float m_yawDegrees = -35.0f; diff --git a/editor/src/Viewport/ViewportHostService.h b/editor/src/Viewport/ViewportHostService.h index cc19860a..7645821a 100644 --- a/editor/src/Viewport/ViewportHostService.h +++ b/editor/src/Viewport/ViewportHostService.h @@ -92,6 +92,11 @@ public: controllerInput.viewportHeight = input.viewportSize.y; controllerInput.zoomDelta = input.hovered ? input.mouseWheel : 0.0f; + if (input.looking) { + controllerInput.lookDeltaX = input.mouseDelta.x; + controllerInput.lookDeltaY = input.mouseDelta.y; + } + if (input.orbiting) { controllerInput.orbitDeltaX = input.mouseDelta.x; controllerInput.orbitDeltaY = input.mouseDelta.y; diff --git a/editor/src/panels/SceneViewPanel.cpp b/editor/src/panels/SceneViewPanel.cpp index 5c27fa92..6bce0ba0 100644 --- a/editor/src/panels/SceneViewPanel.cpp +++ b/editor/src/panels/SceneViewPanel.cpp @@ -239,14 +239,22 @@ void SceneViewPanel::Render() { const ViewportPanelContentResult content = RenderViewportPanelContent(*m_context, EditorViewportKind::Scene); if (IViewportHostService* viewportHostService = m_context->GetViewportHostService()) { const ImGuiIO& io = ImGui::GetIO(); - if (!m_orbitDragging && content.hovered && ImGui::IsMouseClicked(ImGuiMouseButton_Right)) { + const bool altDown = io.KeyAlt; + + if (!m_lookDragging && content.hovered && !altDown && ImGui::IsMouseClicked(ImGuiMouseButton_Right)) { + m_lookDragging = true; + } + if (!m_orbitDragging && content.hovered && altDown && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) { m_orbitDragging = true; } if (!m_panDragging && content.hovered && ImGui::IsMouseClicked(ImGuiMouseButton_Middle)) { m_panDragging = true; } - if (m_orbitDragging && !ImGui::IsMouseDown(ImGuiMouseButton_Right)) { + if (m_lookDragging && (!ImGui::IsMouseDown(ImGuiMouseButton_Right) || altDown)) { + m_lookDragging = false; + } + if (m_orbitDragging && (!ImGui::IsMouseDown(ImGuiMouseButton_Left) || !altDown)) { m_orbitDragging = false; } if (m_panDragging && !ImGui::IsMouseDown(ImGuiMouseButton_Middle)) { @@ -258,12 +266,13 @@ void SceneViewPanel::Render() { input.hovered = content.hovered; input.focused = content.focused; input.mouseWheel = content.hovered ? io.MouseWheel : 0.0f; + input.looking = m_lookDragging; input.orbiting = m_orbitDragging; input.panning = m_panDragging; input.focusSelectionRequested = content.focused && !io.WantTextInput && ImGui::IsKeyPressed(ImGuiKey_F, false); - if (m_orbitDragging || m_panDragging) { + if (m_lookDragging || m_orbitDragging || m_panDragging) { input.mouseDelta = io.MouseDelta; } diff --git a/editor/src/panels/SceneViewPanel.h b/editor/src/panels/SceneViewPanel.h index 0bbe16e3..0c559b62 100644 --- a/editor/src/panels/SceneViewPanel.h +++ b/editor/src/panels/SceneViewPanel.h @@ -11,6 +11,7 @@ public: void Render() override; private: + bool m_lookDragging = false; bool m_orbitDragging = false; bool m_panDragging = false; }; diff --git a/tests/editor/test_scene_viewport_camera_controller.cpp b/tests/editor/test_scene_viewport_camera_controller.cpp index 0600d268..04beb515 100644 --- a/tests/editor/test_scene_viewport_camera_controller.cpp +++ b/tests/editor/test_scene_viewport_camera_controller.cpp @@ -41,28 +41,68 @@ TEST(SceneViewportCameraController_Test, ApplyToMatchesComputedPositionAndForwar EXPECT_GT(Vector3::Dot(cameraObject.GetTransform()->GetUp().Normalized(), Vector3::Up()), 0.0f); } -TEST(SceneViewportCameraController_Test, ApplyInputUpdatesOrbitPanAndZoomState) { +TEST(SceneViewportCameraController_Test, LookInputRotatesCameraInPlaceAndKeepsDistance) { SceneViewportCameraController controller; controller.Reset(); - const float initialDistance = controller.GetDistance(); - const float initialYaw = controller.GetYawDegrees(); - const float initialPitch = controller.GetPitchDegrees(); + const Vector3 initialPosition = controller.GetPosition(); const Vector3 initialFocus = controller.GetFocalPoint(); SceneViewportCameraInputState input = {}; input.viewportHeight = 720.0f; - input.zoomDelta = 1.0f; - input.orbitDeltaX = 20.0f; - input.orbitDeltaY = -15.0f; - input.panDeltaX = 16.0f; - input.panDeltaY = -10.0f; + input.lookDeltaX = 20.0f; + input.lookDeltaY = -15.0f; controller.ApplyInput(input); - EXPECT_LT(controller.GetDistance(), initialDistance); - EXPECT_NE(controller.GetYawDegrees(), initialYaw); - EXPECT_NE(controller.GetPitchDegrees(), initialPitch); + EXPECT_TRUE(NearlyEqual(controller.GetPosition(), initialPosition)); EXPECT_FALSE(NearlyEqual(controller.GetFocalPoint(), initialFocus)); + EXPECT_TRUE(NearlyEqual( + controller.GetFocalPoint(), + controller.GetPosition() + controller.GetForward() * controller.GetDistance(), + 1e-3f)); +} + +TEST(SceneViewportCameraController_Test, OrbitInputRotatesAroundFocalPointAndKeepsDistance) { + SceneViewportCameraController controller; + controller.Reset(); + + const Vector3 initialPosition = controller.GetPosition(); + const Vector3 initialFocus = controller.GetFocalPoint(); + const float initialDistance = controller.GetDistance(); + + SceneViewportCameraInputState input = {}; + input.viewportHeight = 720.0f; + input.orbitDeltaX = 20.0f; + input.orbitDeltaY = -15.0f; + controller.ApplyInput(input); + + EXPECT_FALSE(NearlyEqual(controller.GetPosition(), initialPosition)); + EXPECT_TRUE(NearlyEqual(controller.GetFocalPoint(), initialFocus)); + EXPECT_NEAR((controller.GetFocalPoint() - controller.GetPosition()).Magnitude(), initialDistance, 1e-3f); +} + +TEST(SceneViewportCameraController_Test, PanAndZoomUpdateCameraStateConsistently) { + SceneViewportCameraController controller; + controller.Reset(); + + const Vector3 initialPosition = controller.GetPosition(); + const Vector3 initialFocus = controller.GetFocalPoint(); + const float initialDistance = controller.GetDistance(); + + SceneViewportCameraInputState input = {}; + input.viewportHeight = 720.0f; + input.panDeltaX = 16.0f; + input.panDeltaY = -10.0f; + input.zoomDelta = 1.0f; + controller.ApplyInput(input); + + EXPECT_FALSE(NearlyEqual(controller.GetPosition(), initialPosition)); + EXPECT_FALSE(NearlyEqual(controller.GetFocalPoint(), initialFocus)); + EXPECT_LT(controller.GetDistance(), initialDistance); + EXPECT_TRUE(NearlyEqual( + controller.GetFocalPoint(), + controller.GetPosition() + controller.GetForward() * controller.GetDistance(), + 1e-3f)); } TEST(SceneViewportCameraController_Test, FocusMovesPivotWithoutChangingDistance) {