Переглянути джерело

Ensure camera inputs are cleared when the viewport loses focus (#13433)

* ensure camera inputs are cleared when the viewport loses focus

Signed-off-by: Tom Hulton-Harrop <[email protected]>

* small comment update

Signed-off-by: Tom Hulton-Harrop <[email protected]>

Signed-off-by: Tom Hulton-Harrop <[email protected]>
Tom Hulton-Harrop 2 роки тому
батько
коміт
b6a24915fc

+ 1 - 1
Code/Editor/EditorModularViewportCameraComposerBus.h

@@ -27,5 +27,5 @@ namespace SandboxEditor
     };
 
     using EditorModularViewportCameraComposerNotificationBus =
-        AZ::EBus<EditorModularViewportCameraComposerNotifications, AzToolsFramework::ViewportInteraction::ViewportEBusTraits>;
+        AZ::EBus<EditorModularViewportCameraComposerNotifications, AzToolsFramework::ViewportInteraction::ViewportNotificationsEBusTraits>;
 } // namespace SandboxEditor

+ 66 - 28
Code/Editor/Lib/Tests/test_ModularViewportCameraController.cpp

@@ -97,20 +97,20 @@ namespace UnitTest
         {
             LeakDetectionFixture::SetUp();
 
-            m_rootWidget = AZStd::make_unique<QWidget>();
+            m_rootWidget = new QWidget();
             // set root widget to the the active window to ensure focus in/out events are fired
-            QApplication::setActiveWindow(m_rootWidget.get());
+            QApplication::setActiveWindow(m_rootWidget);
             m_rootWidget->setFixedSize(WidgetSize);
             m_rootWidget->move(0, 0); // explicitly set the widget to be in the upper left corner
 
-            m_otherWidget = AZStd::make_unique<QWidget>();
+            m_otherWidget = new QWidget(m_rootWidget);
             m_otherWidget->setFixedSize(WidgetSize / 2);
             m_otherWidget->move(WidgetSize.width(), 0); // move widget to right of root widget
 
             m_controllerList = AZStd::make_shared<AzFramework::ViewportControllerList>();
             m_controllerList->RegisterViewportContext(TestViewportId);
 
-            m_inputChannelMapper = AZStd::make_unique<AzToolsFramework::QtEventToAzInputMapper>(m_rootWidget.get(), TestViewportId);
+            m_inputChannelMapper = AZStd::make_unique<AzToolsFramework::QtEventToAzInputMapper>(m_rootWidget, TestViewportId);
 
             m_settingsRegistry = AZStd::make_unique<AZ::SettingsRegistryImpl>();
             AZ::SettingsRegistry::Register(m_settingsRegistry.get());
@@ -127,8 +127,7 @@ namespace UnitTest
             m_controllerList.reset();
 
             QApplication::setActiveWindow(nullptr);
-            m_otherWidget.reset();
-            m_rootWidget.reset();
+            delete m_rootWidget;
 
             LeakDetectionFixture::TearDown();
         }
