Fix issue with mouse input for viewport camera (#3210)

* fix for drift accumulating in the viewport camera

Signed-off-by: hultonha <hultonha@amazon.co.uk>

* fix typo and update how events are stored

Signed-off-by: hultonha <hultonha@amazon.co.uk>

* respond to PR feedback and fix linux and windows build issues

Signed-off-by: hultonha <hultonha@amazon.co.uk>

* fix failing unit tests in camera input

Signed-off-by: hultonha <hultonha@amazon.co.uk>
This commit is contained in:
hultonha
2021-08-19 09:06:24 +01:00
committed by GitHub
parent 586678a5f9
commit 80e08dd947
18 changed files with 538 additions and 134 deletions
@@ -8,12 +8,12 @@
#include "CameraInput.h"
#include <AzCore/Console/IConsole.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>
#include <AzFramework/Windowing/WindowBus.h>
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<HorizontalMotionEvent>(&event))
if (const auto& cursor = AZStd::get_if<CursorEvent>(&event))
{
m_motionDelta.m_x = horizonalMotion->m_delta;
m_cursorState.SetCurrentPosition(cursor->m_position);
}
else if (const auto& horizontalMotion = AZStd::get_if<HorizontalMotionEvent>(&event))
{
m_motionDelta.m_x = horizontalMotion->m_delta;
}
else if (const auto& verticalMotion = AZStd::get_if<VerticalMotionEvent>(&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<AzFramework::InputChannel::PositionData2D>();
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<int>(inputChannel.GetValue()) };
}
@@ -761,6 +802,7 @@ namespace AzFramework
{
return VerticalMotionEvent{ aznumeric_cast<int>(inputChannel.GetValue()) };
}
else if (inputChannelId == InputDeviceMouse::Movement::Z)
{
return ScrollEvent{ inputChannel.GetValue() };
@@ -8,17 +8,23 @@
#pragma once
#include <AzCore/Console/IConsole.h>
#include <AzCore/Math/Matrix3x3.h>
#include <AzCore/Math/Transform.h>
#include <AzCore/std/containers/variant.h>
#include <AzCore/std/optional.h>
#include <AzFramework/Input/Channels/InputChannel.h>
#include <AzFramework/Viewport/ClickDetector.h>
#include <AzFramework/Viewport/CursorState.h>
#include <AzFramework/Viewport/ScreenGeometry.h>
#include <AzFramework/Viewport/ViewportId.h>
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<struct HorizontalMotionTag>;
using VerticalMotionEvent = MotionEvent<struct VerticalMotionTag>;
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<AZStd::monostate, HorizontalMotionEvent, VerticalMotionEvent, ScrollEvent, DiscreteInputEvent>;
using InputEvent =
AZStd::variant<AZStd::monostate, HorizontalMotionEvent, VerticalMotionEvent, CursorEvent, ScrollEvent, DiscreteInputEvent>;
//! 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<float()>
m_rotateSmoothnessFn; //!< Rotate smoothing value (useful approx range 3-6, higher values give sharper feel).
AZStd::function<float()>
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<float()> m_rotateSmoothnessFn;
//! Translate smoothing value (useful approx range 3-6, higher values give sharper feel).
AZStd::function<float()> m_translateSmoothnessFn;
//! Enable/disable rotation smoothing.
AZStd::function<bool()> m_rotateSmoothingEnabledFn;
//! Enable/disable translation smoothing.
AZStd::function<bool()> 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
@@ -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)
@@ -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();
@@ -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 <AzFramework/Windowing/WindowBus.h>
#include <gmock/gmock.h>
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
@@ -8,6 +8,7 @@
set(FILES
Mocks/MockSpawnableEntitiesInterface.h
Mocks/MockWindowRequests.h
Utils/Utils.h
Utils/Utils.cpp
FrameworkApplicationFixture.h
@@ -285,7 +285,7 @@ namespace AzToolsFramework
}
}
void QtEventToAzInputMapper::ProcessPendingMouseEvents()
void QtEventToAzInputMapper::ProcessPendingMouseEvents(const QPoint& cursorDelta)
{
auto systemCursorChannel =
GetInputChannel<AzFramework::InputChannelDeltaWithSharedPosition2D>(AzFramework::InputDeviceMouse::SystemCursorPosition);
@@ -297,14 +297,8 @@ namespace AzToolsFramework
GetInputChannel<AzFramework::InputChannelDeltaWithSharedPosition2D>(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<float>(m_sourceWidget->width()) /
m_sourceWidget->devicePixelRatioF());
movementYChannel->ProcessRawInputEvent(
m_mouseDevice->m_cursorPositionData2D->m_normalizedPositionDelta.GetY() * aznumeric_cast<float>(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<float>(position.x()) / aznumeric_cast<float>(m_sourceWidget->width());
const float normalizedY = aznumeric_cast<float>(position.y()) / aznumeric_cast<float>(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<int>(normalizedPosition.GetX() * m_sourceWidget->width());
const int denormalizedY = aznumeric_cast<int>(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)
@@ -21,6 +21,7 @@
#include <QEvent>
#include <QObject>
#include <QPoint>
#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<Qt::Key> m_highPriorityKeys;
// A lookup table for AZ input channel ID -> physical input channel on our mouse or keyboard device.
AZStd::unordered_map<AzFramework::InputChannelId, AzFramework::InputChannel*> 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.
@@ -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;
}