diff --git a/Code/Editor/EditorModularViewportCameraComposer.cpp b/Code/Editor/EditorModularViewportCameraComposer.cpp index f145adf72f..3f66468584 100644 --- a/Code/Editor/EditorModularViewportCameraComposer.cpp +++ b/Code/Editor/EditorModularViewportCameraComposer.cpp @@ -356,7 +356,8 @@ namespace SandboxEditor AZ::TransformBus::EventResult(worldFromLocal, viewEntityId, &AZ::TransformBus::Events::GetWorldTM); AtomToolsFramework::ModularViewportCameraControllerRequestBus::Event( - m_viewportId, &AtomToolsFramework::ModularViewportCameraControllerRequestBus::Events::StartTrackingTransform, worldFromLocal); + m_viewportId, &AtomToolsFramework::ModularViewportCameraControllerRequestBus::Events::StartTrackingTransform, + worldFromLocal); } else { @@ -367,8 +368,10 @@ namespace SandboxEditor void EditorModularViewportCameraComposer::OnTick(const float deltaTime, [[maybe_unused]] AZ::ScriptTimePoint time) { - const float delta = [duration = &ed_cameraDefaultOrbitFadeDuration, deltaTime] { - if (*duration == 0.0f) { + const float delta = [duration = &ed_cameraDefaultOrbitFadeDuration, deltaTime] + { + if (*duration == 0.0f) + { return 1.0f; } return deltaTime / *duration; diff --git a/Code/Editor/GotoPositionDlg.cpp b/Code/Editor/GotoPositionDlg.cpp index 84d149de58..37f55f19ea 100644 --- a/Code/Editor/GotoPositionDlg.cpp +++ b/Code/Editor/GotoPositionDlg.cpp @@ -6,7 +6,6 @@ * */ - #include "GotoPositionDlg.h" #include "EditorDefs.h" @@ -25,6 +24,17 @@ AZ_PUSH_DISABLE_DLL_EXPORT_MEMBER_WARNING #include AZ_POP_DISABLE_DLL_EXPORT_MEMBER_WARNING +void GotoPositionPitchConstraints::DeterminePitchRange(const AngleRangeConfigureFn& configurePitchRangeFn) const +{ + const auto [pitchMinRadians, pitchMaxRadians] = AzFramework::CameraPitchMinMaxRadians(); + configurePitchRangeFn(AZ::RadToDeg(pitchMinRadians), AZ::RadToDeg(pitchMaxRadians)); +} + +float GotoPositionPitchConstraints::PitchClampedRadians(float pitchDegrees) const +{ + return AzFramework::ClampPitchRotation(AZ::DegToRad(pitchDegrees)); +} + GotoPositionDialog::GotoPositionDialog(QWidget* parent) : QDialog(parent) , m_ui(new Ui::GotoPositionDialog) @@ -55,20 +65,23 @@ void GotoPositionDialog::OnInitDialog() const auto yawDegrees = AZ::RadToDeg(cameraRotation.GetZ()); // position - m_ui->m_dymX->setRange(-64000.0, 64000.0); + const double CameraPositionExtent = 64000.0; + m_ui->m_dymX->setRange(-CameraPositionExtent, CameraPositionExtent); m_ui->m_dymX->setValue(cameraTranslation.GetX()); - - m_ui->m_dymY->setRange(-64000.0, 64000.0); + m_ui->m_dymY->setRange(-CameraPositionExtent, CameraPositionExtent); m_ui->m_dymY->setValue(cameraTranslation.GetY()); - - m_ui->m_dymZ->setRange(-64000.0, 64000.0); + m_ui->m_dymZ->setRange(-CameraPositionExtent, CameraPositionExtent); m_ui->m_dymZ->setValue(cameraTranslation.GetZ()); // rotation - m_ui->m_dymAnglePitch->setRange(-180.0, 180.0); + m_gotoPositionPitchConstraints.DeterminePitchRange( + [this](const float minPitchDegrees, const float maxPitchDegrees) + { + m_ui->m_dymAnglePitch->setRange(minPitchDegrees, maxPitchDegrees); + }); m_ui->m_dymAnglePitch->setValue(pitchDegrees); - m_ui->m_dymAngleYaw->setRange(-180.0, 180.0); + m_ui->m_dymAngleYaw->setRange(-360, 360); m_ui->m_dymAngleYaw->setValue(yawDegrees); // ensure the goto button is highlighted correctly. @@ -108,12 +121,13 @@ void GotoPositionDialog::OnUpdateNumbers() void GotoPositionDialog::accept() { - SandboxEditor::InterpolateDefaultViewportCameraToTransform( - AZ::Vector3( - aznumeric_cast(m_ui->m_dymX->value()), aznumeric_cast(m_ui->m_dymY->value()), - aznumeric_cast(m_ui->m_dymZ->value())), - AZ::DegToRad(aznumeric_cast(m_ui->m_dymAnglePitch->value())), - AZ::DegToRad(aznumeric_cast(m_ui->m_dymAngleYaw->value()))); + const auto position = AZ::Vector3( + aznumeric_cast(m_ui->m_dymX->value()), aznumeric_cast(m_ui->m_dymY->value()), + aznumeric_cast(m_ui->m_dymZ->value())); + const auto pitchRadians = m_gotoPositionPitchConstraints.PitchClampedRadians(aznumeric_cast(m_ui->m_dymAnglePitch->value())); + const auto yawRadians = AZ::DegToRad(aznumeric_cast(m_ui->m_dymAngleYaw->value())); + + SandboxEditor::InterpolateDefaultViewportCameraToTransform(position, pitchRadians, yawRadians); QDialog::accept(); } diff --git a/Code/Editor/GotoPositionDlg.h b/Code/Editor/GotoPositionDlg.h index ef46b9cbc8..5b627fcebb 100644 --- a/Code/Editor/GotoPositionDlg.h +++ b/Code/Editor/GotoPositionDlg.h @@ -6,21 +6,33 @@ * */ - #pragma once #if !defined(Q_MOC_RUN) #include #endif +#include + +#include + namespace Ui { class GotoPositionDialog; } +//! Utility to deal with ensuring camera pitch values are in the expected range. +struct GotoPositionPitchConstraints +{ + using AngleRangeConfigureFn = AZStd::function; + //! Notify a callback with the min and max camera pitch constraints (no tolerance included). + SANDBOX_API void DeterminePitchRange(const AngleRangeConfigureFn& configurePitchRangeFn) const; + //! Returns the clamped pitch value (including tolerance with range extents). + SANDBOX_API float PitchClampedRadians(float pitchDegrees) const; +}; + //! GotoPositionDialog for setting camera position and rotation. -class GotoPositionDialog - : public QDialog +class GotoPositionDialog : public QDialog { Q_OBJECT @@ -39,5 +51,6 @@ public: QString m_transform; private: + GotoPositionPitchConstraints m_gotoPositionPitchConstraints; QScopedPointer m_ui; }; diff --git a/Code/Editor/Lib/Tests/Camera/test_EditorCamera.cpp b/Code/Editor/Lib/Tests/Camera/test_EditorCamera.cpp index dd7698a82e..0dcda3be33 100644 --- a/Code/Editor/Lib/Tests/Camera/test_EditorCamera.cpp +++ b/Code/Editor/Lib/Tests/Camera/test_EditorCamera.cpp @@ -15,6 +15,8 @@ #include #include +#include + namespace UnitTest { class EditorCameraFixture : public ::testing::Test @@ -257,4 +259,35 @@ namespace UnitTest EXPECT_THAT(interpolating, ::testing::IsFalse()); EXPECT_THAT(nextInterpolationBegan, ::testing::IsTrue()); } + + TEST(GotoPositionPitchConstraints, GoToPositionPitchIsSetToPlusOrMinusNinetyDegrees) + { + float minPitch = 0.0f; + float maxPitch = 0.0f; + + GotoPositionPitchConstraints m_gotoPositionContraints; + m_gotoPositionContraints.DeterminePitchRange( + [&minPitch, &maxPitch](const float minPitchDegrees, const float maxPitchDegrees) + { + minPitch = minPitchDegrees; + maxPitch = maxPitchDegrees; + }); + + using ::testing::FloatNear; + EXPECT_THAT(minPitch, FloatNear(-90.0f, AZ::Constants::FloatEpsilon)); + EXPECT_THAT(maxPitch, FloatNear(90.0f, AZ::Constants::FloatEpsilon)); + } + + TEST(GotoPositionPitchConstraints, GoToPositionPitchClampsFinalPitchValueWithTolerance) + { + const auto [expectedMinPitchRadians, expectedMaxPitchRadians] = AzFramework::CameraPitchMinMaxRadiansWithTolerance(); + + GotoPositionPitchConstraints m_gotoPositionContraints; + const float minClampedPitchRadians = m_gotoPositionContraints.PitchClampedRadians(-90.0f); + const float maxClampedPitchRadians = m_gotoPositionContraints.PitchClampedRadians(90.0f); + + using ::testing::FloatNear; + EXPECT_THAT(minClampedPitchRadians, FloatNear(expectedMinPitchRadians, AZ::Constants::FloatEpsilon)); + EXPECT_THAT(maxClampedPitchRadians, FloatNear(expectedMaxPitchRadians, AZ::Constants::FloatEpsilon)); + } } // namespace UnitTest diff --git a/Code/Framework/AzFramework/AzFramework/Viewport/CameraInput.cpp b/Code/Framework/AzFramework/AzFramework/Viewport/CameraInput.cpp index 771884ac6a..856d40e57d 100644 --- a/Code/Framework/AzFramework/AzFramework/Viewport/CameraInput.cpp +++ b/Code/Framework/AzFramework/AzFramework/Viewport/CameraInput.cpp @@ -335,10 +335,13 @@ namespace AzFramework Camera nextCamera = targetCamera; const float rotateSpeed = m_rotateSpeedFn(); - nextCamera.m_pitch -= float(cursorDelta.m_y) * rotateSpeed * Invert(m_invertPitchFn()); - nextCamera.m_yaw -= float(cursorDelta.m_x) * rotateSpeed * Invert(m_invertYawFn()); + const float deltaPitch = aznumeric_cast(cursorDelta.m_y) * rotateSpeed * Invert(m_invertPitchFn()); + const float deltaYaw = aznumeric_cast(cursorDelta.m_x) * rotateSpeed * Invert(m_invertYawFn()); + nextCamera.m_pitch -= deltaPitch; + nextCamera.m_yaw -= deltaYaw; nextCamera.m_yaw = WrapYawRotation(nextCamera.m_yaw); + if (m_constrainPitch()) { nextCamera.m_pitch = ClampPitchRotation(nextCamera.m_pitch); diff --git a/Code/Framework/AzFramework/AzFramework/Viewport/CameraInput.h b/Code/Framework/AzFramework/AzFramework/Viewport/CameraInput.h index 4eea94cbe7..96a9b54dc8 100644 --- a/Code/Framework/AzFramework/AzFramework/Viewport/CameraInput.h +++ b/Code/Framework/AzFramework/AzFramework/Viewport/CameraInput.h @@ -25,6 +25,9 @@ namespace AzFramework struct WindowSize; + //! Tolerance to use when limiting pitch to avoid reaching +/-Pi/2 exactly. + constexpr float CameraPitchTolerance = 1.0e-4f; + //! 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); @@ -318,11 +321,26 @@ namespace AzFramework return m_handlingEvents; } - //! Clamps pitch to be +/-90 degrees (-Pi/2, Pi/2). + //! Returns min/max values for camera pitch (in radians). + inline AZStd::tuple CameraPitchMinMaxRadians() + { + return { -AZ::Constants::HalfPi, AZ::Constants::HalfPi }; + } + + //! Returns min/max values for camera pitch (in radians) including a small tolerance at each + //! extreme (looking directly up or down) to avoid floating point accuracy issues. + inline AZStd::tuple CameraPitchMinMaxRadiansWithTolerance() + { + const auto [pitchMinRadians, pitchMaxRadians] = CameraPitchMinMaxRadians(); + return { pitchMinRadians + CameraPitchTolerance, pitchMaxRadians - CameraPitchTolerance }; + } + + //! Clamps pitch to be +/-90 degrees (-Pi/2, Pi/2) with a minor tolerance at each extreme. //! @param pitch Pitch angle in radians. inline float ClampPitchRotation(const float pitch) { - return AZ::GetClamp(pitch, -AZ::Constants::HalfPi, AZ::Constants::HalfPi); + const auto [pitchMin, pitchMax] = CameraPitchMinMaxRadiansWithTolerance(); + return AZ::GetClamp(pitch, pitchMin, pitchMax); } //! Ensures yaw wraps between 0 and 360 degrees (0, 2Pi). diff --git a/Code/Framework/AzFramework/Tests/CameraInputTests.cpp b/Code/Framework/AzFramework/Tests/CameraInputTests.cpp index 005623677c..5f327fb2fd 100644 --- a/Code/Framework/AzFramework/Tests/CameraInputTests.cpp +++ b/Code/Framework/AzFramework/Tests/CameraInputTests.cpp @@ -322,6 +322,17 @@ namespace UnitTest EXPECT_THAT(m_camera.m_offset, IsClose(AZ::Vector3::CreateZero())); } + TEST(CameraInput, CameraPitchIsClampedWithExpectedTolerance) + { + const auto [expectedMinPitch, expectedMaxPitch] = AzFramework::CameraPitchMinMaxRadiansWithTolerance(); + const float minPitch = AzFramework::ClampPitchRotation(-AZ::Constants::HalfPi); + const float maxPitch = AzFramework::ClampPitchRotation(AZ::Constants::HalfPi); + + using ::testing::FloatNear; + EXPECT_THAT(minPitch, FloatNear(expectedMinPitch, AzFramework::CameraPitchTolerance)); + EXPECT_THAT(maxPitch, FloatNear(expectedMaxPitch, AzFramework::CameraPitchTolerance)); + } + TEST_F(CameraInputFixture, OrbitRotateCameraInputRotatesPitchOffsetByNinetyDegreesWithRequiredPixelDelta) { const auto cameraStartingPosition = AZ::Vector3::CreateAxisY(-20.0f); diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/ViewportSelection/EditorBoxSelect.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/ViewportSelection/EditorBoxSelect.cpp index a1b653225b..ebbeac4794 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/ViewportSelection/EditorBoxSelect.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/ViewportSelection/EditorBoxSelect.cpp @@ -64,7 +64,8 @@ namespace AzToolsFramework } } - if (clickOutcome == AzFramework::ClickDetector::ClickOutcome::Release) + if (clickOutcome == AzFramework::ClickDetector::ClickOutcome::Release || + clickOutcome == AzFramework::ClickDetector::ClickOutcome::Click) { if (m_leftMouseUp) {