@@ -141,7 +140,7 @@ namespace UnitTest
             QObject::connect(
                 m_inputChannelMapper.get(),
                 &AzToolsFramework::QtEventToAzInputMapper::InputChannelUpdated,
-                m_rootWidget.get(),
+                m_rootWidget,
                 [this, nativeWindowHandle](const AzFramework::InputChannel* inputChannel, [[maybe_unused]] QEvent* event)
                 {
                     m_controllerList->HandleInputChannelEvent(
@@ -214,7 +213,7 @@ namespace UnitTest
         {
             // move to the center of the screen
             const auto start = QPoint(WidgetSize.width() / 2, WidgetSize.height() / 2);
-            MouseMove(m_rootWidget.get(), start, QPoint(0, 0));
+            MouseMove(m_rootWidget, start, QPoint(0, 0));
             m_controllerList->UpdateViewport({ TestViewportId, AzFramework::FloatSeconds(deltaTimeFn()), AZ::ScriptTimePoint() });
 
             // move mouse diagonally to top right, then to bottom left and back repeatedly
@@ -225,7 +224,7 @@ namespace UnitTest
             {
                 for (int i = 0; i < iterationsPerDiagonal; ++i)
                 {
-                    MousePressAndMove(m_rootWidget.get(), current, halfDelta / iterationsPerDiagonal, Qt::MouseButton::RightButton);
+                    MousePressAndMove(m_rootWidget, current, halfDelta / iterationsPerDiagonal, Qt::MouseButton::RightButton);
                     m_controllerList->UpdateViewport({ TestViewportId, AzFramework::FloatSeconds(deltaTimeFn()), AZ::ScriptTimePoint() });
                     current += halfDelta / iterationsPerDiagonal;
                 }
@@ -237,13 +236,13 @@ namespace UnitTest
                 }
             }
 
-            QTest::mouseRelease(m_rootWidget.get(), Qt::MouseButton::RightButton, Qt::KeyboardModifier::NoModifier, current);
+            QTest::mouseRelease(m_rootWidget, Qt::MouseButton::RightButton, Qt::KeyboardModifier::NoModifier, current);
             m_controllerList->UpdateViewport({ TestViewportId, AzFramework::FloatSeconds(deltaTimeFn()), AZ::ScriptTimePoint() });
         }
 
         ::testing::NiceMock<MockViewportInteractionRequests> m_mockViewportInteractionRequests;
-        AZStd::unique_ptr<QWidget> m_rootWidget;
-        AZStd::unique_ptr<QWidget> m_otherWidget;
+        QWidget* m_rootWidget = nullptr;
+        QWidget* m_otherWidget = nullptr;;
         AzFramework::ViewportControllerListPtr m_controllerList;
         AZStd::unique_ptr<AzToolsFramework::QtEventToAzInputMapper> m_inputChannelMapper;
         ::testing::NiceMock<MockWindowRequests> m_mockWindowRequests;
@@ -323,30 +322,30 @@ namespace UnitTest
         // When
         // move to the center of the screen
         auto start = QPoint(WidgetSize.width() / 2, WidgetSize.height() / 2);
-        MouseMove(m_rootWidget.get(), start, QPoint(0, 0));
+        MouseMove(m_rootWidget, start, QPoint(0, 0));
         m_controllerList->UpdateViewport({ TestViewportId, AzFramework::FloatSeconds(DeltaTime), AZ::ScriptTimePoint() });
 
         const auto mouseDelta = QPoint(5, 0);
 
         // initial movement to begin the camera behavior
-        MousePressAndMove(m_rootWidget.get(), start, mouseDelta, Qt::MouseButton::RightButton);
+        MousePressAndMove(m_rootWidget, start, mouseDelta, Qt::MouseButton::RightButton);
         m_controllerList->UpdateViewport({ TestViewportId, AzFramework::FloatSeconds(DeltaTime), AZ::ScriptTimePoint() });
 
         // move the cursor right
         for (int i = 0; i < 50; ++i)
         {
-            MousePressAndMove(m_rootWidget.get(), start + mouseDelta, mouseDelta, Qt::MouseButton::RightButton);
+            MousePressAndMove(m_rootWidget, start + mouseDelta, mouseDelta, Qt::MouseButton::RightButton);
             m_controllerList->UpdateViewport({ TestViewportId, AzFramework::FloatSeconds(DeltaTime), AZ::ScriptTimePoint() });
         }
 
         // move the cursor left (do an extra iteration moving left to account for the initial dead-zone)
         for (int i = 0; i < 51; ++i)
         {
-            MousePressAndMove(m_rootWidget.get(), start + mouseDelta, -mouseDelta, Qt::MouseButton::RightButton);
+            MousePressAndMove(m_rootWidget, start + mouseDelta, -mouseDelta, Qt::MouseButton::RightButton);
             m_controllerList->UpdateViewport({ TestViewportId, AzFramework::FloatSeconds(DeltaTime), AZ::ScriptTimePoint() });
         }
 
-        QTest::mouseRelease(m_rootWidget.get(), Qt::MouseButton::RightButton, Qt::KeyboardModifier::NoModifier, start + mouseDelta);
+        QTest::mouseRelease(m_rootWidget, Qt::MouseButton::RightButton, Qt::KeyboardModifier::NoModifier, start + mouseDelta);
         m_controllerList->UpdateViewport({ TestViewportId, AzFramework::FloatSeconds(DeltaTime), AZ::ScriptTimePoint() });
 
         // Then
@@ -371,12 +370,12 @@ namespace UnitTest
         // When
         // move to the center of the screen
         auto start = QPoint(WidgetSize.width() / 2, WidgetSize.height() / 2);
-        MouseMove(m_rootWidget.get(), start, QPoint(0, 0));
+        MouseMove(m_rootWidget, start, QPoint(0, 0));
         m_controllerList->UpdateViewport({ TestViewportId, AzFramework::FloatSeconds(DeltaTime), AZ::ScriptTimePoint() });
 
         // will move a small amount initially
         const auto mouseDelta = QPoint(5, 0);
-        MousePressAndMove(m_rootWidget.get(), start, mouseDelta, Qt::MouseButton::RightButton);
+        MousePressAndMove(m_rootWidget, start, mouseDelta, Qt::MouseButton::RightButton);
 
         // ensure further updates to not continue to rotate
         for (int i = 0; i < 50; ++i)
@@ -407,15 +406,15 @@ namespace UnitTest
         // When
         // move cursor to the center of the screen
         auto start = QPoint(WidgetSize.width() / 2, WidgetSize.height() / 2);
-        MouseMove(m_rootWidget.get(), start, QPoint(0, 0));
+        MouseMove(m_rootWidget, start, QPoint(0, 0));
         m_controllerList->UpdateViewport({ TestViewportId, AzFramework::FloatSeconds(DeltaTime), AZ::ScriptTimePoint() });
 
         // move camera right
         const auto mouseDelta = QPoint(200, 0);
-        MousePressAndMove(m_rootWidget.get(), start, mouseDelta, Qt::MouseButton::RightButton);
+        MousePressAndMove(m_rootWidget, start, mouseDelta, Qt::MouseButton::RightButton);
         m_controllerList->UpdateViewport({ TestViewportId, AzFramework::FloatSeconds(DeltaTime), AZ::ScriptTimePoint() });
 
-        QTest::mouseRelease(m_rootWidget.get(), Qt::MouseButton::RightButton, Qt::NoModifier, start + mouseDelta);
+        QTest::mouseRelease(m_rootWidget, Qt::MouseButton::RightButton, Qt::NoModifier, start + mouseDelta);
         m_controllerList->UpdateViewport({ TestViewportId, AzFramework::FloatSeconds(DeltaTime), AZ::ScriptTimePoint() });
 
         // update the position of the widget
@@ -423,11 +422,11 @@ namespace UnitTest
         m_rootWidget->move(offset);
 
         // move cursor back to widget center
-        MouseMove(m_rootWidget.get(), start, QPoint(0, 0));
+        MouseMove(m_rootWidget, start, QPoint(0, 0));
         m_controllerList->UpdateViewport({ TestViewportId, AzFramework::FloatSeconds(DeltaTime), AZ::ScriptTimePoint() });
 
         // move camera left
-        MousePressAndMove(m_rootWidget.get(), start, -mouseDelta, Qt::MouseButton::RightButton);
+        MousePressAndMove(m_rootWidget, start, -mouseDelta, Qt::MouseButton::RightButton);
         m_controllerList->UpdateViewport({ TestViewportId, AzFramework::FloatSeconds(DeltaTime), AZ::ScriptTimePoint() });
 
         // Then
@@ -463,7 +462,7 @@ namespace UnitTest
         // When
         // press alt without main viewport in focus
         m_otherWidget->setFocus();
-        QTest::keyPress(m_otherWidget.get(), Qt::Key::Key_Alt);
+        QTest::keyPress(m_otherWidget, Qt::Key::Key_Alt);
         m_controllerList->UpdateViewport({ TestViewportId, AzFramework::FloatSeconds(DeltaTime), AZ::ScriptTimePoint() });
 
         // change focus
@@ -471,19 +470,19 @@ namespace UnitTest
 
         // move cursor to the center of the screen
         auto start = QPoint(WidgetSize.width() / 2, WidgetSize.height() / 2);
-        MouseMove(m_rootWidget.get(), start, QPoint(0, 0));
+        MouseMove(m_rootWidget, start, QPoint(0, 0));
         m_controllerList->UpdateViewport({ TestViewportId, AzFramework::FloatSeconds(DeltaTime), AZ::ScriptTimePoint() });
 
         // update starting position of mouse cursor request (if needed later)
         m_viewportMouseCursorRequests.m_mousePosition = AzToolsFramework::ViewportInteraction::ScreenPointFromQPoint(start);
 
         // start a mouse press and update the viewport
-        QTest::mousePress(m_rootWidget.get(), Qt::MouseButton::LeftButton, Qt::NoModifier, start);
+        QTest::mousePress(m_rootWidget, Qt::MouseButton::LeftButton, Qt::NoModifier, start);
         m_controllerList->UpdateViewport({ TestViewportId, AzFramework::FloatSeconds(DeltaTime), AZ::ScriptTimePoint() });
 
         // move mouse right and perform a camera orbit (with left mouse button held from before)
         const auto mouseDelta = QPoint(200, 0);
-        MouseMove(m_rootWidget.get(), start, mouseDelta);
+        MouseMove(m_rootWidget, start, mouseDelta);
         m_controllerList->UpdateViewport({ TestViewportId, AzFramework::FloatSeconds(DeltaTime), AZ::ScriptTimePoint() });
 
         // Then
@@ -494,4 +493,43 @@ namespace UnitTest
         // Clean-up
         HaltCollaborators();
     }
+
+    TEST_F(ModularViewportCameraControllerFixture, CameraSystemStopsMovingWhenViewportLosesFocus)
+    {
+        SandboxEditor::SetCameraCaptureCursorForLook(false);
+
+        // Given
+        PrepareCollaborators();
+
+        // ensure widgets are showing to make sure focus in/out events are fired correctly
+        m_rootWidget->setVisible(true);
+        m_otherWidget->setVisible(true);
+
+        // store initial camera translation and rotation
+        const AZ::Vector3 cameraTranslation = m_cameraViewportContextView->GetCameraTransform().GetTranslation();
+
+        // change focus to main widget
+        m_rootWidget->setFocus();
+
+        // start moving the camera left
+        QTest::keyPress(m_rootWidget, Qt::Key::Key_A);
+        // update the viewport
+        m_controllerList->UpdateViewport({ TestViewportId, AzFramework::FloatSeconds(1.0f), AZ::ScriptTimePoint() });
+
+        // ensure the camera moved from its initial position
+        const AZ::Vector3 nextCameraTranslation = m_cameraViewportContextView->GetCameraTransform().GetTranslation();
+        EXPECT_THAT(nextCameraTranslation, ::testing::Not(IsClose(cameraTranslation)));
+
+        // move focus to the other widget
+        m_otherWidget->setFocus();
+        // update the viewport
+        m_controllerList->UpdateViewport({ TestViewportId, AzFramework::FloatSeconds(1.0f), AZ::ScriptTimePoint() });
+
+        // ensure the camera did not move from its last position
+        const AZ::Vector3 lastCameraTranslation = m_cameraViewportContextView->GetCameraTransform().GetTranslation();
+        EXPECT_THAT(lastCameraTranslation, IsClose(nextCameraTranslation));
+
+        // Clean-up
+        HaltCollaborators();
+    }
 } // namespace UnitTest

