diff --git a/Code/Editor/CMakeLists.txt b/Code/Editor/CMakeLists.txt index 9baa83179b..9256fd041f 100644 --- a/Code/Editor/CMakeLists.txt +++ b/Code/Editor/CMakeLists.txt @@ -242,6 +242,7 @@ if(PAL_TRAIT_BUILD_TESTS_SUPPORTED) Legacy::CryCommon AZ::AzToolsFramework AZ::AzToolsFramework.Tests + AZ::AzFrameworkTestShared AZ::AzToolsFrameworkTestCommon Legacy::EditorLib Gem::AtomToolsFramework.Static diff --git a/Code/Editor/EditorViewportSettings.cpp b/Code/Editor/EditorViewportSettings.cpp index 680592a597..6e0ed86d2a 100644 --- a/Code/Editor/EditorViewportSettings.cpp +++ b/Code/Editor/EditorViewportSettings.cpp @@ -31,6 +31,8 @@ namespace SandboxEditor constexpr AZStd::string_view CameraPanSpeedSetting = "/Amazon/Preferences/Editor/Camera/PanSpeed"; constexpr AZStd::string_view CameraRotateSmoothnessSetting = "/Amazon/Preferences/Editor/Camera/RotateSmoothness"; constexpr AZStd::string_view CameraTranslateSmoothnessSetting = "/Amazon/Preferences/Editor/Camera/TranslateSmoothness"; + constexpr AZStd::string_view CameraTranslateSmoothingSetting = "/Amazon/Preferences/Editor/Camera/TranslateSmoothing"; + constexpr AZStd::string_view CameraRotateSmoothingSetting = "/Amazon/Preferences/Editor/Camera/RotateSmoothing"; constexpr AZStd::string_view CameraTranslateForwardIdSetting = "/Amazon/Preferences/Editor/Camera/CameraTranslateForwardId"; constexpr AZStd::string_view CameraTranslateBackwardIdSetting = "/Amazon/Preferences/Editor/Camera/CameraTranslateBackwardId"; constexpr AZStd::string_view CameraTranslateLeftIdSetting = "/Amazon/Preferences/Editor/Camera/CameraTranslateLeftId"; @@ -259,6 +261,26 @@ namespace SandboxEditor SetRegistry(CameraTranslateSmoothnessSetting, smoothness); } + bool CameraRotateSmoothingEnabled() + { + return GetRegistry(CameraRotateSmoothingSetting, true); + } + + void SetCameraRotateSmoothingEnabled(const bool enabled) + { + SetRegistry(CameraRotateSmoothingSetting, enabled); + } + + bool CameraTranslateSmoothingEnabled() + { + return GetRegistry(CameraTranslateSmoothingSetting, true); + } + + void SetCameraTranslateSmoothingEnabled(const bool enabled) + { + SetRegistry(CameraTranslateSmoothingSetting, enabled); + } + AzFramework::InputChannelId CameraTranslateForwardChannelId() { return AzFramework::InputChannelId( diff --git a/Code/Editor/EditorViewportSettings.h b/Code/Editor/EditorViewportSettings.h index b1488c5528..1aca51395f 100644 --- a/Code/Editor/EditorViewportSettings.h +++ b/Code/Editor/EditorViewportSettings.h @@ -80,6 +80,12 @@ namespace SandboxEditor SANDBOX_API float CameraTranslateSmoothness(); SANDBOX_API void SetCameraTranslateSmoothness(float smoothness); + SANDBOX_API bool CameraRotateSmoothingEnabled(); + SANDBOX_API void SetCameraRotateSmoothingEnabled(bool enabled); + + SANDBOX_API bool CameraTranslateSmoothingEnabled(); + SANDBOX_API void SetCameraTranslateSmoothingEnabled(bool enabled); + SANDBOX_API AzFramework::InputChannelId CameraTranslateForwardChannelId(); SANDBOX_API void SetCameraTranslateForwardChannelId(AZStd::string_view cameraTranslateForwardId); diff --git a/Code/Editor/EditorViewportWidget.cpp b/Code/Editor/EditorViewportWidget.cpp index 28e8cce33e..4d40c44402 100644 --- a/Code/Editor/EditorViewportWidget.cpp +++ b/Code/Editor/EditorViewportWidget.cpp @@ -132,12 +132,11 @@ namespace AZ::ViewportHelpers { static const char TextCantCreateCameraNoLevel[] = "Cannot create camera when no level is loaded."; - class EditorEntityNotifications - : public AzToolsFramework::EditorEntityContextNotificationBus::Handler + class EditorEntityNotifications : public AzToolsFramework::EditorEntityContextNotificationBus::Handler { public: - EditorEntityNotifications(EditorViewportWidget& renderViewport) - : m_renderViewport(renderViewport) + EditorEntityNotifications(EditorViewportWidget& editorViewportWidget) + : m_editorViewportWidget(editorViewportWidget) { AzToolsFramework::EditorEntityContextNotificationBus::Handler::BusConnect(); } @@ -147,22 +146,24 @@ namespace AZ::ViewportHelpers AzToolsFramework::EditorEntityContextNotificationBus::Handler::BusDisconnect(); } - // AzToolsFramework::EditorEntityContextNotificationBus + // AzToolsFramework::EditorEntityContextNotificationBus overrides ... void OnStartPlayInEditor() override { - m_renderViewport.OnStartPlayInEditor(); + m_editorViewportWidget.OnStartPlayInEditor(); } + void OnStopPlayInEditor() override { - m_renderViewport.OnStopPlayInEditor(); + m_editorViewportWidget.OnStopPlayInEditor(); } + void OnStartPlayInEditorBegin() override { - m_renderViewport.OnStartPlayInEditorBegin(); + m_editorViewportWidget.OnStartPlayInEditorBegin(); } private: - EditorViewportWidget& m_renderViewport; + EditorViewportWidget& m_editorViewportWidget; }; } // namespace AZ::ViewportHelpers @@ -1027,10 +1028,16 @@ bool EditorViewportWidget::ShowingWorldSpace() } AZStd::shared_ptr CreateModularViewportCameraController( - AzFramework::ViewportId viewportId) + const AzFramework::ViewportId viewportId) { auto controller = AZStd::make_shared(); + controller->SetCameraViewportContextBuilderCallback( + [viewportId](AZStd::unique_ptr& cameraViewportContext) + { + cameraViewportContext = AZStd::make_unique(viewportId); + }); + controller->SetCameraPriorityBuilderCallback( [](AtomToolsFramework::CameraControllerPriorityFn& cameraControllerPriorityFn) { @@ -1049,6 +1056,16 @@ AZStd::shared_ptr CreateMod { return SandboxEditor::CameraTranslateSmoothness(); }; + + cameraProps.m_rotateSmoothingEnabledFn = [] + { + return SandboxEditor::CameraRotateSmoothingEnabled(); + }; + + cameraProps.m_translateSmoothingEnabledFn = [] + { + return SandboxEditor::CameraTranslateSmoothingEnabled(); + }; }); controller->SetCameraListBuilderCallback( diff --git a/Code/Editor/EditorViewportWidget.h b/Code/Editor/EditorViewportWidget.h index 33ed001735..a75928b353 100644 --- a/Code/Editor/EditorViewportWidget.h +++ b/Code/Editor/EditorViewportWidget.h @@ -54,7 +54,8 @@ namespace AZ::ViewportHelpers namespace AtomToolsFramework { class RenderViewportWidget; -} + class ModularViewportCameraController; +} // namespace AtomToolsFramework namespace AzToolsFramework { @@ -389,3 +390,7 @@ private: AZ_POP_DISABLE_DLL_EXPORT_MEMBER_WARNING }; + +//! Creates a modular camera controller in the configuration used by the editor viewport. +SANDBOX_API AZStd::shared_ptr CreateModularViewportCameraController( + const AzFramework::ViewportId viewportId); diff --git a/Code/Editor/Lib/Tests/test_ModularViewportCameraController.cpp b/Code/Editor/Lib/Tests/test_ModularViewportCameraController.cpp new file mode 100644 index 0000000000..c994458baa --- /dev/null +++ b/Code/Editor/Lib/Tests/test_ModularViewportCameraController.cpp @@ -0,0 +1,170 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#include +#include +#include +#include +#include +#include + +namespace UnitTest +{ + const QSize WidgetSize = QSize(1920, 1080); + + using AzToolsFramework::ViewportInteraction::MouseInteractionEvent; + + class ModularViewportCameraControllerFixture : public AllocatorsTestFixture + { + public: + static const AzFramework::ViewportId TestViewportId; + + void SetUp() override + { + AllocatorsTestFixture::SetUp(); + + m_rootWidget = AZStd::make_unique(); + m_rootWidget->setFixedSize(WidgetSize); + + m_controllerList = AZStd::make_shared(); + m_controllerList->RegisterViewportContext(TestViewportId); + + m_inputChannelMapper = AZStd::make_unique(m_rootWidget.get(), TestViewportId); + } + + void TearDown() + { + m_inputChannelMapper.reset(); + + m_controllerList->UnregisterViewportContext(TestViewportId); + m_controllerList.reset(); + m_rootWidget.reset(); + + AllocatorsTestFixture::TearDown(); + } + + AZStd::unique_ptr m_rootWidget; + AzFramework::ViewportControllerListPtr m_controllerList; + AZStd::unique_ptr m_inputChannelMapper; + }; + + const AzFramework::ViewportId ModularViewportCameraControllerFixture::TestViewportId = AzFramework::ViewportId(0); + + class TestModularCameraViewportContextImpl : public AtomToolsFramework::ModularCameraViewportContext + { + public: + AZ::Transform GetCameraTransform() const override + { + return m_cameraTransform; + } + + void SetCameraTransform(const AZ::Transform& transform) override + { + m_cameraTransform = transform; + } + + void ConnectViewMatrixChangedHandler(AZ::RPI::ViewportContext::MatrixChangedEvent::Handler&) override + { + // noop + } + + private: + AZ::Transform m_cameraTransform = AZ::Transform::CreateIdentity(); + }; + + TEST_F(ModularViewportCameraControllerFixture, Mouse_movement_does_not_accumulate_excessive_drift_in_modular_viewport_camera) + { + AzFramework::NativeWindowHandle nativeWindowHandle = nullptr; + + const float deltaTime = 1.0f / 60.0f; // mimic 60fps + + // Given + // listen for events signaled from QtEventToAzInputMapper and forward to the controller list + QObject::connect( + m_inputChannelMapper.get(), &AzToolsFramework::QtEventToAzInputMapper::InputChannelUpdated, m_rootWidget.get(), + [this, nativeWindowHandle](const AzFramework::InputChannel* inputChannel, [[maybe_unused]] QEvent* event) + { + m_controllerList->HandleInputChannelEvent( + AzFramework::ViewportControllerInputEvent{ TestViewportId, nativeWindowHandle, *inputChannel }); + }); + + using ::testing::NiceMock; + using ::testing::Return; + + NiceMock mockWindowRequests; + mockWindowRequests.Connect(nativeWindowHandle); + + // note: WindowRequests is used internally by ModularViewportCameraController, this ensures it returns the viewport size we want + ON_CALL(mockWindowRequests, GetClientAreaSize()) + .WillByDefault(Return(AzFramework::WindowSize(WidgetSize.width(), WidgetSize.height()))); + + // create editor modular camera + auto controller = CreateModularViewportCameraController(TestViewportId); + + // set some overrides for the test + AtomToolsFramework::ModularCameraViewportContext* cameraViewportContextView = nullptr; + controller->SetCameraViewportContextBuilderCallback( + [&cameraViewportContextView](AZStd::unique_ptr& cameraViewportContext) + { + cameraViewportContext = AZStd::make_unique(); + cameraViewportContextView = cameraViewportContext.get(); + }); + + controller->SetCameraPropsBuilderCallback( + [](AzFramework::CameraProps& cameraProps) + { + cameraProps.m_rotateSmoothingEnabledFn = [] + { + return false; + }; + + cameraProps.m_translateSmoothingEnabledFn = [] + { + return false; + }; + }); + + m_controllerList->Add(controller); + + // move to the center of the screen + auto start = QPoint(WidgetSize.width() / 2, WidgetSize.height() / 2); + MouseMove(m_rootWidget.get(), start, QPoint(0, 0)); + m_controllerList->UpdateViewport({ TestViewportId, AzFramework::FloatSeconds(deltaTime), AZ::ScriptTimePoint() }); + + // When + // move mouse diagonally to top right, then to bottom left and back repeatedly + auto current = start; + auto halfDelta = QPoint(200, -200); + const int iterationsPerDiagonal = 50; + for (int diagonals = 0; diagonals < 80; ++diagonals) + { + for (int i = 0; i < iterationsPerDiagonal; ++i) + { + MousePressAndMove(m_rootWidget.get(), current, halfDelta / iterationsPerDiagonal, Qt::MouseButton::RightButton); + m_controllerList->UpdateViewport({ TestViewportId, AzFramework::FloatSeconds(deltaTime), AZ::ScriptTimePoint() }); + current += halfDelta / iterationsPerDiagonal; + } + + if (diagonals % 2 == 0) + { + halfDelta.setX(halfDelta.x() * -1); + halfDelta.setY(halfDelta.y() * -1); + } + } + + QTest::mouseRelease(m_rootWidget.get(), Qt::MouseButton::RightButton, Qt::KeyboardModifier::NoModifier, current); + m_controllerList->UpdateViewport({ TestViewportId, AzFramework::FloatSeconds(deltaTime), AZ::ScriptTimePoint() }); + + // Then + // ensure the camera rotation is the identity (no significant drift has occurred as we moved the mouse) + const AZ::Transform cameraRotation = cameraViewportContextView->GetCameraTransform(); + EXPECT_THAT(cameraRotation.GetRotation(), IsClose(AZ::Quaternion::CreateIdentity())); + + mockWindowRequests.Disconnect(); + } +} // namespace UnitTest diff --git a/Code/Editor/editor_lib_test_files.cmake b/Code/Editor/editor_lib_test_files.cmake index 49f707b1f6..2ae3d22c19 100644 --- a/Code/Editor/editor_lib_test_files.cmake +++ b/Code/Editor/editor_lib_test_files.cmake @@ -21,6 +21,7 @@ set(FILES Lib/Tests/test_ViewportTitleDlgPythonBindings.cpp Lib/Tests/test_DisplaySettingsPythonBindings.cpp Lib/Tests/test_ViewportManipulatorController.cpp + Lib/Tests/test_ModularViewportCameraController.cpp DisplaySettingsPythonFuncs.cpp DisplaySettingsPythonFuncs.h ) diff --git a/Code/Framework/AzFramework/AzFramework/Viewport/CameraInput.cpp b/Code/Framework/AzFramework/AzFramework/Viewport/CameraInput.cpp index c5b6a2ff96..f40c92997a 100644 --- a/Code/Framework/AzFramework/AzFramework/Viewport/CameraInput.cpp +++ b/Code/Framework/AzFramework/AzFramework/Viewport/CameraInput.cpp @@ -8,12 +8,12 @@ #include "CameraInput.h" -#include #include #include #include #include #include +#include namespace AzFramework { @@ -26,6 +26,13 @@ namespace AzFramework "The default height of the ground plane to do intersection tests against when orbiting"); AZ_CVAR(float, ed_cameraSystemMinOrbitDistance, 10.0f, nullptr, AZ::ConsoleFunctorFlags::Null, ""); AZ_CVAR(float, ed_cameraSystemMaxOrbitDistance, 50.0f, nullptr, AZ::ConsoleFunctorFlags::Null, ""); + AZ_CVAR( + bool, + ed_cameraSystemUseCursor, + true, + nullptr, + AZ::ConsoleFunctorFlags::Null, + "Should the camera use cursor absolute positions or motion deltas"); //! return -1.0f if inverted, 1.0f otherwise constexpr static float Invert(const bool invert) @@ -134,9 +141,13 @@ namespace AzFramework bool CameraSystem::HandleEvents(const InputEvent& event) { - if (const auto& horizonalMotion = AZStd::get_if(&event)) + if (const auto& cursor = AZStd::get_if(&event)) { - m_motionDelta.m_x = horizonalMotion->m_delta; + m_cursorState.SetCurrentPosition(cursor->m_position); + } + else if (const auto& horizontalMotion = AZStd::get_if(&event)) + { + m_motionDelta.m_x = horizontalMotion->m_delta; } else if (const auto& verticalMotion = AZStd::get_if(&event)) { @@ -147,15 +158,18 @@ namespace AzFramework m_scrollDelta = scroll->m_delta; } - m_handlingEvents = m_cameras.HandleEvents(event, m_motionDelta, m_scrollDelta); + m_handlingEvents = + m_cameras.HandleEvents(event, ed_cameraSystemUseCursor ? m_cursorState.CursorDelta() : m_motionDelta, m_scrollDelta); return m_handlingEvents; } Camera CameraSystem::StepCamera(const Camera& targetCamera, const float deltaTime) { - const auto nextCamera = m_cameras.StepCamera(targetCamera, m_motionDelta, m_scrollDelta, deltaTime); + const auto nextCamera = m_cameras.StepCamera( + targetCamera, ed_cameraSystemUseCursor ? m_cursorState.CursorDelta() : m_motionDelta, m_scrollDelta, deltaTime); + m_cursorState.Update(); m_motionDelta = ScreenVector{ 0, 0 }; m_scrollDelta = 0.0f; @@ -727,18 +741,36 @@ namespace AzFramework Camera camera; // note: the math for the lerp smoothing implementation for camera rotation and translation was inspired by this excellent // article by Scott Lembcke: https://www.gamasutra.com/blogs/ScottLembcke/20180404/316046/Improved_Lerp_Smoothing.php - const float lookRate = AZStd::exp2(cameraProps.m_rotateSmoothnessFn()); - const float lookT = AZStd::exp2(-lookRate * deltaTime); - camera.m_pitch = AZ::Lerp(targetCamera.m_pitch, currentCamera.m_pitch, lookT); - camera.m_yaw = AZ::Lerp(targetYaw, currentYaw, lookT); - const float moveRate = AZStd::exp2(cameraProps.m_translateSmoothnessFn()); - const float moveT = AZStd::exp2(-moveRate * deltaTime); - camera.m_lookDist = AZ::Lerp(targetCamera.m_lookDist, currentCamera.m_lookDist, moveT); - camera.m_lookAt = targetCamera.m_lookAt.Lerp(currentCamera.m_lookAt, moveT); + if (cameraProps.m_rotateSmoothingEnabledFn()) + { + const float lookRate = AZStd::exp2(cameraProps.m_rotateSmoothnessFn()); + const float lookTime = AZStd::exp2(-lookRate * deltaTime); + camera.m_pitch = AZ::Lerp(targetCamera.m_pitch, currentCamera.m_pitch, lookTime); + camera.m_yaw = AZ::Lerp(targetYaw, currentYaw, lookTime); + } + else + { + camera.m_pitch = targetCamera.m_pitch; + camera.m_yaw = targetYaw; + } + + if (cameraProps.m_translateSmoothingEnabledFn()) + { + const float moveRate = AZStd::exp2(cameraProps.m_translateSmoothnessFn()); + const float moveTime = AZStd::exp2(-moveRate * deltaTime); + camera.m_lookDist = AZ::Lerp(targetCamera.m_lookDist, currentCamera.m_lookDist, moveTime); + camera.m_lookAt = targetCamera.m_lookAt.Lerp(currentCamera.m_lookAt, moveTime); + } + else + { + camera.m_lookDist = targetCamera.m_lookDist; + camera.m_lookAt = targetCamera.m_lookAt; + } + return camera; } - InputEvent BuildInputEvent(const InputChannel& inputChannel) + InputEvent BuildInputEvent(const InputChannel& inputChannel, const WindowSize& windowSize) { const auto& inputChannelId = inputChannel.GetInputChannelId(); const auto& inputDeviceId = inputChannel.GetInputDevice().GetInputDeviceId(); @@ -753,7 +785,16 @@ namespace AzFramework // accept active mouse channel updates, inactive movement channels will just have a 0 delta if (inputChannel.IsActive()) { - if (inputChannelId == InputDeviceMouse::Movement::X) + if (inputChannelId == InputDeviceMouse::SystemCursorPosition) + { + const auto* position = inputChannel.GetCustomData(); + AZ_Assert(position, "Expected PositionData2D but found nullptr"); + + return CursorEvent{ ScreenPoint( + position->m_normalizedPosition.GetX() * windowSize.m_width, + position->m_normalizedPosition.GetY() * windowSize.m_height) }; + } + else if (inputChannelId == InputDeviceMouse::Movement::X) { return HorizontalMotionEvent{ aznumeric_cast(inputChannel.GetValue()) }; } @@ -761,6 +802,7 @@ namespace AzFramework { return VerticalMotionEvent{ aznumeric_cast(inputChannel.GetValue()) }; } + else if (inputChannelId == InputDeviceMouse::Movement::Z) { return ScrollEvent{ inputChannel.GetValue() }; diff --git a/Code/Framework/AzFramework/AzFramework/Viewport/CameraInput.h b/Code/Framework/AzFramework/AzFramework/Viewport/CameraInput.h index bb0df4853a..0b7bbbc30d 100644 --- a/Code/Framework/AzFramework/AzFramework/Viewport/CameraInput.h +++ b/Code/Framework/AzFramework/AzFramework/Viewport/CameraInput.h @@ -8,17 +8,23 @@ #pragma once +#include #include #include #include #include #include #include +#include #include #include namespace AzFramework { + AZ_CVAR_EXTERNED(bool, ed_cameraSystemUseCursor); + + struct WindowSize; + //! Returns Euler angles (pitch, roll, yaw) for the incoming orientation. //! @note Order of rotation is Z, Y, X. AZ::Vector3 EulerAngles(const AZ::Matrix3x3& orientation); @@ -79,6 +85,11 @@ namespace AzFramework using HorizontalMotionEvent = MotionEvent; using VerticalMotionEvent = MotionEvent; + struct CursorEvent + { + ScreenPoint m_position; + }; + struct ScrollEvent { float m_delta; @@ -93,7 +104,8 @@ namespace AzFramework }; //! Represents a type-safe union of input events that are handled by the camera system. - using InputEvent = AZStd::variant; + using InputEvent = + AZStd::variant; //! Base class for all camera behaviors. //! The core interface consists of: @@ -219,10 +231,14 @@ namespace AzFramework //! Properties to use to configure behavior across all types of camera. struct CameraProps { - AZStd::function - m_rotateSmoothnessFn; //!< Rotate smoothing value (useful approx range 3-6, higher values give sharper feel). - AZStd::function - m_translateSmoothnessFn; //!< Translate smoothing value (useful approx range 3-6, higher values give sharper feel). + //! Rotate smoothing value (useful approx range 3-6, higher values give sharper feel). + AZStd::function m_rotateSmoothnessFn; + //! Translate smoothing value (useful approx range 3-6, higher values give sharper feel). + AZStd::function m_translateSmoothnessFn; + //! Enable/disable rotation smoothing. + AZStd::function m_rotateSmoothingEnabledFn; + //! Enable/disable translation smoothing. + AZStd::function m_translateSmoothingEnabledFn; }; //! An interpolation function to smoothly interpolate all camera properties from currentCamera to targetCamera. @@ -262,12 +278,16 @@ namespace AzFramework public: bool HandleEvents(const InputEvent& event); Camera StepCamera(const Camera& targetCamera, float deltaTime); - bool HandlingEvents() const { return m_handlingEvents; } + bool HandlingEvents() const + { + return m_handlingEvents; + } Cameras m_cameras; //!< Represents a collection of camera inputs that together provide a camera controller. private: ScreenVector m_motionDelta; //!< The delta used for look/orbit/pan (rotation + translation) - two dimensional. + CursorState m_cursorState; //!< The current and previous position of the cursor (used to calculate movement delta). float m_scrollDelta = 0.0f; //!< The delta used for dolly/movement (translation) - one dimensional. bool m_handlingEvents = false; //!< Is the camera system currently handling events (events are consumed and not propagated). }; @@ -548,5 +568,5 @@ namespace AzFramework } //! Map from a generic InputChannel event to a camera specific InputEvent. - InputEvent BuildInputEvent(const InputChannel& inputChannel); + InputEvent BuildInputEvent(const InputChannel& inputChannel, const WindowSize& windowSize); } // namespace AzFramework diff --git a/Code/Framework/AzFramework/CMakeLists.txt b/Code/Framework/AzFramework/CMakeLists.txt index 8a68aac887..4359ae1f02 100644 --- a/Code/Framework/AzFramework/CMakeLists.txt +++ b/Code/Framework/AzFramework/CMakeLists.txt @@ -70,6 +70,9 @@ if(PAL_TRAIT_BUILD_TESTS_SUPPORTED) PRIVATE AZ::AzCore AZ::AzFramework + PUBLIC + AZ::AzTest + AZ::AzTestShared ) if(PAL_TRAIT_BUILD_HOST_TOOLS) diff --git a/Code/Framework/AzFramework/Tests/CameraInputTests.cpp b/Code/Framework/AzFramework/Tests/CameraInputTests.cpp index 590d46850e..ef340a41a5 100644 --- a/Code/Framework/AzFramework/Tests/CameraInputTests.cpp +++ b/Code/Framework/AzFramework/Tests/CameraInputTests.cpp @@ -59,10 +59,15 @@ namespace UnitTest m_cameraSystem->m_cameras.AddCamera(m_firstPersonRotateCamera); m_cameraSystem->m_cameras.AddCamera(m_firstPersonTranslateCamera); m_cameraSystem->m_cameras.AddCamera(orbitCamera); + + // these tests rely on using motion delta, not cursor positions (default is true) + AzFramework::ed_cameraSystemUseCursor = false; } void TearDown() override { + AzFramework::ed_cameraSystemUseCursor = true; + m_firstPersonRotateCamera.reset(); m_firstPersonTranslateCamera.reset(); diff --git a/Code/Framework/AzFramework/Tests/Mocks/MockWindowRequests.h b/Code/Framework/AzFramework/Tests/Mocks/MockWindowRequests.h new file mode 100644 index 0000000000..63f73d0b28 --- /dev/null +++ b/Code/Framework/AzFramework/Tests/Mocks/MockWindowRequests.h @@ -0,0 +1,41 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#pragma once + +#include + +#include + +namespace UnitTest +{ + class MockWindowRequests : public AzFramework::WindowRequestBus::Handler + { + public: + void Connect(AzFramework::NativeWindowHandle handle) + { + AzFramework::WindowRequestBus::Handler::BusConnect(handle); + } + void Disconnect() + { + AzFramework::WindowRequestBus::Handler::BusDisconnect(); + } + + // AzFramework::WindowRequestBus overrides ... + MOCK_METHOD1(SetWindowTitle, void(const AZStd::string&)); + MOCK_CONST_METHOD0(GetClientAreaSize, AzFramework::WindowSize()); + MOCK_METHOD1(ResizeClientArea, void(AzFramework::WindowSize clientAreaSize)); + MOCK_CONST_METHOD0(GetFullScreenState, bool()); + MOCK_METHOD1(SetFullScreenState, void(bool)); + MOCK_CONST_METHOD0(CanToggleFullScreenState, bool()); + MOCK_METHOD0(ToggleFullScreenState, void()); + MOCK_CONST_METHOD0(GetDpiScaleFactor, float()); + MOCK_CONST_METHOD0(GetSyncInterval, uint32_t()); + MOCK_CONST_METHOD0(GetDisplayRefreshRate, uint32_t()); + }; +} // namespace UnitTest diff --git a/Code/Framework/AzFramework/Tests/framework_shared_tests_files.cmake b/Code/Framework/AzFramework/Tests/framework_shared_tests_files.cmake index 3d2c2a51be..85c00a2e8a 100644 --- a/Code/Framework/AzFramework/Tests/framework_shared_tests_files.cmake +++ b/Code/Framework/AzFramework/Tests/framework_shared_tests_files.cmake @@ -8,6 +8,7 @@ set(FILES Mocks/MockSpawnableEntitiesInterface.h + Mocks/MockWindowRequests.h Utils/Utils.h Utils/Utils.cpp FrameworkApplicationFixture.h diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Input/QtEventToAzInputManager.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/Input/QtEventToAzInputManager.cpp index b7776238ba..260270e85e 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Input/QtEventToAzInputManager.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Input/QtEventToAzInputManager.cpp @@ -285,7 +285,7 @@ namespace AzToolsFramework } } - void QtEventToAzInputMapper::ProcessPendingMouseEvents() + void QtEventToAzInputMapper::ProcessPendingMouseEvents(const QPoint& cursorDelta) { auto systemCursorChannel = GetInputChannel(AzFramework::InputDeviceMouse::SystemCursorPosition); @@ -297,14 +297,8 @@ namespace AzToolsFramework GetInputChannel(AzFramework::InputDeviceMouse::Movement::Z); systemCursorChannel->ProcessRawInputEvent(m_mouseDevice->m_cursorPositionData2D->m_normalizedPositionDelta.GetLength()); - // Generate movement events based on the pixel delta divided by the DPI scaling factor, to calculate a rough approximation - // of cursor movement velocity. - movementXChannel->ProcessRawInputEvent( - m_mouseDevice->m_cursorPositionData2D->m_normalizedPositionDelta.GetX() * aznumeric_cast(m_sourceWidget->width()) / - m_sourceWidget->devicePixelRatioF()); - movementYChannel->ProcessRawInputEvent( - m_mouseDevice->m_cursorPositionData2D->m_normalizedPositionDelta.GetY() * aznumeric_cast(m_sourceWidget->height()) / - m_sourceWidget->devicePixelRatioF()); + movementXChannel->ProcessRawInputEvent(cursorDelta.x()); + movementYChannel->ProcessRawInputEvent(cursorDelta.y()); mouseWheelChannel->ProcessRawInputEvent(0.0f); NotifyUpdateChannelIfNotIdle(systemCursorChannel, nullptr); @@ -337,41 +331,43 @@ namespace AzToolsFramework } } - AZ::Vector2 QtEventToAzInputMapper::WidgetPositionToNormalizedPosition(QPoint position) + AZ::Vector2 QtEventToAzInputMapper::WidgetPositionToNormalizedPosition(const QPoint& position) { const float normalizedX = aznumeric_cast(position.x()) / aznumeric_cast(m_sourceWidget->width()); const float normalizedY = aznumeric_cast(position.y()) / aznumeric_cast(m_sourceWidget->height()); - return AZ::Vector2{normalizedX, normalizedY}; + return AZ::Vector2{ normalizedX, normalizedY }; } - QPoint QtEventToAzInputMapper::NormalizedPositionToWidgetPosition(AZ::Vector2 normalizedPosition) + QPoint QtEventToAzInputMapper::NormalizedPositionToWidgetPosition(const AZ::Vector2& normalizedPosition) { const int denormalizedX = aznumeric_cast(normalizedPosition.GetX() * m_sourceWidget->width()); const int denormalizedY = aznumeric_cast(normalizedPosition.GetY() * m_sourceWidget->height()); - return QPoint{denormalizedX, denormalizedY}; + return QPoint{ denormalizedX, denormalizedY }; } void QtEventToAzInputMapper::HandleMouseMoveEvent(QMouseEvent* mouseEvent) { - AZ::Vector2 lastCursorPosition = m_mouseDevice->m_cursorPositionData2D->m_normalizedPosition; + const QPoint cursorPosition = mouseEvent->pos(); + const QPoint cursorDelta = cursorPosition - m_previousCursorPosition; - const QPoint mousePos = mouseEvent->pos(); - const AZ::Vector2 normalizedPosition = WidgetPositionToNormalizedPosition(mousePos); - m_mouseDevice->m_cursorPositionData2D->m_normalizedPositionDelta = normalizedPosition - m_mouseDevice->m_cursorPositionData2D->m_normalizedPosition; - m_mouseDevice->m_cursorPositionData2D->m_normalizedPosition = normalizedPosition; - ProcessPendingMouseEvents(); + m_mouseDevice->m_cursorPositionData2D->m_normalizedPosition = WidgetPositionToNormalizedPosition(cursorPosition); + m_mouseDevice->m_cursorPositionData2D->m_normalizedPositionDelta = WidgetPositionToNormalizedPosition(cursorDelta); + + ProcessPendingMouseEvents(cursorDelta); if (m_capturingCursor) { // Reset our cursor position to the previous point. - QPoint targetScreenPosition = m_sourceWidget->mapToGlobal(NormalizedPositionToWidgetPosition(lastCursorPosition)); + const QPoint targetScreenPosition = m_sourceWidget->mapToGlobal(m_previousCursorPosition); AzQtComponents::SetCursorPos(targetScreenPosition); // Even though we just set the cursor position, there are edge cases such as remote desktop that will leave // the cursor position unchanged. For safety, we re-cache our last cursor position for delta generation. - QPoint actualWidgetPosition = m_sourceWidget->mapFromGlobal(QCursor::pos()); + const QPoint actualWidgetPosition = m_sourceWidget->mapFromGlobal(QCursor::pos()); m_mouseDevice->m_cursorPositionData2D->m_normalizedPosition = WidgetPositionToNormalizedPosition(actualWidgetPosition); } + + m_previousCursorPosition = cursorPosition; } void QtEventToAzInputMapper::HandleKeyEvent(QKeyEvent* keyEvent) diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Input/QtEventToAzInputManager.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Input/QtEventToAzInputManager.h index 0187cb2e5b..6e73bf4f9c 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Input/QtEventToAzInputManager.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Input/QtEventToAzInputManager.h @@ -21,6 +21,7 @@ #include #include +#include #endif //! defined(Q_MOC_RUN) class QWidget; @@ -111,12 +112,12 @@ namespace AzToolsFramework void NotifyUpdateChannelIfNotIdle(const AzFramework::InputChannel* channel, QEvent* event); // Processes any pending mouse movement events, this allows mouse movement channels to close themselves. - void ProcessPendingMouseEvents(); + void ProcessPendingMouseEvents(const QPoint& cursorDelta); // Converts a point in logical source widget space [0..m_sourceWidget->size()] to normalized [0..1] space. - AZ::Vector2 WidgetPositionToNormalizedPosition(QPoint position); + AZ::Vector2 WidgetPositionToNormalizedPosition(const QPoint& position); // Converts a point in normalized [0..1] space to logical source widget space [0..m_sourceWidget->size()]. - QPoint NormalizedPositionToWidgetPosition(AZ::Vector2 normalizedPosition); + QPoint NormalizedPositionToWidgetPosition(const AZ::Vector2& normalizedPosition); // Handle mouse click events. void HandleMouseButtonEvent(QMouseEvent* mouseEvent); @@ -148,6 +149,8 @@ namespace AzToolsFramework AZStd::unordered_set m_highPriorityKeys; // A lookup table for AZ input channel ID -> physical input channel on our mouse or keyboard device. AZStd::unordered_map m_channels; + // Where the position of the mouse cursor was at the last cursor event. + QPoint m_previousCursorPosition; // The source widget to map events from, used to calculate the relative mouse position within the widget bounds. QWidget* m_sourceWidget; // Flags whether or not Qt events should currently be processed. diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/ViewportSelection/EditorDefaultSelection.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/ViewportSelection/EditorDefaultSelection.cpp index 0eae7707bc..d7dd008c9e 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/ViewportSelection/EditorDefaultSelection.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/ViewportSelection/EditorDefaultSelection.cpp @@ -188,7 +188,7 @@ namespace AzToolsFramework return false; } - using namespace AzToolsFramework::ViewportInteraction; + using AzToolsFramework::ViewportInteraction::MouseEvent; const auto& mouseInteraction = mouseInteractionEvent.m_mouseInteraction; // store the current interaction for use in DrawManipulators m_currentInteraction = mouseInteraction; @@ -196,28 +196,19 @@ namespace AzToolsFramework switch (mouseInteractionEvent.m_mouseEvent) { case MouseEvent::Down: - { - return m_manipulatorManager->ConsumeViewportMousePress(mouseInteraction); - } + return m_manipulatorManager->ConsumeViewportMousePress(mouseInteraction); case MouseEvent::DoubleClick: - { - return false; - } + return false; case MouseEvent::Move: { - AzToolsFramework::ManipulatorManager::ConsumeMouseMoveResult mouseMoveResult = - AzToolsFramework::ManipulatorManager::ConsumeMouseMoveResult::None; - mouseMoveResult = m_manipulatorManager->ConsumeViewportMouseMove(mouseInteraction); + const AzToolsFramework::ManipulatorManager::ConsumeMouseMoveResult mouseMoveResult = + m_manipulatorManager->ConsumeViewportMouseMove(mouseInteraction); return mouseMoveResult == AzToolsFramework::ManipulatorManager::ConsumeMouseMoveResult::Interacting; } case MouseEvent::Up: - { - return m_manipulatorManager->ConsumeViewportMouseRelease(mouseInteraction); - } + return m_manipulatorManager->ConsumeViewportMouseRelease(mouseInteraction); case MouseEvent::Wheel: - { - return m_manipulatorManager->ConsumeViewportMouseWheel(mouseInteraction); - } + return m_manipulatorManager->ConsumeViewportMouseWheel(mouseInteraction); default: return false; } diff --git a/Gems/Atom/Tools/AtomToolsFramework/Code/Include/AtomToolsFramework/Viewport/ModularViewportCameraController.h b/Gems/Atom/Tools/AtomToolsFramework/Code/Include/AtomToolsFramework/Viewport/ModularViewportCameraController.h index e6a666c640..42fe9a01c9 100644 --- a/Gems/Atom/Tools/AtomToolsFramework/Code/Include/AtomToolsFramework/Viewport/ModularViewportCameraController.h +++ b/Gems/Atom/Tools/AtomToolsFramework/Code/Include/AtomToolsFramework/Viewport/ModularViewportCameraController.h @@ -18,9 +18,22 @@ namespace AtomToolsFramework { class ModularViewportCameraControllerInstance; + //! A reduced ViewportContext interface for use by the ModularViewportCameraController. + //! @note This extra indirection is used to facilitate testing the ModularViewportCameraController. + class ModularCameraViewportContext + { + public: + virtual ~ModularCameraViewportContext() = default; + + virtual AZ::Transform GetCameraTransform() const = 0; + virtual void SetCameraTransform(const AZ::Transform& transform) = 0; + virtual void ConnectViewMatrixChangedHandler(AZ::RPI::ViewportContext::MatrixChangedEvent::Handler& handler) = 0; + }; + //! A function object to represent returning a camera controller priority. using CameraControllerPriorityFn = AZStd::function; + using CameraViewportContextFn = AZStd::function(AzFramework::ViewportId)>; //! The default behavior for what priority the camera controller should respond to events at. //! @note This can change based on the state of the camera controller/system. @@ -38,6 +51,7 @@ namespace AtomToolsFramework using CameraListBuilder = AZStd::function; using CameraPropsBuilder = AZStd::function; using CameraPriorityBuilder = AZStd::function; + using CameraViewportContextBuilder = AZStd::function&)>; //! Sets the camera list builder callback used to populate new ModularViewportCameraControllerInstances. void SetCameraListBuilderCallback(const CameraListBuilder& builder); @@ -45,6 +59,8 @@ namespace AtomToolsFramework void SetCameraPropsBuilderCallback(const CameraPropsBuilder& builder); //! Sets the camera controller priority builder callback used to populate new ModularViewportCameraControllerInstances. void SetCameraPriorityBuilderCallback(const CameraPriorityBuilder& builder); + //! Sets the camera controller viewport context builder callback to populate new ModularViewportCameraControllerInstances. + void SetCameraViewportContextBuilderCallback(const CameraViewportContextBuilder& builder); private: //! Sets up a camera list based on this controller's CameraListBuilderCallback. @@ -53,6 +69,8 @@ namespace AtomToolsFramework void SetupCameraProperties(AzFramework::CameraProps& cameraProps); //! Sets up how the camera controller should decide at what priority level to respond to. void SetupCameraControllerPriority(CameraControllerPriorityFn& cameraPriorityFn); + //! Sets up what viewport context should be used by the camera controller. + void SetupCameraControllerViewportContext(AZStd::unique_ptr& cameraViewportContext); //! Builder to generate a list of CameraInputs to run in the ModularViewportCameraControllerInstance. CameraListBuilder m_cameraListBuilder; @@ -60,6 +78,24 @@ namespace AtomToolsFramework CameraPropsBuilder m_cameraPropsBuilder; //! Builder to define what priority level the camera controller should respond to events at. CameraPriorityBuilder m_cameraControllerPriorityBuilder; + //! Builder to define what viewport context interface the camera controller should use. + CameraViewportContextBuilder m_cameraViewportContextBuilder; + }; + + //! The production modular camera viewport context backed by an AZ::RPI::ViewportContextPtr. + //! @note This is instantiated during normal runtime use. + class ModularCameraViewportContextImpl : public ModularCameraViewportContext + { + public: + explicit ModularCameraViewportContextImpl(AzFramework::ViewportId viewportId); + + // ModularCameraViewportContext overrides ... + AZ::Transform GetCameraTransform() const override; + void SetCameraTransform(const AZ::Transform& transform) override; + void ConnectViewMatrixChangedHandler(AZ::RPI::ViewportContext::MatrixChangedEvent::Handler& handler) override; + + private: + AzFramework::ViewportId m_viewportId; }; //! A customizable camera controller that can be configured to run a varying set of CameraInput instances. @@ -115,5 +151,7 @@ namespace AtomToolsFramework bool m_updatingTransformInternally = false; //! Listen for camera view changes outside of the camera controller. AZ::RPI::ViewportContext::MatrixChangedEvent::Handler m_cameraViewMatrixChangeHandler; + //! The current instance of the modular camera viewport context. + AZStd::unique_ptr m_modularCameraViewportContext; }; } // namespace AtomToolsFramework diff --git a/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Viewport/ModularViewportCameraController.cpp b/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Viewport/ModularViewportCameraController.cpp index e98df83930..0fc55e2363 100644 --- a/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Viewport/ModularViewportCameraController.cpp +++ b/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Viewport/ModularViewportCameraController.cpp @@ -41,6 +41,7 @@ namespace AtomToolsFramework display.DrawLine(transform.GetTranslation(), transform.GetTranslation() + transform.GetBasisZ().GetNormalizedSafe() * axisLength); } + // convenience function to access the ViewportContext for the given ViewportId. static AZ::RPI::ViewportContextPtr RetrieveViewportContext(const AzFramework::ViewportId viewportId) { auto viewportContextManager = AZ::Interface::Get(); @@ -58,6 +59,35 @@ namespace AtomToolsFramework return viewportContext; } + ModularCameraViewportContextImpl::ModularCameraViewportContextImpl(const AzFramework::ViewportId viewportId) + : m_viewportId(viewportId) + { + } + + AZ::Transform ModularCameraViewportContextImpl::GetCameraTransform() const + { + if (auto viewportContext = RetrieveViewportContext(m_viewportId)) + { + return viewportContext->GetCameraTransform(); + } + + return AZ::Transform::CreateIdentity(); + } + void ModularCameraViewportContextImpl::SetCameraTransform(const AZ::Transform& transform) + { + if (auto viewportContext = RetrieveViewportContext(m_viewportId)) + { + viewportContext->SetCameraTransform(transform); + } + } + void ModularCameraViewportContextImpl::ConnectViewMatrixChangedHandler(AZ::RPI::ViewportContext::MatrixChangedEvent::Handler& handler) + { + if (auto viewportContext = RetrieveViewportContext(m_viewportId)) + { + viewportContext->ConnectViewMatrixChangedHandler(handler); + } + } + void ModularViewportCameraController::SetCameraListBuilderCallback(const CameraListBuilder& builder) { m_cameraListBuilder = builder; @@ -73,6 +103,11 @@ namespace AtomToolsFramework m_cameraControllerPriorityBuilder = builder; } + void ModularViewportCameraController::SetCameraViewportContextBuilderCallback(const CameraViewportContextBuilder& builder) + { + m_cameraViewportContextBuilder = builder; + } + void ModularViewportCameraController::SetupCameras(AzFramework::Cameras& cameras) { if (m_cameraListBuilder) @@ -97,6 +132,15 @@ namespace AtomToolsFramework } } + void ModularViewportCameraController::SetupCameraControllerViewportContext( + AZStd::unique_ptr& cameraViewportContext) + { + if (m_cameraViewportContextBuilder) + { + m_cameraViewportContextBuilder(cameraViewportContext); + } + } + // what priority should the camera system respond to AzFramework::ViewportControllerPriority DefaultCameraControllerPriority(const AzFramework::CameraSystem& cameraSystem) { @@ -119,23 +163,20 @@ namespace AtomToolsFramework controller->SetupCameras(m_cameraSystem.m_cameras); controller->SetupCameraProperties(m_cameraProps); controller->SetupCameraControllerPriority(m_priorityFn); + controller->SetupCameraControllerViewportContext(m_modularCameraViewportContext); - if (auto viewportContext = RetrieveViewportContext(GetViewportId())) + auto handleCameraChange = [this](const AZ::Matrix4x4&) { - auto handleCameraChange = [this, viewportContext](const AZ::Matrix4x4&) + // ignore these updates if the camera is being updated internally + if (!m_updatingTransformInternally) { - // ignore these updates if the camera is being updated internally - if (!m_updatingTransformInternally) - { - UpdateCameraFromTransform(m_targetCamera, viewportContext->GetCameraTransform()); - m_camera = m_targetCamera; - } - }; + UpdateCameraFromTransform(m_targetCamera, m_modularCameraViewportContext->GetCameraTransform()); + m_camera = m_targetCamera; + } + }; - m_cameraViewMatrixChangeHandler = AZ::RPI::ViewportContext::MatrixChangedEvent::Handler(handleCameraChange); - - viewportContext->ConnectViewMatrixChangedHandler(m_cameraViewMatrixChangeHandler); - } + m_cameraViewMatrixChangeHandler = AZ::RPI::ViewportContext::MatrixChangedEvent::Handler(handleCameraChange); + m_modularCameraViewportContext->ConnectViewMatrixChangedHandler(m_cameraViewMatrixChangeHandler); AzFramework::ViewportDebugDisplayEventBus::Handler::BusConnect(AzToolsFramework::GetEntityContextId()); ModularViewportCameraControllerRequestBus::Handler::BusConnect(viewportId); @@ -151,7 +192,11 @@ namespace AtomToolsFramework { if (event.m_priority == m_priorityFn(m_cameraSystem)) { - return m_cameraSystem.HandleEvents(AzFramework::BuildInputEvent(event.m_inputChannel)); + AzFramework::WindowSize windowSize; + AzFramework::WindowRequestBus::EventResult( + windowSize, event.m_windowHandle, &AzFramework::WindowRequestBus::Events::GetClientAreaSize); + + return m_cameraSystem.HandleEvents(AzFramework::BuildInputEvent(event.m_inputChannel, windowSize)); } return false; @@ -165,61 +210,58 @@ namespace AtomToolsFramework return; } - if (auto viewportContext = RetrieveViewportContext(GetViewportId())) + m_updatingTransformInternally = true; + + if (m_cameraMode == CameraMode::Control) { - m_updatingTransformInternally = true; + m_targetCamera = m_cameraSystem.StepCamera(m_targetCamera, event.m_deltaTime.count()); + m_camera = AzFramework::SmoothCamera(m_camera, m_targetCamera, m_cameraProps, event.m_deltaTime.count()); - if (m_cameraMode == CameraMode::Control) + // if there has been an interpolation, only clear the look at point if it is no longer + // centered in the view (the camera has looked away from it) + if (m_lookAtAfterInterpolation.has_value()) { - m_targetCamera = m_cameraSystem.StepCamera(m_targetCamera, event.m_deltaTime.count()); - m_camera = AzFramework::SmoothCamera(m_camera, m_targetCamera, m_cameraProps, event.m_deltaTime.count()); - - // if there has been an interpolation, only clear the look at point if it is no longer - // centered in the view (the camera has looked away from it) - if (m_lookAtAfterInterpolation.has_value()) + if (const float lookDirection = + (*m_lookAtAfterInterpolation - m_camera.Translation()).GetNormalized().Dot(m_camera.Transform().GetBasisY()); + !AZ::IsCloseMag(lookDirection, 1.0f, 0.001f)) { - if (const float lookDirection = - (*m_lookAtAfterInterpolation - m_camera.Translation()).GetNormalized().Dot(m_camera.Transform().GetBasisY()); - !AZ::IsCloseMag(lookDirection, 1.0f, 0.001f)) - { - m_lookAtAfterInterpolation = {}; - } + m_lookAtAfterInterpolation = {}; } - - viewportContext->SetCameraTransform(m_camera.Transform()); - } - else if (m_cameraMode == CameraMode::Animation) - { - const auto smootherStepFn = [](const float t) - { - return t * t * t * (t * (t * 6.0f - 15.0f) + 10.0f); - }; - - const auto& [transformStart, transformEnd, animationTime] = m_cameraAnimation; - - const float transitionTime = smootherStepFn(animationTime); - const AZ::Transform current = AZ::Transform::CreateFromQuaternionAndTranslation( - transformStart.GetRotation().Slerp(transformEnd.GetRotation(), transitionTime), - transformStart.GetTranslation().Lerp(transformEnd.GetTranslation(), transitionTime)); - - const AZ::Vector3 eulerAngles = AzFramework::EulerAngles(AZ::Matrix3x3::CreateFromTransform(current)); - m_camera.m_pitch = eulerAngles.GetX(); - m_camera.m_yaw = eulerAngles.GetZ(); - m_camera.m_lookAt = current.GetTranslation(); - m_targetCamera = m_camera; - - if (animationTime >= 1.0f) - { - m_cameraMode = CameraMode::Control; - } - - m_cameraAnimation.m_time = AZ::GetClamp(animationTime + event.m_deltaTime.count(), 0.0f, 1.0f); - - viewportContext->SetCameraTransform(current); } - m_updatingTransformInternally = false; + m_modularCameraViewportContext->SetCameraTransform(m_camera.Transform()); } + else if (m_cameraMode == CameraMode::Animation) + { + const auto smootherStepFn = [](const float t) + { + return t * t * t * (t * (t * 6.0f - 15.0f) + 10.0f); + }; + + const auto& [transformStart, transformEnd, animationTime] = m_cameraAnimation; + + const float transitionTime = smootherStepFn(animationTime); + const AZ::Transform current = AZ::Transform::CreateFromQuaternionAndTranslation( + transformStart.GetRotation().Slerp(transformEnd.GetRotation(), transitionTime), + transformStart.GetTranslation().Lerp(transformEnd.GetTranslation(), transitionTime)); + + const AZ::Vector3 eulerAngles = AzFramework::EulerAngles(AZ::Matrix3x3::CreateFromTransform(current)); + m_camera.m_pitch = eulerAngles.GetX(); + m_camera.m_yaw = eulerAngles.GetZ(); + m_camera.m_lookAt = current.GetTranslation(); + m_targetCamera = m_camera; + + if (animationTime >= 1.0f) + { + m_cameraMode = CameraMode::Control; + } + + m_cameraAnimation.m_time = AZ::GetClamp(animationTime + event.m_deltaTime.count(), 0.0f, 1.0f); + + m_modularCameraViewportContext->SetCameraTransform(current); + } + + m_updatingTransformInternally = false; } void ModularViewportCameraControllerInstance::DisplayViewport(