diff --git a/Code/Framework/AzFramework/AzFramework/Viewport/ViewportScreen.cpp b/Code/Framework/AzFramework/AzFramework/Viewport/ViewportScreen.cpp index 5d2d02a398..8283e0b1f2 100644 --- a/Code/Framework/AzFramework/AzFramework/Viewport/ViewportScreen.cpp +++ b/Code/Framework/AzFramework/AzFramework/Viewport/ViewportScreen.cpp @@ -128,12 +128,12 @@ namespace AzFramework worldPosition, CameraView(cameraState), CameraProjection(cameraState), cameraState.m_viewportSize); } - AZ::Vector3 ScreenToWorld( - const ScreenPoint& screenPosition, const AZ::Matrix4x4& inverseCameraView, - const AZ::Matrix4x4& inverseCameraProjection, const AZ::Vector2& viewportSize) + AZ::Vector3 ScreenNDCToWorld( + const AZ::Vector2& normalizedScreenPosition, const AZ::Matrix4x4& inverseCameraView, + const AZ::Matrix4x4& inverseCameraProjection) { // convert screen space coordinates from <0, 1> to <-1,1> range - const auto ndcPosition = NDCFromScreenPoint(screenPosition, viewportSize) * 2.0f - AZ::Vector2::CreateOne(); + const auto ndcPosition = normalizedScreenPosition * 2.0f - AZ::Vector2::CreateOne(); // transform ndc space position to clip space const auto clipSpacePosition = inverseCameraProjection * Vector2ToVector4(ndcPosition, -1.0f, 1.0f); @@ -145,6 +145,15 @@ namespace AzFramework return worldPosition; } + AZ::Vector3 ScreenToWorld( + const ScreenPoint& screenPosition, const AZ::Matrix4x4& inverseCameraView, + const AZ::Matrix4x4& inverseCameraProjection, const AZ::Vector2& viewportSize) + { + const auto normalizedScreenPosition = NDCFromScreenPoint(screenPosition, viewportSize); + + return ScreenNDCToWorld(normalizedScreenPosition, inverseCameraView, inverseCameraProjection); + } + AZ::Vector3 ScreenToWorld(const ScreenPoint& screenPosition, const CameraState& cameraState) { return ScreenToWorld( diff --git a/Code/Framework/AzFramework/AzFramework/Viewport/ViewportScreen.h b/Code/Framework/AzFramework/AzFramework/Viewport/ViewportScreen.h index a2c650465f..844f52b565 100644 --- a/Code/Framework/AzFramework/AzFramework/Viewport/ViewportScreen.h +++ b/Code/Framework/AzFramework/AzFramework/Viewport/ViewportScreen.h @@ -42,7 +42,7 @@ namespace AzFramework const AZ::Vector3& worldPosition, const AZ::Matrix4x4& cameraView, const AZ::Matrix4x4& cameraProjection, const AZ::Vector2& viewportSize); - //! Unprojects a position in screen space to world space. + //! Unprojects a position in screen space pixel coordinates to world space. //! Note: The position returned will be on the near clip plane of the camera in world space. AZ::Vector3 ScreenToWorld(const ScreenPoint& screenPosition, const CameraState& cameraState); @@ -52,6 +52,12 @@ namespace AzFramework const ScreenPoint& screenPosition, const AZ::Matrix4x4& inverseCameraView, const AZ::Matrix4x4& inverseCameraProjection, const AZ::Vector2& viewportSize); + //! Unprojects a position in screen space normalized device coordinates to world space. + //! Note: The position returned will be on the near clip plane of the camera in world space. + AZ::Vector3 ScreenNDCToWorld( + const AZ::Vector2& ndcPosition, const AZ::Matrix4x4& inverseCameraView, + const AZ::Matrix4x4& inverseCameraProjection); + //! Returns the camera projection for the current camera state. AZ::Matrix4x4 CameraProjection(const CameraState& cameraState); diff --git a/Code/Framework/AzToolsFramework/Tests/Viewport/ViewportScreenTests.cpp b/Code/Framework/AzToolsFramework/Tests/Viewport/ViewportScreenTests.cpp index 18462bc97b..d82c7ec425 100644 --- a/Code/Framework/AzToolsFramework/Tests/Viewport/ViewportScreenTests.cpp +++ b/Code/Framework/AzToolsFramework/Tests/Viewport/ViewportScreenTests.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -23,6 +24,15 @@ namespace UnitTest { + // transform a point from normalized device coordinates to world space, and then from world space back to normalized device coordinates + AZ::Vector2 ScreenNDCToWorldToScreenNDC( + const AZ::Vector2& ndcPoint, const AzFramework::CameraState& cameraState) + { + const auto worldResult = AzFramework::ScreenNDCToWorld(ndcPoint, InverseCameraView(cameraState), InverseCameraProjection(cameraState)); + const auto ndcResult = AzFramework::WorldToScreenNDC(worldResult, CameraView(cameraState), CameraProjection(cameraState)); + return AZ::Vector3ToVector2(ndcResult); + } + // transform a point from screen space to world space, and then from world space back to screen space AzFramework::ScreenPoint ScreenToWorldToScreen( const AzFramework::ScreenPoint& screenPoint, const AzFramework::CameraState& cameraState) @@ -30,7 +40,8 @@ namespace UnitTest const auto worldResult = AzFramework::ScreenToWorld(screenPoint, cameraState); return AzFramework::WorldToScreen(worldResult, cameraState); } - + //////////////////////////////////////////////////////////////////////////////////////////////////////// + // ScreenPoint tests TEST(ViewportScreen, WorldToScreenAndScreenToWorldReturnsTheSameValueIdentityCameraOffsetFromOrigin) { using AzFramework::ScreenPoint; @@ -38,8 +49,6 @@ namespace UnitTest const auto screenDimensions = AZ::Vector2(800.0f, 600.0f); const auto cameraPosition = AZ::Vector3::CreateAxisY(-10.0f); - // note: nearClip is 0.1 - the world space value returned will be aligned to the near clip - // plane of the camera so use that to confirm the mapping to/from is correct const auto cameraState = AzFramework::CreateIdentityDefaultCamera(cameraPosition, screenDimensions); { const auto expectedScreenPoint = ScreenPoint{600, 450}; @@ -81,6 +90,8 @@ namespace UnitTest EXPECT_EQ(resultScreenPoint, expectedScreenPoint); } + // note: nearClip is 0.1 - the world space value returned will be aligned to the near clip + // plane of the camera so use that to confirm the mapping to/from is correct TEST(ViewportScreen, ScreenToWorldReturnsPositionOnNearClipPlaneInWorldSpace) { using AzFramework::ScreenPoint; @@ -94,7 +105,75 @@ namespace UnitTest const auto worldResult = AzFramework::ScreenToWorld(ScreenPoint{400, 300}, cameraState); EXPECT_THAT(worldResult, IsClose(AZ::Vector3(10.1f, 0.0f, 0.0f))); } + + //////////////////////////////////////////////////////////////////////////////////////////////////////// + // NDC tests + TEST(ViewportScreen, WorldToScreenNDCAndScreenNDCToWorldReturnsTheSameValueIdentityCameraOffsetFromOrigin) + { + using NdcPoint = AZ::Vector2; + + const auto screenDimensions = AZ::Vector2(800.0f, 600.0f); + const auto cameraPosition = AZ::Vector3::CreateAxisY(-10.0f); + const auto cameraState = AzFramework::CreateIdentityDefaultCamera(cameraPosition, screenDimensions); + { + const auto expectedNdcPoint = NdcPoint{0.75f, 0.75f}; + const auto resultNdcPoint = ScreenNDCToWorldToScreenNDC(expectedNdcPoint, cameraState); + EXPECT_THAT(resultNdcPoint, IsClose(expectedNdcPoint)); + } + + { + const auto expectedNdcPoint = NdcPoint{0.5f, 0.5f}; + const auto resultNdcPoint = ScreenNDCToWorldToScreenNDC(expectedNdcPoint, cameraState); + EXPECT_THAT(resultNdcPoint, IsClose(expectedNdcPoint)); + } + + { + const auto expectedNdcPoint = NdcPoint{0.0f, 0.0f}; + const auto resultNdcPoint = ScreenNDCToWorldToScreenNDC(expectedNdcPoint, cameraState); + EXPECT_THAT(resultNdcPoint, IsClose(expectedNdcPoint)); + } + + { + const auto expectedNdcPoint = NdcPoint{1.0f, 1.0f}; + const auto resultNdcPoint = ScreenNDCToWorldToScreenNDC(expectedNdcPoint, cameraState); + EXPECT_THAT(resultNdcPoint, IsClose(expectedNdcPoint)); + } + } + + TEST(ViewportScreen, WorldToScreenNDCAndScreenNDCToWorldReturnsTheSameValueOrientatedCamera) + { + using NdcPoint = AZ::Vector2; + + const auto screenDimensions = AZ::Vector2(800.0f, 600.0f); + const auto cameraTransform = + AZ::Transform::CreateRotationX(AZ::DegToRad(45.0f)) * AZ::Transform::CreateRotationZ(AZ::DegToRad(90.0f)); + + const auto cameraState = AzFramework::CreateDefaultCamera(cameraTransform, screenDimensions); + + const auto expectedNdcPoint = NdcPoint{0.25f, 0.5f}; + const auto resultNdcPoint = ScreenNDCToWorldToScreenNDC(expectedNdcPoint, cameraState); + EXPECT_THAT(resultNdcPoint, IsClose(expectedNdcPoint)); + } + + // note: nearClip is 0.1 - the world space value returned will be aligned to the near clip + // plane of the camera so use that to confirm the mapping to/from is correct + TEST(ViewportScreen, ScreenNDCToWorldReturnsPositionOnNearClipPlaneInWorldSpace) + { + using NdcPoint = AZ::Vector2; + + const auto screenDimensions = AZ::Vector2(800.0f, 600.0f); + const auto cameraTransform = AZ::Transform::CreateTranslation(AZ::Vector3(10.0f, 0.0f, 0.0f)) * + AZ::Transform::CreateRotationZ(AZ::DegToRad(-90.0f)); + + const auto cameraState = AzFramework::CreateDefaultCamera(cameraTransform, screenDimensions); + + const auto worldResult = AzFramework::ScreenNDCToWorld(NdcPoint{0.5f, 0.5f}, InverseCameraView(cameraState), InverseCameraProjection(cameraState)); + EXPECT_THAT(worldResult, IsClose(AZ::Vector3(10.1f, 0.0f, 0.0f))); + } + + //////////////////////////////////////////////////////////////////////////////////////////////////////// + // ScreenVector tests TEST(ViewportScreen, SubstractingScreenPointGivesScreenVector) { using AzFramework::ScreenPoint; @@ -220,6 +299,8 @@ namespace UnitTest EXPECT_NEAR(AzFramework::ScreenVectorLength(ScreenVector(12, 15)), 19.20937f, 0.001f); } + //////////////////////////////////////////////////////////////////////////////////////////////////////// + // Other tests TEST(ViewportScreen, CanGetCameraTransformFromCameraViewAndBack) { const auto screenDimensions = AZ::Vector2(1024.0f, 768.0f);