+ 2 - 4
Code/Framework/AzFramework/AzFramework/Viewport/CameraInput.cpp

@@ -8,8 +8,6 @@
 
 #include "CameraInput.h"
 
-#include <AzCore/Math/MathUtils.h>
-#include <AzCore/Math/Plane.h>
 #include <AzCore/std/numeric.h>
 #include <AzFramework/Input/Devices/Keyboard/InputDeviceKeyboard.h>
 #include <AzFramework/Input/Devices/Mouse/InputDeviceMouse.h>
@@ -134,7 +132,7 @@ namespace AzFramework
         camera.m_offset = AZ::Vector3::CreateZero();
     }
 
-    float SmoothValueTime(const float smoothness, float deltaTime)
+    float SmoothValueTime(const float smoothness, const float deltaTime)
     {
         // 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
@@ -532,7 +530,7 @@ namespace AzFramework
     }
 
     bool TranslateCameraInput::HandleEvents(
-        const InputState& state, [[maybe_unused]] const ScreenVector& cursorDelta, [[maybe_unused]] float scrollDelta)
+        const InputState& state, [[maybe_unused]] const ScreenVector& cursorDelta, [[maybe_unused]] const float scrollDelta)
     {
         if (const auto& input = AZStd::get_if<DiscreteInputEvent>(&state.m_inputEvent))
         {

+ 5 - 1
Code/Framework/AzFramework/AzFramework/Viewport/CameraInput.h

@@ -18,7 +18,6 @@
 #include <AzFramework/Viewport/ClickDetector.h>
 #include <AzFramework/Viewport/CursorState.h>
 #include <AzFramework/Viewport/ScreenGeometry.h>
-#include <AzFramework/Viewport/ViewportId.h>
 
 namespace AzFramework
 {
@@ -684,6 +683,11 @@ namespace AzFramework
     private:
         InputChannelId m_orbitChannelId; //!< Input channel to begin the orbit camera input (note: A modifier key is preferred).
         PivotFn m_pivotFn; //!< The pivot position to use for this orbit camera (how is the pivot point calculated/retrieved).
+
+        void ResetImpl() override
+        {
+            m_orbitCameras.Reset();
+        }
     };
 
     inline void OrbitCameraInput::SetPivotFn(PivotFn pivotFn)

+ 10 - 0
Code/Framework/AzToolsFramework/AzToolsFramework/Input/QtEventToAzInputMapper.cpp

@@ -233,6 +233,8 @@ namespace AzToolsFramework
         // Install a global event filter to ensure we don't miss mouse and key release events.
         QApplication::instance()->installEventFilter(this);
         AzFramework::InputChannelNotificationBus::Handler::BusConnect();
+
+        m_viewportId = syntheticDeviceId;
     }
 
     bool QtEventToAzInputMapper::HandlesInputEvent(const AzFramework::InputChannel& channel) const
@@ -337,12 +339,20 @@ namespace AzToolsFramework
             // ensures cursor positions are refreshed correctly with context menu focus changes)
             if (eventType == QEvent::FocusIn)
             {
+                ViewportInteraction::ViewportInteractionNotificationBus::Event(
+                    m_viewportId, &ViewportInteraction::ViewportInteractionNotificationBus::Events::OnViewportFocusIn);
+
                 const auto globalCursorPosition = QCursor::pos();
                 if (m_sourceWidget->geometry().contains(m_sourceWidget->mapFromGlobal(globalCursorPosition)))
                 {
                     HandleMouseMoveEvent(globalCursorPosition);
                 }
             }
+            else if (eventType == QEvent::FocusOut)
+            {
+                ViewportInteraction::ViewportInteractionNotificationBus::Event(
+                    m_viewportId, &ViewportInteraction::ViewportInteractionNotificationBus::Events::OnViewportFocusOut);
+            }
         }
         // Map key events to input channels.
         // ShortcutOverride is used in lieu of KeyPress for high priority input channels like Alt

