Overhaul of how camera orbit/pivot behavior works (#4345)

* overhaul to how camera orbit/pivot behavior works

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

* update naming from orbit to pivot

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

* fix camera unit tests

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

* add additional tests for new camera pivot behavior

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

* fix comment and add additional info for tests

Signed-off-by: hultonha <hultonha@amazon.co.uk>
This commit is contained in:
hultonha
2021-09-29 10:11:49 +01:00
committed by GitHub
parent f765dc0942
commit c34b6ffe3b
11 changed files with 410 additions and 334 deletions
@@ -17,15 +17,6 @@
namespace AzFramework
{
AZ_CVAR(
float,
ed_cameraSystemDefaultPlaneHeight,
34.0f,
nullptr,
AZ::ConsoleFunctorFlags::Null,
"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,
@@ -135,8 +126,8 @@ namespace AzFramework
camera.m_pitch = eulerAngles.GetX();
camera.m_yaw = eulerAngles.GetZ();
// note: m_lookDist is negative so we must invert it here
camera.m_lookAt = transform.GetTranslation() + (camera.Rotation().GetBasisY() * -camera.m_lookDist);
camera.m_pivot = transform.GetTranslation();
camera.m_offset = AZ::Vector3::CreateZero();
}
bool CameraSystem::HandleEvents(const InputEvent& event)
@@ -320,14 +311,8 @@ namespace AzFramework
nextCamera.m_pitch -= float(cursorDelta.m_y) * rotateSpeed * Invert(m_invertPitchFn());
nextCamera.m_yaw -= float(cursorDelta.m_x) * rotateSpeed * Invert(m_invertYawFn());
const auto clampRotation = [](const float angle)
{
return AZStd::fmod(angle + AZ::Constants::TwoPi, AZ::Constants::TwoPi);
};
nextCamera.m_yaw = clampRotation(nextCamera.m_yaw);
// clamp pitch to be +/-90 degrees
nextCamera.m_pitch = AZ::GetClamp(nextCamera.m_pitch, -AZ::Constants::HalfPi, AZ::Constants::HalfPi);
nextCamera.m_yaw = WrapYawRotation(nextCamera.m_yaw);
nextCamera.m_pitch = ClampPitchRotation(nextCamera.m_pitch);
return nextCamera;
}
@@ -337,9 +322,10 @@ namespace AzFramework
m_rotateChannelId = rotateChannelId;
}
PanCameraInput::PanCameraInput(const InputChannelId& panChannelId, PanAxesFn panAxesFn)
PanCameraInput::PanCameraInput(const InputChannelId& panChannelId, PanAxesFn panAxesFn, TranslationDeltaFn translationDeltaFn)
: m_panAxesFn(AZStd::move(panAxesFn))
, m_panChannelId(panChannelId)
, m_translationDeltaFn(translationDeltaFn)
{
m_panSpeedFn = []() constexpr
{
@@ -375,11 +361,11 @@ namespace AzFramework
const auto panAxes = m_panAxesFn(nextCamera);
const float panSpeed = m_panSpeedFn();
const auto deltaPanX = float(cursorDelta.m_x) * panAxes.m_horizontalAxis * panSpeed;
const auto deltaPanY = float(cursorDelta.m_y) * panAxes.m_verticalAxis * panSpeed;
const auto deltaPanX = aznumeric_cast<float>(cursorDelta.m_x) * panAxes.m_horizontalAxis * panSpeed;
const auto deltaPanY = aznumeric_cast<float>(cursorDelta.m_y) * panAxes.m_verticalAxis * panSpeed;
nextCamera.m_lookAt += deltaPanX * Invert(m_invertPanXFn());
nextCamera.m_lookAt += deltaPanY * -Invert(m_invertPanYFn());
m_translationDeltaFn(nextCamera, deltaPanX * Invert(m_invertPanXFn()));
m_translationDeltaFn(nextCamera, deltaPanY * -Invert(m_invertPanYFn()));
return nextCamera;
}
@@ -426,8 +412,11 @@ namespace AzFramework
}
TranslateCameraInput::TranslateCameraInput(
TranslationAxesFn translationAxesFn, const TranslateCameraInputChannelIds& translateCameraInputChannelIds)
const TranslateCameraInputChannelIds& translateCameraInputChannelIds,
TranslationAxesFn translationAxesFn,
TranslationDeltaFn translateDeltaFn)
: m_translationAxesFn(AZStd::move(translationAxesFn))
, m_translateDeltaFn(AZStd::move(translateDeltaFn))
, m_translateCameraInputChannelIds(translateCameraInputChannelIds)
{
m_translateSpeedFn = []() constexpr
@@ -497,32 +486,32 @@ namespace AzFramework
if ((m_translation & TranslationType::Forward) == TranslationType::Forward)
{
nextCamera.m_lookAt += axisY * speed * deltaTime;
m_translateDeltaFn(nextCamera, axisY * speed * deltaTime);
}
if ((m_translation & TranslationType::Backward) == TranslationType::Backward)
{
nextCamera.m_lookAt -= axisY * speed * deltaTime;
m_translateDeltaFn(nextCamera, -axisY * speed * deltaTime);
}
if ((m_translation & TranslationType::Left) == TranslationType::Left)
{
nextCamera.m_lookAt -= axisX * speed * deltaTime;
m_translateDeltaFn(nextCamera, -axisX * speed * deltaTime);
}
if ((m_translation & TranslationType::Right) == TranslationType::Right)
{
nextCamera.m_lookAt += axisX * speed * deltaTime;
m_translateDeltaFn(nextCamera, axisX * speed * deltaTime);
}
if ((m_translation & TranslationType::Up) == TranslationType::Up)
{
nextCamera.m_lookAt += axisZ * speed * deltaTime;
m_translateDeltaFn(nextCamera, axisZ * speed * deltaTime);
}
if ((m_translation & TranslationType::Down) == TranslationType::Down)
{
nextCamera.m_lookAt -= axisZ * speed * deltaTime;
m_translateDeltaFn(nextCamera, -axisZ * speed * deltaTime);
}
if (Ending())
@@ -544,16 +533,20 @@ namespace AzFramework
m_translateCameraInputChannelIds = translateCameraInputChannelIds;
}
OrbitCameraInput::OrbitCameraInput(const InputChannelId& orbitChannelId)
: m_orbitChannelId(orbitChannelId)
PivotCameraInput::PivotCameraInput(const InputChannelId& pivotChannelId)
: m_pivotChannelId(pivotChannelId)
{
m_pivotFn = []([[maybe_unused]] const AZ::Vector3& position, [[maybe_unused]] const AZ::Vector3& direction)
{
return AZ::Vector3::CreateZero();
};
}
bool OrbitCameraInput::HandleEvents(const InputEvent& event, const ScreenVector& cursorDelta, const float scrollDelta)
bool PivotCameraInput::HandleEvents(const InputEvent& event, const ScreenVector& cursorDelta, const float scrollDelta)
{
if (const auto* input = AZStd::get_if<DiscreteInputEvent>(&event))
{
if (input->m_channelId == m_orbitChannelId)
if (input->m_channelId == m_pivotChannelId)
{
if (input->m_state == InputChannel::State::Began)
{
@@ -568,85 +561,46 @@ namespace AzFramework
if (Active())
{
return m_orbitCameras.HandleEvents(event, cursorDelta, scrollDelta);
return m_pivotCameras.HandleEvents(event, cursorDelta, scrollDelta);
}
return !Idle();
}
Camera OrbitCameraInput::StepCamera(
Camera PivotCameraInput::StepCamera(
const Camera& targetCamera, const ScreenVector& cursorDelta, const float scrollDelta, const float deltaTime)
{
Camera nextCamera = targetCamera;
if (Beginning())
{
const auto hasLookAt = [&nextCamera, &targetCamera, &lookAtFn = m_lookAtFn]
{
if (lookAtFn)
{
// pass through the camera's position and look vector for use in the lookAt function
if (const auto lookAt = lookAtFn(targetCamera.Translation(), targetCamera.Rotation().GetBasisY()))
{
// default to internal look at behavior if the look at point matches the camera translation
if (targetCamera.m_lookAt.IsClose(*lookAt))
{
return false;
}
auto transform = AZ::Transform::CreateLookAt(targetCamera.m_lookAt, *lookAt);
nextCamera.m_lookDist = -lookAt->GetDistance(targetCamera.m_lookAt);
UpdateCameraFromTransform(nextCamera, transform);
return true;
}
}
return false;
}();
if (!hasLookAt)
{
float hit_distance = 0.0f;
AZ::Plane::CreateFromNormalAndPoint(AZ::Vector3::CreateAxisZ(), AZ::Vector3::CreateAxisZ(ed_cameraSystemDefaultPlaneHeight))
.CastRay(targetCamera.Translation(), targetCamera.Rotation().GetBasisY(), hit_distance);
if (hit_distance > 0.0f)
{
hit_distance = AZStd::min<float>(hit_distance, ed_cameraSystemMaxOrbitDistance);
nextCamera.m_lookDist = -hit_distance;
nextCamera.m_lookAt = targetCamera.Translation() + targetCamera.Rotation().GetBasisY() * hit_distance;
}
else
{
nextCamera.m_lookDist = -ed_cameraSystemMinOrbitDistance;
nextCamera.m_lookAt =
targetCamera.Translation() + targetCamera.Rotation().GetBasisY() * ed_cameraSystemMinOrbitDistance;
}
}
nextCamera.m_pivot = m_pivotFn(targetCamera.Translation(), targetCamera.Rotation().GetBasisY());
nextCamera.m_offset = nextCamera.View().TransformPoint(targetCamera.Translation());
}
if (Active())
{
nextCamera = m_orbitCameras.StepCamera(nextCamera, cursorDelta, scrollDelta, deltaTime);
MovePivotDetached(nextCamera, m_pivotFn(targetCamera.Translation(), targetCamera.Rotation().GetBasisY()));
nextCamera = m_pivotCameras.StepCamera(nextCamera, cursorDelta, scrollDelta, deltaTime);
}
if (Ending())
{
m_orbitCameras.Reset();
m_pivotCameras.Reset();
nextCamera.m_lookAt = nextCamera.Translation();
nextCamera.m_lookDist = 0.0f;
nextCamera.m_pivot = nextCamera.Translation();
nextCamera.m_offset = AZ::Vector3::CreateZero();
}
return nextCamera;
}
void OrbitCameraInput::SetOrbitInputChannelId(const InputChannelId& orbitChanneId)
void PivotCameraInput::SetPivotInputChannelId(const InputChannelId& pivotChanneId)
{
m_orbitChannelId = orbitChanneId;
m_pivotChannelId = pivotChanneId;
}
OrbitDollyScrollCameraInput::OrbitDollyScrollCameraInput()
PivotDollyScrollCameraInput::PivotDollyScrollCameraInput()
{
m_scrollSpeedFn = []() constexpr
{
@@ -654,7 +608,7 @@ namespace AzFramework
};
}
bool OrbitDollyScrollCameraInput::HandleEvents(
bool PivotDollyScrollCameraInput::HandleEvents(
const InputEvent& event, [[maybe_unused]] const ScreenVector& cursorDelta, [[maybe_unused]] const float scrollDelta)
{
if (const auto* scroll = AZStd::get_if<ScrollEvent>(&event))
@@ -665,46 +619,61 @@ namespace AzFramework
return !Idle();
}
Camera OrbitDollyScrollCameraInput::StepCamera(
static Camera PivotDolly(const Camera& targetCamera, const float delta)
{
Camera nextCamera = targetCamera;
const auto pivotDirection = targetCamera.m_offset.GetNormalized();
nextCamera.m_offset -= pivotDirection * delta;
const auto pivotDot = targetCamera.m_offset.Dot(nextCamera.m_offset);
const auto distance = nextCamera.m_offset.GetLength() * AZ::GetSign(pivotDot);
const auto minDistance = 0.01f;
if (distance < minDistance || pivotDot < 0.0f)
{
nextCamera.m_offset = pivotDirection * minDistance;
}
return nextCamera;
}
Camera PivotDollyScrollCameraInput::StepCamera(
const Camera& targetCamera,
[[maybe_unused]] const ScreenVector& cursorDelta,
const float scrollDelta,
[[maybe_unused]] const float deltaTime)
{
Camera nextCamera = targetCamera;
nextCamera.m_lookDist = AZ::GetMin(nextCamera.m_lookDist + scrollDelta * m_scrollSpeedFn(), 0.0f);
const auto nextCamera = PivotDolly(targetCamera, aznumeric_cast<float>(scrollDelta) * m_scrollSpeedFn());
EndActivation();
return nextCamera;
}
OrbitDollyCursorMoveCameraInput::OrbitDollyCursorMoveCameraInput(const InputChannelId& dollyChannelId)
PivotDollyMotionCameraInput::PivotDollyMotionCameraInput(const InputChannelId& dollyChannelId)
: m_dollyChannelId(dollyChannelId)
{
m_cursorSpeedFn = []() constexpr
m_motionSpeedFn = []() constexpr
{
return 0.01f;
};
}
bool OrbitDollyCursorMoveCameraInput::HandleEvents(
bool PivotDollyMotionCameraInput::HandleEvents(
const InputEvent& event, [[maybe_unused]] const ScreenVector& cursorDelta, [[maybe_unused]] const float scrollDelta)
{
HandleActivationEvents(event, m_dollyChannelId, cursorDelta, m_clickDetector, *this);
return CameraInputUpdatingAfterMotion(*this);
}
Camera OrbitDollyCursorMoveCameraInput::StepCamera(
Camera PivotDollyMotionCameraInput::StepCamera(
const Camera& targetCamera,
const ScreenVector& cursorDelta,
[[maybe_unused]] const float scrollDelta,
[[maybe_unused]] const float deltaTime)
{
Camera nextCamera = targetCamera;
nextCamera.m_lookDist = AZ::GetMin(nextCamera.m_lookDist + float(cursorDelta.m_y) * m_cursorSpeedFn(), 0.0f);
return nextCamera;
return PivotDolly(targetCamera, aznumeric_cast<float>(cursorDelta.m_y) * m_motionSpeedFn());
}
void OrbitDollyCursorMoveCameraInput::SetDollyInputChannelId(const InputChannelId& dollyChannelId)
void PivotDollyMotionCameraInput::SetDollyInputChannelId(const InputChannelId& dollyChannelId)
{
m_dollyChannelId = dollyChannelId;
}
@@ -739,7 +708,7 @@ namespace AzFramework
const auto translation_basis = LookTranslation(nextCamera);
const auto axisY = translation_basis.GetBasisY();
nextCamera.m_lookAt += axisY * scrollDelta * m_scrollSpeedFn();
nextCamera.m_pivot += axisY * scrollDelta * m_scrollSpeedFn();
EndActivation();
@@ -790,13 +759,13 @@ namespace AzFramework
{
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);
camera.m_pivot = targetCamera.m_pivot.Lerp(currentCamera.m_pivot, moveTime);
camera.m_offset = targetCamera.m_offset.Lerp(currentCamera.m_offset, moveTime);
}
else
{
camera.m_lookDist = targetCamera.m_lookDist;
camera.m_lookAt = targetCamera.m_lookAt;
camera.m_pivot = targetCamera.m_pivot;
camera.m_offset = targetCamera.m_offset;
}
return camera;
@@ -29,17 +29,15 @@ namespace AzFramework
//! @note Order of rotation is Z, Y, X.
AZ::Vector3 EulerAngles(const AZ::Matrix3x3& orientation);
//! A simple camera representation using spherical coordinates as input (pitch, yaw and look distance).
//! A simple camera representation using spherical coordinates as input (pitch, yaw, pivot and offset).
//! The cameras transform and view can be obtained through accessor functions that use the internal
//! spherical coordinates to calculate the position and orientation.
struct Camera
{
AZ::Vector3 m_lookAt = AZ::Vector3::CreateZero(); //!< Position of camera when m_lookDist is zero,
//!< or position of m_lookAt when m_lookDist is greater
//!< than zero.
float m_yaw{ 0.0 }; //!< Yaw rotation of camera (stored in radians) usually clamped to 0-360 degrees (0-2Pi radians).
float m_pitch{ 0.0 }; //!< Pitch rotation of the camera (stored in radians) usually clamped to +/-90 degrees (-Pi/2 - Pi/2 radians).
float m_lookDist{ 0.0 }; //!< Zero gives first person free look, otherwise orbit about m_lookAt
AZ::Vector3 m_pivot = AZ::Vector3::CreateZero(); //!< Pivot point to rotate about (modified in world space).
AZ::Vector3 m_offset = AZ::Vector3::CreateZero(); //!< Offset relative to pivot (modified in camera space).
float m_yaw = 0.0f; //!< Yaw rotation of camera (stored in radians) usually clamped to 0-360 degrees (0-2Pi radians).
float m_pitch = 0.0f; //!< Pitch rotation of the camera (stored in radians) usually clamped to +/-90 degrees (-Pi/2 - Pi/2 radians).
//! View camera transform (V in model-view-projection matrix (MVP)).
AZ::Transform View() const;
@@ -51,6 +49,15 @@ namespace AzFramework
AZ::Vector3 Translation() const;
};
//! Helper to allow the pivot to be positioned without altering the camera's position.
inline void MovePivotDetached(Camera& camera, const AZ::Vector3& pivot)
{
const auto& view = camera.View();
const auto delta = view.TransformPoint(pivot) - view.TransformPoint(camera.m_pivot);
camera.m_offset -= delta;
camera.m_pivot = pivot;
}
inline AZ::Transform Camera::View() const
{
return Transform().GetInverse();
@@ -58,8 +65,8 @@ namespace AzFramework
inline AZ::Transform Camera::Transform() const
{
return AZ::Transform::CreateTranslation(m_lookAt) * AZ::Transform::CreateRotationZ(m_yaw) *
AZ::Transform::CreateRotationX(m_pitch) * AZ::Transform::CreateTranslation(AZ::Vector3::CreateAxisY(m_lookDist));
return AZ::Transform::CreateTranslation(m_pivot) * AZ::Transform::CreateRotationZ(m_yaw) * AZ::Transform::CreateRotationX(m_pitch) *
AZ::Transform::CreateTranslation(m_offset);
}
inline AZ::Matrix3x3 Camera::Rotation() const
@@ -279,21 +286,37 @@ namespace AzFramework
public:
bool HandleEvents(const InputEvent& event);
Camera StepCamera(const Camera& targetCamera, float deltaTime);
bool HandlingEvents() const
{
return m_handlingEvents;
}
bool HandlingEvents() const;
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.
ScreenVector m_motionDelta; //!< The delta used for look/pivot/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).
};
//! A camera input to handle motion deltas that can rotate or orbit the camera.
inline bool CameraSystem::HandlingEvents() const
{
return m_handlingEvents;
}
//! Clamps pitch to be +/-90 degrees (-Pi/2, Pi/2).
//! @param pitch Pitch angle in radians.
inline float ClampPitchRotation(const float pitch)
{
return AZ::GetClamp(pitch, -AZ::Constants::HalfPi, AZ::Constants::HalfPi);
}
//! Ensures yaw wraps between 0 and 360 degrees (0, 2Pi).
//! @param yaw Yaw angle in radians.
inline float WrapYawRotation(const float yaw)
{
return AZStd::fmod(yaw + AZ::Constants::TwoPi, AZ::Constants::TwoPi);
}
//! A camera input to handle motion deltas that can rotate or pivot the camera.
class RotateCameraInput : public CameraInput
{
public:
@@ -332,8 +355,8 @@ namespace AzFramework
return { orientation.GetBasisX(), orientation.GetBasisZ() };
}
//! PanAxes to use while in 'orbit' camera behavior.
inline PanAxes OrbitPan(const Camera& camera)
//! PanAxes to use while in 'pivot' camera behavior.
inline PanAxes PivotPan(const Camera& camera)
{
const AZ::Matrix3x3 orientation = camera.Rotation();
@@ -347,11 +370,23 @@ namespace AzFramework
return { basisX, basisY };
}
using TranslationDeltaFn = AZStd::function<void(Camera& camera, const AZ::Vector3& delta)>;
inline void TranslatePivot(Camera& camera, const AZ::Vector3& delta)
{
camera.m_pivot += delta;
}
inline void TranslateOffset(Camera& camera, const AZ::Vector3& delta)
{
camera.m_offset += camera.View().TransformVector(delta);
}
//! A camera input to handle motion deltas that can pan the camera (translate in two axes).
class PanCameraInput : public CameraInput
{
public:
PanCameraInput(const InputChannelId& panChannelId, PanAxesFn panAxesFn);
PanCameraInput(const InputChannelId& panChannelId, PanAxesFn panAxesFn, TranslationDeltaFn translationDeltaFn);
// CameraInput overrides ...
bool HandleEvents(const InputEvent& event, const ScreenVector& cursorDelta, float scrollDelta) override;
@@ -365,6 +400,7 @@ namespace AzFramework
private:
PanAxesFn m_panAxesFn; //!< Builder for the particular pan axes (provided in the constructor).
TranslationDeltaFn m_translationDeltaFn; //!< How to apply the translation delta to the camera offset or pivot.
InputChannelId m_panChannelId; //!< Input channel to begin the pan camera input.
ClickDetector m_clickDetector; //!< Used to determine when a sufficient motion delta has occurred after an initial discrete input
//!< event has started (press and move event).
@@ -385,8 +421,8 @@ namespace AzFramework
return AZ::Matrix3x3::CreateFromColumns(basisX, basisY, basisZ);
}
//! TranslationAxes to use while in 'orbit' camera behavior.
inline AZ::Matrix3x3 OrbitTranslation(const Camera& camera)
//! TranslationAxes to use while in 'pivot' camera behavior.
inline AZ::Matrix3x3 PivotTranslation(const Camera& camera)
{
const AZ::Matrix3x3 orientation = camera.Rotation();
@@ -417,8 +453,10 @@ namespace AzFramework
class TranslateCameraInput : public CameraInput
{
public:
explicit TranslateCameraInput(
TranslationAxesFn translationAxesFn, const TranslateCameraInputChannelIds& translateCameraInputChannelIds);
TranslateCameraInput(
const TranslateCameraInputChannelIds& translateCameraInputChannelIds,
TranslationAxesFn translationAxesFn,
TranslationDeltaFn translateDeltaFn);
// CameraInput overrides ...
bool HandleEvents(const InputEvent& event, const ScreenVector& cursorDelta, float scrollDelta) override;
@@ -492,15 +530,16 @@ namespace AzFramework
TranslationType m_translation = TranslationType::Nil; //!< Types of translation the camera input is under.
TranslationAxesFn m_translationAxesFn; //!< Builder for translation axes.
TranslationDeltaFn m_translateDeltaFn; //!< How to apply the translation delta to the camera offset or pivot.
TranslateCameraInputChannelIds m_translateCameraInputChannelIds; //!< Input channel ids that map to internal translation types.
bool m_boost = false; //!< Is the translation speed currently being multiplied/scaled upwards.
};
//! A camera input to handle discrete scroll events that can modify the camera look distance.
class OrbitDollyScrollCameraInput : public CameraInput
//! A camera input to handle discrete scroll events that can modify the camera pivot distance.
class PivotDollyScrollCameraInput : public CameraInput
{
public:
OrbitDollyScrollCameraInput();
PivotDollyScrollCameraInput();
// CameraInput overrides ...
bool HandleEvents(const InputEvent& event, const ScreenVector& cursorDelta, float scrollDelta) override;
@@ -509,11 +548,11 @@ namespace AzFramework
AZStd::function<float()> m_scrollSpeedFn;
};
//! A camera input to handle motion deltas that can modify the camera look distance.
class OrbitDollyCursorMoveCameraInput : public CameraInput
//! A camera input to handle motion deltas that can modify the camera pivot distance.
class PivotDollyMotionCameraInput : public CameraInput
{
public:
explicit OrbitDollyCursorMoveCameraInput(const InputChannelId& dollyChannelId);
explicit PivotDollyMotionCameraInput(const InputChannelId& dollyChannelId);
// CameraInput overrides ...
bool HandleEvents(const InputEvent& event, const ScreenVector& cursorDelta, float scrollDelta) override;
@@ -521,7 +560,7 @@ namespace AzFramework
void SetDollyInputChannelId(const InputChannelId& dollyChannelId);
AZStd::function<float()> m_cursorSpeedFn;
AZStd::function<float()> m_motionSpeedFn;
private:
InputChannelId m_dollyChannelId; //!< Input channel to begin the dolly cursor camera input.
@@ -544,36 +583,36 @@ namespace AzFramework
//! A camera input that doubles as its own set of camera inputs.
//! It is 'exclusive', so does not overlap with other sibling camera inputs - it runs its own set of camera inputs as 'children'.
class OrbitCameraInput : public CameraInput
class PivotCameraInput : public CameraInput
{
public:
using LookAtFn = AZStd::function<AZStd::optional<AZ::Vector3>(const AZ::Vector3& position, const AZ::Vector3& direction)>;
using PivotFn = AZStd::function<AZ::Vector3(const AZ::Vector3& position, const AZ::Vector3& direction)>;
explicit OrbitCameraInput(const InputChannelId& orbitChannelId);
explicit PivotCameraInput(const InputChannelId& pivotChannelId);
// CameraInput overrides ...
bool HandleEvents(const InputEvent& event, const ScreenVector& cursorDelta, float scrollDelta) override;
Camera StepCamera(const Camera& targetCamera, const ScreenVector& cursorDelta, float scrollDelta, float deltaTime) override;
bool Exclusive() const override;
void SetOrbitInputChannelId(const InputChannelId& orbitChanneId);
void SetPivotInputChannelId(const InputChannelId& pivotChanneId);
Cameras m_orbitCameras; //!< The camera inputs to run when this camera input is active (only these will run as it is exclusive).
Cameras m_pivotCameras; //!< The camera inputs to run when this camera input is active (only these will run as it is exclusive).
//! Override the default behavior for how a look-at point is calculated.
void SetLookAtFn(const LookAtFn& lookAtFn);
//! Override the default behavior for how a pivot point is calculated.
void SetPivotFn(PivotFn pivotFn);
private:
InputChannelId m_orbitChannelId; //!< Input channel to begin the orbit camera input.
LookAtFn m_lookAtFn; //!< The look-at behavior to use for this orbit camera (how is the look-at point calculated/retrieved).
InputChannelId m_pivotChannelId; //!< Input channel to begin the pivot camera input.
PivotFn m_pivotFn; //!< The pivot position to use for this pivot camera (how is the pivot point calculated/retrieved).
};
inline void OrbitCameraInput::SetLookAtFn(const LookAtFn& lookAtFn)
inline void PivotCameraInput::SetPivotFn(PivotFn pivotFn)
{
m_lookAtFn = lookAtFn;
m_pivotFn = AZStd::move(pivotFn);
}
inline bool OrbitCameraInput::Exclusive() const
inline bool PivotCameraInput::Exclusive() const
{
return true;
}
@@ -26,7 +26,8 @@ namespace UnitTest
{
constexpr float deltaTime = 0.01666f; // 60fps
const bool consumed = m_cameraSystem->HandleEvents(event);
m_camera = m_cameraSystem->StepCamera(m_targetCamera, deltaTime);
m_targetCamera = m_cameraSystem->StepCamera(m_targetCamera, deltaTime);
m_camera = m_targetCamera; // no smoothing
return consumed;
}
@@ -45,20 +46,38 @@ namespace UnitTest
m_translateCameraInputChannelIds.m_boostChannelId = AzFramework::InputChannelId("keyboard_key_modifier_shift_l");
m_firstPersonRotateCamera = AZStd::make_shared<AzFramework::RotateCameraInput>(AzFramework::InputDeviceMouse::Button::Right);
m_firstPersonTranslateCamera =
AZStd::make_shared<AzFramework::TranslateCameraInput>(AzFramework::LookTranslation, m_translateCameraInputChannelIds);
// set rotate speed to be a value that will scale motion delta (pixels moved) by a thousandth.
m_firstPersonRotateCamera->m_rotateSpeedFn = []()
{
return 0.001f;
};
m_orbitCamera = AZStd::make_shared<AzFramework::OrbitCameraInput>(m_orbitChannelId);
auto orbitRotateCamera = AZStd::make_shared<AzFramework::RotateCameraInput>(AzFramework::InputDeviceMouse::Button::Left);
auto orbitTranslateCamera =
AZStd::make_shared<AzFramework::TranslateCameraInput>(AzFramework::OrbitTranslation, m_translateCameraInputChannelIds);
m_firstPersonTranslateCamera = AZStd::make_shared<AzFramework::TranslateCameraInput>(
m_translateCameraInputChannelIds, AzFramework::LookTranslation, AzFramework::TranslatePivot);
m_orbitCamera->m_orbitCameras.AddCamera(orbitRotateCamera);
m_orbitCamera->m_orbitCameras.AddCamera(orbitTranslateCamera);
m_pivotCamera = AZStd::make_shared<AzFramework::PivotCameraInput>(m_pivotChannelId);
m_pivotCamera->SetPivotFn(
[this](const AZ::Vector3&, const AZ::Vector3&)
{
return m_pivot;
});
auto pivotRotateCamera = AZStd::make_shared<AzFramework::RotateCameraInput>(AzFramework::InputDeviceMouse::Button::Left);
// set rotate speed to be a value that will scale motion delta (pixels moved) by a thousandth.
pivotRotateCamera->m_rotateSpeedFn = []()
{
return 0.001f;
};
auto pivotTranslateCamera = AZStd::make_shared<AzFramework::TranslateCameraInput>(
m_translateCameraInputChannelIds, AzFramework::PivotTranslation, AzFramework::TranslateOffset);
m_pivotCamera->m_pivotCameras.AddCamera(pivotRotateCamera);
m_pivotCamera->m_pivotCameras.AddCamera(pivotTranslateCamera);
m_cameraSystem->m_cameras.AddCamera(m_firstPersonRotateCamera);
m_cameraSystem->m_cameras.AddCamera(m_firstPersonTranslateCamera);
m_cameraSystem->m_cameras.AddCamera(m_orbitCamera);
m_cameraSystem->m_cameras.AddCamera(m_pivotCamera);
// these tests rely on using motion delta, not cursor positions (default is true)
AzFramework::ed_cameraSystemUseCursor = false;
@@ -68,7 +87,7 @@ namespace UnitTest
{
AzFramework::ed_cameraSystemUseCursor = true;
m_orbitCamera.reset();
m_pivotCamera.reset();
m_firstPersonRotateCamera.reset();
m_firstPersonTranslateCamera.reset();
@@ -78,24 +97,29 @@ namespace UnitTest
AllocatorsTestFixture::TearDown();
}
AzFramework::InputChannelId m_orbitChannelId = AzFramework::InputChannelId("keyboard_key_modifier_alt_l");
AzFramework::InputChannelId m_pivotChannelId = AzFramework::InputChannelId("keyboard_key_modifier_alt_l");
AzFramework::TranslateCameraInputChannelIds m_translateCameraInputChannelIds;
AZStd::shared_ptr<AzFramework::RotateCameraInput> m_firstPersonRotateCamera;
AZStd::shared_ptr<AzFramework::TranslateCameraInput> m_firstPersonTranslateCamera;
AZStd::shared_ptr<AzFramework::OrbitCameraInput> m_orbitCamera;
AZStd::shared_ptr<AzFramework::PivotCameraInput> m_pivotCamera;
AZ::Vector3 m_pivot = AZ::Vector3::CreateZero();
//! This is approximately Pi/2 * 1000 - this can be used to rotate the camera 90 degrees (pitch or yaw based
//! on vertical or horizontal motion) as the rotate speed function is set to be 1/1000.
inline static const int PixelMotionDelta = 1570;
};
TEST_F(CameraInputFixture, BeginAndEndOrbitCameraInputConsumesCorrectEvents)
TEST_F(CameraInputFixture, BeginAndEndPivotCameraInputConsumesCorrectEvents)
{
// begin orbit camera
// begin pivot camera
const bool consumed1 = HandleEventAndUpdate(AzFramework::DiscreteInputEvent{ AzFramework::InputDeviceKeyboard::Key::ModifierAltL,
AzFramework::InputChannel::State::Began });
// begin listening for orbit rotate (click detector) - event is not consumed
// begin listening for pivot rotate (click detector) - event is not consumed
const bool consumed2 = HandleEventAndUpdate(
AzFramework::DiscreteInputEvent{ AzFramework::InputDeviceMouse::Button::Left, AzFramework::InputChannel::State::Began });
// begin orbit rotate (mouse has moved sufficient distance to initiate)
// begin pivot rotate (mouse has moved sufficient distance to initiate)
const bool consumed3 = HandleEventAndUpdate(AzFramework::HorizontalMotionEvent{ 5 });
// end orbit (mouse up) - event is not consumed
// end pivot (mouse up) - event is not consumed
const bool consumed4 = HandleEventAndUpdate(
AzFramework::DiscreteInputEvent{ AzFramework::InputDeviceMouse::Button::Left, AzFramework::InputChannel::State::Ended });
@@ -236,29 +260,110 @@ namespace UnitTest
EXPECT_TRUE(activationEnded);
}
TEST_F(CameraInputFixture, OrbitCameraInputHandlesLookAtPointAndSelfAtSamePositionWhenOrbiting)
TEST_F(CameraInputFixture, PivotCameraInputHandlesLookAtPointAndSelfAtSamePositionWhenPivoting)
{
// create pathological lookAtFn that just returns the same position as the camera
m_orbitCamera->SetLookAtFn(
m_pivotCamera->SetPivotFn(
[](const AZ::Vector3& position, [[maybe_unused]] const AZ::Vector3& direction)
{
return position;
});
const auto expectedCameraPosition = AZ::Vector3(10.0f, 10.0f, 10.0f);
AzFramework::UpdateCameraFromTransform(
m_targetCamera,
AZ::Transform::CreateFromQuaternionAndTranslation(
AZ::Quaternion::CreateFromEulerAnglesDegrees(AZ::Vector3(0.0f, 0.0f, 90.0f)), AZ::Vector3(10.0f, 10.0f, 10.0f)));
AZ::Quaternion::CreateFromEulerAnglesDegrees(AZ::Vector3(0.0f, 0.0f, 90.0f)), expectedCameraPosition));
m_camera = m_targetCamera;
HandleEventAndUpdate(AzFramework::DiscreteInputEvent{ m_pivotChannelId, AzFramework::InputChannel::State::Began });
HandleEventAndUpdate(AzFramework::DiscreteInputEvent{ m_orbitChannelId, AzFramework::InputChannel::State::Began });
// verify the camera yaw has not changed and pivot point matches the expected camera position
using ::testing::FloatNear;
EXPECT_THAT(m_camera.m_yaw, FloatNear(AZ::DegToRad(90.0f), 0.001f));
EXPECT_THAT(m_camera.m_pitch, FloatNear(0.0f, 0.001f));
EXPECT_THAT(m_camera.m_offset, IsClose(AZ::Vector3::CreateZero()));
EXPECT_THAT(m_camera.m_pivot, IsClose(expectedCameraPosition));
}
// verify the camera yaw has not changed and the look at point
// does not match that of the camera translation
using ::testing::Eq;
using ::testing::Not;
EXPECT_THAT(m_camera.m_yaw, Eq(AZ::DegToRad(90.0f)));
EXPECT_THAT(m_camera.m_lookAt, Not(IsClose(m_camera.Translation())));
TEST_F(CameraInputFixture, FirstPersonRotateCameraInputRotatesYawByNinetyDegreesWithRequiredPixelDelta)
{
const auto cameraStartingPosition = AZ::Vector3::CreateAxisY(-10.0f);
m_targetCamera.m_pivot = cameraStartingPosition;
HandleEventAndUpdate(
AzFramework::DiscreteInputEvent{ AzFramework::InputDeviceMouse::Button::Right, AzFramework::InputChannel::State::Began });
HandleEventAndUpdate(AzFramework::HorizontalMotionEvent{ PixelMotionDelta });
const float expectedYaw = AzFramework::WrapYawRotation(-AZ::Constants::HalfPi);
using ::testing::FloatNear;
EXPECT_THAT(m_camera.m_yaw, FloatNear(expectedYaw, 0.001f));
EXPECT_THAT(m_camera.m_pitch, FloatNear(0.0f, 0.001f));
EXPECT_THAT(m_camera.m_pivot, IsClose(cameraStartingPosition));
EXPECT_THAT(m_camera.m_offset, IsClose(AZ::Vector3::CreateZero()));
}
TEST_F(CameraInputFixture, FirstPersonRotateCameraInputRotatesPitchByNinetyDegreesWithRequiredPixelDelta)
{
const auto cameraStartingPosition = AZ::Vector3::CreateAxisY(-10.0f);
m_targetCamera.m_pivot = cameraStartingPosition;
HandleEventAndUpdate(
AzFramework::DiscreteInputEvent{ AzFramework::InputDeviceMouse::Button::Right, AzFramework::InputChannel::State::Began });
HandleEventAndUpdate(AzFramework::VerticalMotionEvent{ PixelMotionDelta });
const float expectedPitch = AzFramework::ClampPitchRotation(-AZ::Constants::HalfPi);
using ::testing::FloatNear;
EXPECT_THAT(m_camera.m_yaw, FloatNear(0.0f, 0.001f));
EXPECT_THAT(m_camera.m_pitch, FloatNear(expectedPitch, 0.001f));
EXPECT_THAT(m_camera.m_pivot, IsClose(cameraStartingPosition));
EXPECT_THAT(m_camera.m_offset, IsClose(AZ::Vector3::CreateZero()));
}
TEST_F(CameraInputFixture, PivotRotateCameraInputRotatesPitchOffsetByNinetyDegreesWithRequiredPixelDelta)
{
const auto cameraStartingPosition = AZ::Vector3::CreateAxisY(-20.0f);
m_targetCamera.m_pivot = cameraStartingPosition;
m_pivot = AZ::Vector3::CreateAxisY(-10.0f);
HandleEventAndUpdate(AzFramework::DiscreteInputEvent{ m_pivotChannelId, AzFramework::InputChannel::State::Began });
HandleEventAndUpdate(
AzFramework::DiscreteInputEvent{ AzFramework::InputDeviceMouse::Button::Left, AzFramework::InputChannel::State::Began });
HandleEventAndUpdate(AzFramework::VerticalMotionEvent{ PixelMotionDelta });
const auto expectedCameraEndingPosition = AZ::Vector3(0.0f, -10.0f, 10.0f);
const float expectedPitch = AzFramework::ClampPitchRotation(-AZ::Constants::HalfPi);
using ::testing::FloatNear;
EXPECT_THAT(m_camera.m_yaw, FloatNear(0.0f, 0.001f));
EXPECT_THAT(m_camera.m_pitch, FloatNear(expectedPitch, 0.001f));
EXPECT_THAT(m_camera.m_pivot, IsClose(m_pivot));
EXPECT_THAT(m_camera.m_offset, IsClose(AZ::Vector3::CreateAxisY(-10.0f)));
EXPECT_THAT(m_camera.Translation(), IsCloseTolerance(expectedCameraEndingPosition, 0.01f));
}
TEST_F(CameraInputFixture, PivotRotateCameraInputRotatesYawOffsetByNinetyDegreesWithRequiredPixelDelta)
{
const auto cameraStartingPosition = AZ::Vector3(15.0f, -20.0f, 0.0f);
m_targetCamera.m_pivot = cameraStartingPosition;
m_pivot = AZ::Vector3(10.0f, -10.0f, 0.0f);
HandleEventAndUpdate(AzFramework::DiscreteInputEvent{ m_pivotChannelId, AzFramework::InputChannel::State::Began });
HandleEventAndUpdate(
AzFramework::DiscreteInputEvent{ AzFramework::InputDeviceMouse::Button::Left, AzFramework::InputChannel::State::Began });
HandleEventAndUpdate(AzFramework::HorizontalMotionEvent{ -PixelMotionDelta });
const auto expectedCameraEndingPosition = AZ::Vector3(20.0f, -5.0f, 0.0f);
const float expectedYaw = AzFramework::WrapYawRotation(AZ::Constants::HalfPi);
using ::testing::FloatNear;
EXPECT_THAT(m_camera.m_yaw, FloatNear(expectedYaw, 0.001f));
EXPECT_THAT(m_camera.m_pitch, FloatNear(0.0f, 0.001f));
EXPECT_THAT(m_camera.m_pivot, IsClose(m_pivot));
EXPECT_THAT(m_camera.m_offset, IsClose(AZ::Vector3(5.0f, -10.0f, 0.0f)));
EXPECT_THAT(m_camera.Translation(), IsCloseTolerance(expectedCameraEndingPosition, 0.01f));
}
} // namespace UnitTest