+ 6 - 4
Code/Framework/AzToolsFramework/AzToolsFramework/Input/QtEventToAzInputMapper.h

@@ -44,8 +44,8 @@ namespace AzToolsFramework
         CursorModeWrappedY //!< Flags whether the cursor is going to wrap around the source widget only on the top and bottom side.
     };
 
-    AzFramework::InputDeviceId GetSyntheticKeyboardDeviceId(const AzFramework::ViewportId viewportId);
-    AzFramework::InputDeviceId GetSyntheticMouseDeviceId(const AzFramework::ViewportId viewportId);
+    AzFramework::InputDeviceId GetSyntheticKeyboardDeviceId(AzFramework::ViewportId viewportId);
+    AzFramework::InputDeviceId GetSyntheticMouseDeviceId(AzFramework::ViewportId viewportId);
 
     //! Maps events from the Qt input system to synthetic InputChannels in AzFramework
     //! that can be used by AzFramework::ViewportControllers.
@@ -193,10 +193,12 @@ namespace AzToolsFramework
         QPoint m_previousGlobalCursorPosition;
         // 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.
-        bool m_enabled = true;
         // Controls the cursor behavior.
         AzToolsFramework::CursorInputMode m_cursorMode = AzToolsFramework::CursorInputMode::CursorModeNone;
+        // The viewport id this input mapper is associated with.
+        AzFramework::ViewportId m_viewportId = AzFramework::InvalidViewportId;
+        // Flags whether or not Qt events should currently be processed.
+        bool m_enabled = true;
         // Flags whether the cursor has been overridden.
         bool m_overrideCursor = false;
 

+ 43 - 8
Code/Framework/AzToolsFramework/AzToolsFramework/Viewport/ViewportMessages.h

@@ -32,7 +32,7 @@ namespace AzFramework
 namespace AzToolsFramework
 {
     enum class CursorInputMode;
-    
+
     namespace ViewportInteraction
     {
         //! Result of handling mouse interaction.
@@ -145,6 +145,8 @@ namespace AzToolsFramework
         };
 
         //! The EBusTraits for ViewportInteractionRequests.
+        //! @deprecated ViewportEBusTraits is deprecated, please use ViewportRequestsEBusTraits.
+        //! O3DE_DEPRECATION_NOTICE(GHI-13429)
         class ViewportEBusTraits : public AZ::EBusTraits
         {
         public:
@@ -153,8 +155,11 @@ namespace AzToolsFramework
             static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Single;
         };
 
+        //! The EBusTraits for ViewportInteractionRequests.
+        using ViewportRequestsEBusTraits = ViewportEBusTraits;
+
         //! A bus to listen to just the MouseViewportRequests.
-        using ViewportMouseRequestBus = AZ::EBus<MouseViewportRequests, ViewportEBusTraits>;
+        using ViewportMouseRequestBus = AZ::EBus<MouseViewportRequests, ViewportRequestsEBusTraits>;
 
         //! Requests that can be made to the viewport to query and modify its state.
         class ViewportInteractionRequests
@@ -179,7 +184,7 @@ namespace AzToolsFramework
         };
 
         //! Type to inherit to implement ViewportInteractionRequests.
-        using ViewportInteractionRequestBus = AZ::EBus<ViewportInteractionRequests, ViewportEBusTraits>;
+        using ViewportInteractionRequestBus = AZ::EBus<ViewportInteractionRequests, ViewportRequestsEBusTraits>;
 
         //! Utility function to return a viewport ray using the ViewportInteractionRequestBus.
         inline ProjectedViewportRay ViewportScreenToWorldRay(
@@ -192,6 +197,36 @@ namespace AzToolsFramework
             return viewportRay;
         }
 
+        //! The EBusTraits for ViewportInteractionNotifications.
+        class ViewportNotificationsEBusTraits : public AZ::EBusTraits
+        {
+        public:
+            using BusIdType = AzFramework::ViewportId; //!< ViewportId - used to address requests to this EBus.
+            static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::ById;
+            static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Multiple;
+        };
+
+        //! Notifications for a specific viewport relating to user input/interactions.
+        class ViewportInteractionNotifications
+        {
+        public:
+            //! Notification to indicate when the viewport has gained focus.
+            virtual void OnViewportFocusIn()
+            {
+            }
+
+            //! Notification to indicate when the viewport has lost focus.
+            virtual void OnViewportFocusOut()
+            {
+            }
+
+        protected:
+            ~ViewportInteractionNotifications() = default;
+        };
+
+        //! Type to inherit to implement ViewportInteractionNotifications.
+        using ViewportInteractionNotificationBus = AZ::EBus<ViewportInteractionNotifications, ViewportNotificationsEBusTraits>;
+
         //! Interface to return only viewport specific settings (e.g. snapping).
         class ViewportSettingsRequests
         {
@@ -228,7 +263,7 @@ namespace AzToolsFramework
         };
 
         //! Type to inherit to implement ViewportSettingsRequests.
-        using ViewportSettingsRequestBus = AZ::EBus<ViewportSettingsRequests, ViewportEBusTraits>;
+        using ViewportSettingsRequestBus = AZ::EBus<ViewportSettingsRequests, ViewportRequestsEBusTraits>;
 
         //! An interface to notify when changes to viewport settings have happened.
         class ViewportSettingNotifications
@@ -254,7 +289,7 @@ namespace AzToolsFramework
             ~ViewportSettingNotifications() = default;
         };
 
-        using ViewportSettingsNotificationBus = AZ::EBus<ViewportSettingNotifications, ViewportEBusTraits>;
+        using ViewportSettingsNotificationBus = AZ::EBus<ViewportSettingNotifications, ViewportRequestsEBusTraits>;
 
         //! Viewport requests that are only guaranteed to be serviced by the Main Editor viewport.
         class MainEditorViewportInteractionRequests
@@ -270,7 +305,7 @@ namespace AzToolsFramework
         };
 
         //! Type to inherit to implement MainEditorViewportInteractionRequests.
-        using MainEditorViewportInteractionRequestBus = AZ::EBus<MainEditorViewportInteractionRequests, ViewportEBusTraits>;
+        using MainEditorViewportInteractionRequestBus = AZ::EBus<MainEditorViewportInteractionRequests, ViewportRequestsEBusTraits>;
 
         //! Editor entity requests to be made about the viewport.
         class EditorEntityViewportInteractionRequests
@@ -283,7 +318,7 @@ namespace AzToolsFramework
             ~EditorEntityViewportInteractionRequests() = default;
         };
 
-        using EditorEntityViewportInteractionRequestBus = AZ::EBus<EditorEntityViewportInteractionRequests, ViewportEBusTraits>;
+        using EditorEntityViewportInteractionRequestBus = AZ::EBus<EditorEntityViewportInteractionRequests, ViewportRequestsEBusTraits>;
 
         //! An interface to query editor modifier keys.
         class EditorModifierKeyRequests : public AZ::EBusTraits
@@ -354,7 +389,7 @@ namespace AzToolsFramework
         };
 
         //! Type to inherit to implement MainEditorViewportInteractionRequests.
-        using ViewportMouseCursorRequestBus = AZ::EBus<ViewportMouseCursorRequests, ViewportEBusTraits>;
+        using ViewportMouseCursorRequestBus = AZ::EBus<ViewportMouseCursorRequests, ViewportRequestsEBusTraits>;
     } // namespace ViewportInteraction
 
     //! Utility function to return EntityContextId.

+ 6 - 0
Gems/Atom/Tools/AtomToolsFramework/Code/Include/AtomToolsFramework/Viewport/ModularViewportCameraController.h

@@ -13,6 +13,7 @@
 #include <AzCore/std/smart_ptr/shared_ptr.h>
 #include <AzFramework/Viewport/CameraInput.h>
 #include <AzFramework/Viewport/MultiViewportController.h>
+#include <AzToolsFramework/Viewport/ViewportMessages.h>
 
 namespace AtomToolsFramework
 {
@@ -103,6 +104,7 @@ namespace AtomToolsFramework
     class ModularViewportCameraControllerInstance final
         : public AzFramework::MultiViewportControllerInstanceInterface<ModularViewportCameraController>
         , public ModularViewportCameraControllerRequestBus::Handler
+        , public AzToolsFramework::ViewportInteraction::ViewportInteractionNotificationBus::Handler
     {
     public:
         explicit ModularViewportCameraControllerInstance(AzFramework::ViewportId viewportId, ModularViewportCameraController* controller);
@@ -126,8 +128,12 @@ namespace AtomToolsFramework
         void SetCameraOffsetImmediate(const AZ::Vector3& offset) override;
         bool AddCameras(const AZStd::vector<AZStd::shared_ptr<AzFramework::CameraInput>>& cameraInputs) override;
         bool RemoveCameras(const AZStd::vector<AZStd::shared_ptr<AzFramework::CameraInput>>& cameraInputs) override;
+        void ResetCameras() override;
 
     private:
+        // ViewportInteractionNotificationBus overrides ...
+        void OnViewportFocusOut() override;
+
         //! Combine the current camera transform with any potential roll from the tracked
         //! transform (this is usually zero).
         AZ::Transform CombinedCameraTransform() const;

+ 2 - 0
Gems/Atom/Tools/AtomToolsFramework/Code/Include/AtomToolsFramework/Viewport/ModularViewportCameraControllerRequestBus.h

@@ -70,6 +70,8 @@ namespace AtomToolsFramework
         virtual bool AddCameras(const AZStd::vector<AZStd::shared_ptr<AzFramework::CameraInput>>& cameraInputs) = 0;
         //! Remove one or more camera inputs (behaviors) to stop them running for the current camera.
         virtual bool RemoveCameras(const AZStd::vector<AZStd::shared_ptr<AzFramework::CameraInput>>& cameraInputs) = 0;
+        //! Reset the state of all camera inputs (clear inputs from running).
+        virtual void ResetCameras() = 0;
 
     protected:
         ~ModularViewportCameraControllerRequests() = default;

+ 16 - 0
Gems/Atom/Tools/AtomToolsFramework/Code/Source/Viewport/ModularViewportCameraController.cpp

@@ -193,10 +193,12 @@ namespace AtomToolsFramework
         m_modularCameraViewportContext->ConnectViewMatrixChangedHandler(m_cameraViewMatrixChangeHandler);
 
         ModularViewportCameraControllerRequestBus::Handler::BusConnect(viewportId);
+        AzToolsFramework::ViewportInteraction::ViewportInteractionNotificationBus::Handler::BusConnect(viewportId);
     }
 
     ModularViewportCameraControllerInstance::~ModularViewportCameraControllerInstance()
     {
+        AzToolsFramework::ViewportInteraction::ViewportInteractionNotificationBus::Handler::BusDisconnect();
         ModularViewportCameraControllerRequestBus::Handler::BusDisconnect();
     }
 
@@ -346,6 +348,15 @@ namespace AtomToolsFramework
         return m_cameraSystem.m_cameras.RemoveCameras(cameraInputs);
     }
 
+    void ModularViewportCameraControllerInstance::ResetCameras()
+    {
+        // clear any pivot oribit offset and store combined
+        // translation as new pivot
+        m_targetCamera.m_pivot = m_targetCamera.Translation();
+        m_targetCamera.m_offset = AZ::Vector3::CreateZero();
+        m_cameraSystem.m_cameras.Reset();
+    }
+
     bool ModularViewportCameraControllerInstance::IsInterpolating() const
     {
         return m_cameraMode == CameraMode::Animation;
@@ -382,6 +393,11 @@ namespace AtomToolsFramework
         return m_storedCamera.has_value();
     }
 
+    void ModularViewportCameraControllerInstance::OnViewportFocusOut()
+    {
+        ResetCameras();
+    }
+
     AZ::Transform PlaceholderModularCameraViewportContextImpl::GetCameraTransform() const
     {
         return m_cameraTransform;

+ 1 - 2
Gems/Atom/Tools/AtomToolsFramework/Code/Source/Viewport/RenderViewportWidget.cpp

@@ -468,9 +468,8 @@ namespace AtomToolsFramework
     }
 
     // Editor ignores requests to change the sync interval
-    bool RenderViewportWidget::SetSyncInterval(uint32_t /*ignored*/)
+    bool RenderViewportWidget::SetSyncInterval([[maybe_unused]] uint32_t newSyncInterval)
     {
         return false;
     }
-
 } //namespace AtomToolsFramework