diff --git a/Gems/Atom/RPI/Code/CMakeLists.txt b/Gems/Atom/RPI/Code/CMakeLists.txt index 2898967add..8a2684347e 100644 --- a/Gems/Atom/RPI/Code/CMakeLists.txt +++ b/Gems/Atom/RPI/Code/CMakeLists.txt @@ -150,6 +150,7 @@ if(PAL_TRAIT_BUILD_TESTS_SUPPORTED AND PAL_TRAIT_BUILD_HOST_TOOLS) PRIVATE AZ::AtomCore AZ::AzTest + AZ::AzTestShared AZ::AzFramework AZ::AzToolsFramework Legacy::CryCommon diff --git a/Gems/Atom/RPI/Code/Include/Atom/RPI.Public/Model/Model.h b/Gems/Atom/RPI/Code/Include/Atom/RPI.Public/Model/Model.h index 35af200759..514e3e37a5 100644 --- a/Gems/Atom/RPI/Code/Include/Atom/RPI.Public/Model/Model.h +++ b/Gems/Atom/RPI/Code/Include/Atom/RPI.Public/Model/Model.h @@ -61,12 +61,13 @@ namespace AZ //! Important: only to be used in the Editor, it may kick off a job to calculate spatial information. //! [GFX TODO][ATOM-4343 Bake mesh spatial during AP processing] //! - //! @param rayStart position where the ray starts - //! @param dir direction where the ray ends (does not have to be unit length) - //! @param distanceFactor if an intersection is detected, this will be set such that distanceFactor * dir.length == distance to intersection - //! @param normal if an intersection is detected, this will be set to the normal at the point of intersection - //! @return true if the ray intersects the mesh - bool LocalRayIntersection(const AZ::Vector3& rayStart, const AZ::Vector3& dir, float& distanceFactor, AZ::Vector3& normal) const; + //! @param rayStart The starting point of the ray. + //! @param rayDir The direction and length of the ray (magnitude is encoded in the direction). + //! @param[out] distanceNormalized If an intersection is found, will be set to the normalized distance of the intersection + //! (in the range 0.0-1.0) - to calculate the actual distance, multiply distanceNormalized by the magnitude of rayDir. + //! @param[out] normal If an intersection is found, will be set to the normal at the point of collision. + //! @return True if the ray intersects the mesh. + bool LocalRayIntersection(const AZ::Vector3& rayStart, const AZ::Vector3& rayDir, float& distanceNormalized, AZ::Vector3& normal) const; //! Checks a ray for intersection against this model, where the ray is in a different coordinate space. //! Important: only to be used in the Editor, it may kick off a job to calculate spatial information. @@ -74,13 +75,19 @@ namespace AZ //! //! @param modelTransform a transform that puts the model into the ray's coordinate space //! @param nonUniformScale Non-uniform scale applied in the model's local frame. - //! @param rayStart position where the ray starts - //! @param dir direction where the ray ends (does not have to be unit length) - //! @param distanceFactor if an intersection is detected, this will be set such that distanceFactor * dir.length == distance to intersection - //! @param normal if an intersection is detected, this will be set to the normal at the point of intersection - //! @return true if the ray intersects the mesh - bool RayIntersection(const AZ::Transform& modelTransform, const AZ::Vector3& nonUniformScale, const AZ::Vector3& rayStart, - const AZ::Vector3& dir, float& distanceFactor, AZ::Vector3& normal) const; + //! @param rayStart The starting point of the ray. + //! @param rayDir The direction and length of the ray (magnitude is encoded in the direction). + //! @param[out] distanceNormalized If an intersection is found, will be set to the normalized distance of the intersection + //! (in the range 0.0-1.0) - to calculate the actual distance, multiply distanceNormalized by the magnitude of rayDir. + //! @param[out] normal If an intersection is found, will be set to the normal at the point of collision. + //! @return True if the ray intersects the mesh. + bool RayIntersection( + const AZ::Transform& modelTransform, + const AZ::Vector3& nonUniformScale, + const AZ::Vector3& rayStart, + const AZ::Vector3& rayDir, + float& distanceNormalized, + AZ::Vector3& normal) const; //! Get available UV names from the model and its lods. const AZStd::unordered_set& GetUvNames() const; diff --git a/Gems/Atom/RPI/Code/Include/Atom/RPI.Reflect/Model/ModelAsset.h b/Gems/Atom/RPI/Code/Include/Atom/RPI.Reflect/Model/ModelAsset.h index 8a773dc29e..f3da349195 100644 --- a/Gems/Atom/RPI/Code/Include/Atom/RPI.Reflect/Model/ModelAsset.h +++ b/Gems/Atom/RPI/Code/Include/Atom/RPI.Reflect/Model/ModelAsset.h @@ -63,12 +63,14 @@ namespace AZ //! Important: only to be used in the Editor, it may kick off a job to calculate spatial information. //! [GFX TODO][ATOM-4343 Bake mesh spatial information during AP processing] //! - //! @param rayStart position where the ray starts - //! @param dir direction where the ray ends (does not have to be unit length) - //! @param distance if an intersection is detected, this will be set such that distanceFactor * dir.length == distance to intersection - //! @param normal if an intersection is detected, this will be set to the normal at the point of collision - //! @return true if the ray intersects the mesh - virtual bool LocalRayIntersectionAgainstModel(const AZ::Vector3& rayStart, const AZ::Vector3& dir, float& distance, AZ::Vector3& normal) const; + //! @param rayStart The starting point of the ray. + //! @param rayDir The direction and length of the ray (magnitude is encoded in the direction). + //! @param[out] distanceNormalized If an intersection is found, will be set to the normalized distance of the intersection + //! (in the range 0.0-1.0) - to calculate the actual distance, multiply distanceNormalized by the magnitude of rayDir. + //! @param[out] normal If an intersection is found, will be set to the normal at the point of collision. + //! @return True if the ray intersects the mesh. + virtual bool LocalRayIntersectionAgainstModel( + const AZ::Vector3& rayStart, const AZ::Vector3& rayDir, float& distanceNormalized, AZ::Vector3& normal) const; private: void SetReady(); @@ -79,9 +81,15 @@ namespace AZ // mutable method void BuildKdTree() const; - bool BruteForceRayIntersect(const AZ::Vector3& rayStart, const AZ::Vector3& dir, float& distance, AZ::Vector3& normal) const; - - bool LocalRayIntersectionAgainstMesh(const ModelLodAsset::Mesh& mesh, const AZ::Vector3& rayStart, const AZ::Vector3& dir, float& distance, AZ::Vector3& normal) const; + bool BruteForceRayIntersect( + const AZ::Vector3& rayStart, const AZ::Vector3& rayDir, float& distanceNormalized, AZ::Vector3& normal) const; + + bool LocalRayIntersectionAgainstMesh( + const ModelLodAsset::Mesh& mesh, + const AZ::Vector3& rayStart, + const AZ::Vector3& rayDir, + float& distanceNormalized, + AZ::Vector3& normal) const; // Various model information used in raycasting AZ::Name m_positionName{ "POSITION" }; diff --git a/Gems/Atom/RPI/Code/Source/RPI.Public/Model/Model.cpp b/Gems/Atom/RPI/Code/Source/RPI.Public/Model/Model.cpp index 86477bf785..17ff2c64c8 100644 --- a/Gems/Atom/RPI/Code/Source/RPI.Public/Model/Model.cpp +++ b/Gems/Atom/RPI/Code/Source/RPI.Public/Model/Model.cpp @@ -137,12 +137,12 @@ namespace AZ return m_modelAsset; } - bool Model::LocalRayIntersection(const AZ::Vector3& rayStart, const AZ::Vector3& dir, float& distance, AZ::Vector3& normal) const + bool Model::LocalRayIntersection(const AZ::Vector3& rayStart, const AZ::Vector3& rayDir, float& distanceNormalized, AZ::Vector3& normal) const { AZ_PROFILE_FUNCTION(Debug::ProfileCategory::AzRender); float start; float end; - const int result = Intersect::IntersectRayAABB2(rayStart, dir.GetReciprocal(), m_aabb, start, end); + const int result = Intersect::IntersectRayAABB2(rayStart, rayDir.GetReciprocal(), m_aabb, start, end); if (Intersect::ISECT_RAY_AABB_NONE != result) { if (ModelAsset* modelAssetPtr = m_modelAsset.Get()) @@ -151,7 +151,7 @@ namespace AZ AZ::Debug::Timer timer; timer.Stamp(); #endif - const bool hit = modelAssetPtr->LocalRayIntersectionAgainstModel(rayStart, dir, distance, normal); + const bool hit = modelAssetPtr->LocalRayIntersectionAgainstModel(rayStart, rayDir, distanceNormalized, normal); #if defined(AZ_RPI_PROFILE_RAYCASTING_AGAINST_MODELS) if (hit) { @@ -166,8 +166,12 @@ namespace AZ } bool Model::RayIntersection( - const AZ::Transform& modelTransform, const AZ::Vector3& nonUniformScale, const AZ::Vector3& rayStart, const AZ::Vector3& dir, - float& distanceFactor, AZ::Vector3& normal) const + const AZ::Transform& modelTransform, + const AZ::Vector3& nonUniformScale, + const AZ::Vector3& rayStart, + const AZ::Vector3& rayDir, + float& distanceNormalized, + AZ::Vector3& normal) const { AZ_PROFILE_FUNCTION(Debug::ProfileCategory::AzRender); const AZ::Vector3 clampedScale = nonUniformScale.GetMax(AZ::Vector3(AZ::MinTransformScale)); @@ -175,12 +179,13 @@ namespace AZ const AZ::Transform inverseTM = modelTransform.GetInverse(); const AZ::Vector3 raySrcLocal = inverseTM.TransformPoint(rayStart) / clampedScale; - // Instead of just rotating 'dir' we need it to be scaled too, so that 'distanceFactor' will be in the target units rather than object local units. - const AZ::Vector3 rayDest = rayStart + dir; + // Instead of just rotating 'rayDir' we need it to be scaled too, so that 'distanceNormalized' will be in the target units rather + // than object local units. + const AZ::Vector3 rayDest = rayStart + rayDir; const AZ::Vector3 rayDestLocal = inverseTM.TransformPoint(rayDest) / clampedScale; const AZ::Vector3 rayDirLocal = rayDestLocal - raySrcLocal; - bool result = LocalRayIntersection(raySrcLocal, rayDirLocal, distanceFactor, normal); + const bool result = LocalRayIntersection(raySrcLocal, rayDirLocal, distanceNormalized, normal); normal = (normal * clampedScale).GetNormalized(); return result; } diff --git a/Gems/Atom/RPI/Code/Source/RPI.Reflect/Model/ModelAsset.cpp b/Gems/Atom/RPI/Code/Source/RPI.Reflect/Model/ModelAsset.cpp index 988d07e66d..52fda0f56b 100644 --- a/Gems/Atom/RPI/Code/Source/RPI.Reflect/Model/ModelAsset.cpp +++ b/Gems/Atom/RPI/Code/Source/RPI.Reflect/Model/ModelAsset.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include @@ -75,7 +76,8 @@ namespace AZ m_status = Data::AssetData::AssetStatus::Ready; } - bool ModelAsset::LocalRayIntersectionAgainstModel(const AZ::Vector3& rayStart, const AZ::Vector3& dir, float& distance, AZ::Vector3& normal) const + bool ModelAsset::LocalRayIntersectionAgainstModel( + const AZ::Vector3& rayStart, const AZ::Vector3& rayDir, float& distanceNormalized, AZ::Vector3& normal) const { AZ_PROFILE_FUNCTION(AZ::Debug::ProfileCategory::AzRender); @@ -85,7 +87,7 @@ namespace AZ m_modelTriangleCount = CalculateTriangleCount(); } - // check the total vertex count for this model and skip kdtree if the model is simple enough + // check the total vertex count for this model and skip kd-tree if the model is simple enough if (*m_modelTriangleCount > s_minimumModelTriangleCountToOptimize) { if (!m_kdTree) @@ -97,11 +99,11 @@ namespace AZ } else { - return m_kdTree->RayIntersection(rayStart, dir, distance, normal); + return m_kdTree->RayIntersection(rayStart, rayDir, distanceNormalized, normal); } } - return BruteForceRayIntersect(rayStart, dir, distance, normal); + return BruteForceRayIntersect(rayStart, rayDir, distanceNormalized, normal); } void ModelAsset::BuildKdTree() const @@ -136,7 +138,8 @@ namespace AZ } } - bool ModelAsset::BruteForceRayIntersect(const AZ::Vector3& rayStart, const AZ::Vector3& dir, float& distance, AZ::Vector3& normal) const + bool ModelAsset::BruteForceRayIntersect( + const AZ::Vector3& rayStart, const AZ::Vector3& rayDir, float& distanceNormalized, AZ::Vector3& normal) const { // brute force - check every triangle if (GetLodAssets().empty() == false) @@ -144,27 +147,27 @@ namespace AZ // intersect against the highest level of detail if (ModelLodAsset* loadAssetPtr = GetLodAssets()[0].Get()) { - float shortestDistance = std::numeric_limits::max(); bool anyHit = false; - AZ::Vector3 intersectionNormal; - + float shortestDistanceNormalized = AZStd::numeric_limits::max(); for (const ModelLodAsset::Mesh& mesh : loadAssetPtr->GetMeshes()) { - if (LocalRayIntersectionAgainstMesh(mesh, rayStart, dir, distance, intersectionNormal)) + float currentDistanceNormalized; + if (LocalRayIntersectionAgainstMesh(mesh, rayStart, rayDir, currentDistanceNormalized, intersectionNormal)) { anyHit = true; - if (distance < shortestDistance) + + if (currentDistanceNormalized < shortestDistanceNormalized) { normal = intersectionNormal; - shortestDistance = distance; + shortestDistanceNormalized = currentDistanceNormalized; } } } if (anyHit) { - distance = shortestDistance; + distanceNormalized = shortestDistanceNormalized; } return anyHit; @@ -174,7 +177,12 @@ namespace AZ return false; } - bool ModelAsset::LocalRayIntersectionAgainstMesh(const ModelLodAsset::Mesh& mesh, const AZ::Vector3& rayStart, const AZ::Vector3& dir, float& distance, AZ::Vector3& normal) const + bool ModelAsset::LocalRayIntersectionAgainstMesh( + const ModelLodAsset::Mesh& mesh, + const AZ::Vector3& rayStart, + const AZ::Vector3& rayDir, + float& distanceNormalized, + AZ::Vector3& normal) const { const BufferAssetView& indexBufferView = mesh.GetIndexBufferAssetView(); const AZStd::array_view& streamBufferList = mesh.GetStreamBufferInfoList(); @@ -217,14 +225,13 @@ namespace AZ AZStd::array_view indexRawBuffer = indexAssetViewPtr->GetBuffer(); RHI::BufferViewDescriptor indexRawDesc = indexAssetViewPtr->GetBufferViewDescriptor(); - float closestNormalizedDistance = 1.f; bool anyHit = false; - const AZ::Vector3 rayEnd = rayStart + dir * distance; + const AZ::Vector3 rayEnd = rayStart + rayDir; AZ::Vector3 a, b, c; AZ::Vector3 intersectionNormal; - float normalizedDistance = 1.f; + float shortestDistanceNormalized = AZStd::numeric_limits::max(); const AZ::u32* indexPtr = reinterpret_cast(indexRawBuffer.data()); for (uint32_t indexIter = 0; indexIter <= indexRawDesc.m_elementCount - 3; indexIter += 3, indexPtr += 3) { @@ -247,20 +254,22 @@ namespace AZ p = reinterpret_cast(&positionRawBuffer[index2 * positionElementSize]); c.Set(const_cast(p)); - if (AZ::Intersect::IntersectSegmentTriangleCCW(rayStart, rayEnd, a, b, c, intersectionNormal, normalizedDistance)) + float currentDistanceNormalized; + if (AZ::Intersect::IntersectSegmentTriangleCCW(rayStart, rayEnd, a, b, c, intersectionNormal, currentDistanceNormalized)) { - if (normalizedDistance < closestNormalizedDistance) + anyHit = true; + + if (currentDistanceNormalized < shortestDistanceNormalized) { normal = intersectionNormal; - closestNormalizedDistance = normalizedDistance; + shortestDistanceNormalized = currentDistanceNormalized; } - anyHit = true; } } if (anyHit) { - distance = closestNormalizedDistance * distance; + distanceNormalized = shortestDistanceNormalized; } return anyHit; diff --git a/Gems/Atom/RPI/Code/Source/RPI.Reflect/Model/ModelKdTree.cpp b/Gems/Atom/RPI/Code/Source/RPI.Reflect/Model/ModelKdTree.cpp index bee489c2fd..2ee6d93df3 100644 --- a/Gems/Atom/RPI/Code/Source/RPI.Reflect/Model/ModelKdTree.cpp +++ b/Gems/Atom/RPI/Code/Source/RPI.Reflect/Model/ModelKdTree.cpp @@ -208,10 +208,10 @@ namespace AZ bool ModelKdTree::RayIntersection( const AZ::Vector3& raySrc, const AZ::Vector3& rayDir, float& distanceNormalized, AZ::Vector3& normal) const { - float closestDistanceNormalized = AZStd::numeric_limits::max(); - if (RayIntersectionRecursively(m_pRootNode.get(), raySrc, rayDir, closestDistanceNormalized, normal)) + float shortestDistanceNormalized = AZStd::numeric_limits::max(); + if (RayIntersectionRecursively(m_pRootNode.get(), raySrc, rayDir, shortestDistanceNormalized, normal)) { - distanceNormalized = closestDistanceNormalized; + distanceNormalized = shortestDistanceNormalized; return true; } diff --git a/Gems/Atom/RPI/Code/Tests/Model/ModelTests.cpp b/Gems/Atom/RPI/Code/Tests/Model/ModelTests.cpp index 3ce17bee8b..f039240ee0 100644 --- a/Gems/Atom/RPI/Code/Tests/Model/ModelTests.cpp +++ b/Gems/Atom/RPI/Code/Tests/Model/ModelTests.cpp @@ -22,6 +22,7 @@ #include #include +#include #include #include @@ -568,7 +569,7 @@ namespace UnitTest ValidateModelAsset(serializedModelAsset.Get(), expectedModel); } - // Tests that if we try to set the name on a Model + // Tests that if we try to set the name on a Model // before calling Begin that it will fail. TEST_F(ModelTests, SetNameNoBegin) { @@ -581,7 +582,7 @@ namespace UnitTest creator.SetName("TestName"); } - // Tests that if we try to add a ModelLod to a Model + // Tests that if we try to add a ModelLod to a Model // before calling Begin that it will fail. TEST_F(ModelTests, AddLodNoBegin) { @@ -598,7 +599,7 @@ namespace UnitTest creator.AddLodAsset(AZStd::move(lod)); } - // Tests that if we create a ModelAsset without adding + // Tests that if we create a ModelAsset without adding // any ModelLodAssets that the creator will properly fail to produce an asset. TEST_F(ModelTests, CreateModelNoLods) { @@ -618,8 +619,8 @@ namespace UnitTest ASSERT_EQ(asset.Get(), nullptr); } - // Tests that if we call SetLodIndexBuffer without calling - // Begin first on the ModelLodAssetCreator that it + // Tests that if we call SetLodIndexBuffer without calling + // Begin first on the ModelLodAssetCreator that it // fails as expected. TEST_F(ModelTests, SetLodIndexBufferNoBegin) { @@ -633,8 +634,8 @@ namespace UnitTest creator.SetLodIndexBuffer(validIndexBuffer); } - // Tests that if we call AddLodStreamBuffer without calling - // Begin first on the ModelLodAssetCreator that it + // Tests that if we call AddLodStreamBuffer without calling + // Begin first on the ModelLodAssetCreator that it // fails as expected. TEST_F(ModelTests, AddLodStreamBufferNoBegin) { @@ -648,8 +649,8 @@ namespace UnitTest creator.AddLodStreamBuffer(validStreamBuffer); } - // Tests that if we call BeginMesh without calling - // Begin first on the ModelLodAssetCreator that it + // Tests that if we call BeginMesh without calling + // Begin first on the ModelLodAssetCreator that it // fails as expected. TEST_F(ModelTests, BeginMeshNoBegin) { @@ -662,13 +663,13 @@ namespace UnitTest } // Tests that if we try to set an AABB on a mesh - // without calling Begin or BeginMesh that it fails + // without calling Begin or BeginMesh that it fails // as expected. Also tests the case that Begin *is* // called but BeginMesh is not. TEST_F(ModelTests, SetAabbNoBeginNoBeginMesh) { using namespace AZ; - + RPI::ModelLodAssetCreator creator; AZ::Aabb aabb = AZ::Aabb::CreateCenterRadius(AZ::Vector3::CreateZero(), 1.0f); @@ -691,13 +692,13 @@ namespace UnitTest } // Tests that if we try to set the material id on a mesh - // without calling Begin or BeginMesh that it fails + // without calling Begin or BeginMesh that it fails // as expected. Also tests the case that Begin *is* // called but BeginMesh is not. TEST_F(ModelTests, SetMaterialIdNoBeginNoBeginMesh) { using namespace AZ; - + RPI::ModelLodAssetCreator creator; { @@ -715,7 +716,7 @@ namespace UnitTest } // Tests that if we try to set the index buffer on a mesh - // without calling Begin or BeginMesh that it fails + // without calling Begin or BeginMesh that it fails // as expected. Also tests the case that Begin *is* // called but BeginMesh is not. TEST_F(ModelTests, SetIndexBufferNoBeginNoBeginMesh) @@ -751,7 +752,7 @@ namespace UnitTest } // Tests that if we try to add a stream buffer on a mesh - // without calling Begin or BeginMesh that it fails + // without calling Begin or BeginMesh that it fails // as expected. Also tests the case that Begin *is* // called but BeginMesh is not. TEST_F(ModelTests, AddStreamBufferNoBeginNoBeginMesh) @@ -785,7 +786,7 @@ namespace UnitTest } } - // Tests that if we try to end the creation of a + // Tests that if we try to end the creation of a // ModelLodAsset that has no meshes that it fails // as expected. TEST_F(ModelTests, CreateLodNoMeshes) @@ -804,7 +805,7 @@ namespace UnitTest ASSERT_EQ(asset.Get(), nullptr); } - // Tests that validation still fails when expected + // Tests that validation still fails when expected // even after producing a valid mesh due to a missing // BeginMesh call TEST_F(ModelTests, SecondMeshFailureNoBeginMesh) @@ -862,8 +863,8 @@ namespace UnitTest ASSERT_EQ(asset->GetMeshes().size(), 1); } - // Tests that validation still fails when expected - // even after producing a valid mesh due to SetMeshX + // Tests that validation still fails when expected + // even after producing a valid mesh due to SetMeshX // calls coming after End TEST_F(ModelTests, SecondMeshAfterEnd) { @@ -907,7 +908,7 @@ namespace UnitTest AZ::Aabb aabb = AZ::Aabb::CreateCenterRadius(Vector3::CreateZero(), 1.0f); ErrorMessageFinder messageFinder("Begin() was not called", 6); - + creator.BeginMesh(); creator.SetMeshAabb(AZStd::move(aabb)); creator.SetMeshMaterialAsset(m_materialAsset); @@ -955,6 +956,20 @@ namespace UnitTest EXPECT_EQ(uvStreamTangentBitmask.GetFullTangentBitmask(), 0x70000F51); } + // + // +----+ + // / /| + // +----+ | + // | | + + // | |/ + // +----+ + // + static constexpr AZStd::array CubePositions = { -1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, -1.0f, 1.0f, + -1.0f, 1.0f, -1.0f, 1.0f, 1.0f, -1.0f, -1.0f, -1.0f, -1.0f, 1.0f, -1.0f, -1.0f }; + static constexpr AZStd::array CubeIndices = { + uint32_t{ 0 }, 2, 1, 1, 2, 3, 4, 5, 6, 5, 7, 6, 0, 4, 2, 4, 6, 2, 1, 3, 5, 5, 3, 7, 0, 1, 4, 4, 1, 5, 2, 6, 3, 6, 7, 3, + }; + // This class creates a Model with one LOD, whose mesh contains 2 planes. Plane 1 is in the XY plane at Z=-0.5, and // plane 2 is in the XY plane at Z=0.5. The two planes each have 9 quads which have been triangulated. It only has // a position and index buffer. @@ -972,52 +987,75 @@ namespace UnitTest // *---*---*---* // \ / \ / \ / \ // *---*---*---* + static constexpr AZStd::array TwoSeparatedPlanesPositions{ + -1.0f, -0.333f, -0.5f, -0.333f, -1.0f, -0.5f, -0.333f, -0.333f, -0.5f, 0.333f, -0.333f, -0.5f, 1.0f, -1.0f, -0.5f, + 1.0f, -0.333f, -0.5f, 0.333f, -1.0f, -0.5f, 0.333f, 1.0f, -0.5f, 1.0f, 0.333f, -0.5f, 1.0f, 1.0f, -0.5f, + 0.333f, 0.333f, -0.5f, -0.333f, 1.0f, -0.5f, -0.333f, 0.333f, -0.5f, -1.0f, 1.0f, -0.5f, -1.0f, 0.333f, -0.5f, + -1.0f, -0.333f, 0.5f, -0.333f, -1.0f, 0.5f, -0.333f, -0.333f, 0.5f, 0.333f, -0.333f, 0.5f, 1.0f, -1.0f, 0.5f, + 1.0f, -0.333f, 0.5f, -0.333f, -0.333f, 0.5f, 0.333f, -1.0f, 0.5f, 0.333f, -0.333f, 0.5f, 0.333f, 1.0f, 0.5f, + 1.0f, 0.333f, 0.5f, 1.0f, 1.0f, 0.5f, 0.333f, 0.333f, 0.5f, 1.0f, -0.333f, 0.5f, -0.333f, 1.0f, 0.5f, + -0.333f, 0.333f, 0.5f, 0.333f, -0.333f, 0.5f, 0.333f, 0.333f, 0.5f, -1.0f, 1.0f, 0.5f, -0.333f, 0.333f, 0.5f, + -1.0f, 0.333f, 0.5f, -1.0f, -1.0f, -0.5f, -1.0f, -1.0f, 0.5f, 0.333f, -0.333f, 0.5f, 0.333f, -1.0f, 0.5f, + 1.0f, -1.0f, 0.5f, 0.333f, -1.0f, 0.5f, 0.333f, 0.333f, 0.5f, 0.333f, -0.333f, 0.5f, 1.0f, -0.333f, 0.5f, + -0.333f, 0.333f, 0.5f, -0.333f, -0.333f, 0.5f, 0.333f, -0.333f, 0.5f, + }; + // clang-format off + static constexpr AZStd::array TwoSeparatedPlanesIndices{ + uint32_t{ 0 }, 1, 2, 3, 4, 5, 2, 6, 3, 7, 8, 9, 10, 5, 8, 11, 10, 7, 12, 3, 10, 13, 12, 11, 14, 2, 12, + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 25, 29, 27, 24, 30, 31, 32, 33, 34, 29, 35, 17, 34, + 0, 36, 1, 3, 6, 4, 2, 1, 6, 7, 10, 8, 10, 3, 5, 11, 12, 10, 12, 2, 3, 13, 14, 12, 14, 0, 2, + 15, 37, 16, 38, 39, 40, 17, 16, 41, 24, 27, 25, 42, 43, 44, 29, 34, 27, 45, 46, 47, 33, 35, 34, 35, 15, 17, + }; + // clang-format on + + // Ensure that the index buffer references all the positions in the position buffer + static constexpr inline auto minmaxElement = AZStd::minmax_element(begin(TwoSeparatedPlanesIndices), end(TwoSeparatedPlanesIndices)); + static_assert(*minmaxElement.second == (TwoSeparatedPlanesPositions.size() / 3) - 1); + template class TD; - class TwoSeparatedPlanesMesh + class TestMesh { public: - TwoSeparatedPlanesMesh() + TestMesh(const float* positions, size_t positionCount, const uint32_t* indices, size_t indicesCount) { - using namespace AZ; - - RPI::ModelLodAssetCreator lodCreator; - lodCreator.Begin(Data::AssetId(AZ::Uuid::CreateRandom())); + AZ::RPI::ModelLodAssetCreator lodCreator; + lodCreator.Begin(AZ::Data::AssetId(AZ::Uuid::CreateRandom())); lodCreator.BeginMesh(); - lodCreator.SetMeshAabb(Aabb::CreateFromMinMax({-1.0f, -1.0f, -0.5f}, {1.0f, 1.0f, 0.5f})); + lodCreator.SetMeshAabb(AZ::Aabb::CreateFromMinMax({-1.0f, -1.0f, -0.5f}, {1.0f, 1.0f, 0.5f})); lodCreator.SetMeshMaterialAsset( AZ::Data::Asset(AZ::Data::AssetId(AZ::Uuid::CreateRandom(), 0), AZ::AzTypeInfo::Uuid(), "") ); { - AZ::Data::Asset indexBuffer = BuildTestBuffer(s_indexes.size(), sizeof(uint32_t)); - AZStd::copy(s_indexes.begin(), s_indexes.end(), reinterpret_cast(const_cast(indexBuffer->GetBuffer().data()))); + AZ::Data::Asset indexBuffer = BuildTestBuffer(indicesCount, sizeof(uint32_t)); + AZStd::copy(indices, indices + indicesCount, reinterpret_cast(const_cast(indexBuffer->GetBuffer().data()))); lodCreator.SetMeshIndexBuffer({ indexBuffer, - RHI::BufferViewDescriptor::CreateStructured(0, s_indexes.size(), sizeof(uint32_t)) + AZ::RHI::BufferViewDescriptor::CreateStructured(0, indicesCount, sizeof(uint32_t)) }); } { - AZ::Data::Asset positionBuffer = BuildTestBuffer(s_positions.size() / 3, sizeof(float) * 3); - AZStd::copy(s_positions.begin(), s_positions.end(), reinterpret_cast(const_cast(positionBuffer->GetBuffer().data()))); + AZ::Data::Asset positionBuffer = BuildTestBuffer(positionCount / 3, sizeof(float) * 3); + AZStd::copy(positions, positions + positionCount, reinterpret_cast(const_cast(positionBuffer->GetBuffer().data()))); lodCreator.AddMeshStreamBuffer( AZ::RHI::ShaderSemantic(AZ::Name("POSITION")), AZ::Name(), { positionBuffer, - RHI::BufferViewDescriptor::CreateStructured(0, s_positions.size() / 3, sizeof(float) * 3) + AZ::RHI::BufferViewDescriptor::CreateStructured(0, positionCount / 3, sizeof(float) * 3) } ); } lodCreator.EndMesh(); - Data::Asset lodAsset; + AZ::Data::Asset lodAsset; lodCreator.End(lodAsset); - RPI::ModelAssetCreator modelCreator; - modelCreator.Begin(Data::AssetId(AZ::Uuid::CreateRandom())); + AZ::RPI::ModelAssetCreator modelCreator; + modelCreator.Begin(AZ::Data::AssetId(AZ::Uuid::CreateRandom())); modelCreator.SetName("TestModel"); modelCreator.AddLodAsset(AZStd::move(lodAsset)); modelCreator.End(m_modelAsset); @@ -1030,40 +1068,20 @@ namespace UnitTest private: AZ::Data::Asset m_modelAsset; - - static constexpr AZStd::array s_positions{ - -1.0f, -0.333f, -0.5f, -0.333f, -1.0f, -0.5f, -0.333f, -0.333f, -0.5f, 0.333f, -0.333f, -0.5f, 1.0f, -1.0f, -0.5f, - 1.0f, -0.333f, -0.5f, 0.333f, -1.0f, -0.5f, 0.333f, 1.0f, -0.5f, 1.0f, 0.333f, -0.5f, 1.0f, 1.0f, -0.5f, - 0.333f, 0.333f, -0.5f, -0.333f, 1.0f, -0.5f, -0.333f, 0.333f, -0.5f, -1.0f, 1.0f, -0.5f, -1.0f, 0.333f, -0.5f, - -1.0f, -0.333f, 0.5f, -0.333f, -1.0f, 0.5f, -0.333f, -0.333f, 0.5f, 0.333f, -0.333f, 0.5f, 1.0f, -1.0f, 0.5f, - 1.0f, -0.333f, 0.5f, -0.333f, -0.333f, 0.5f, 0.333f, -1.0f, 0.5f, 0.333f, -0.333f, 0.5f, 0.333f, 1.0f, 0.5f, - 1.0f, 0.333f, 0.5f, 1.0f, 1.0f, 0.5f, 0.333f, 0.333f, 0.5f, 1.0f, -0.333f, 0.5f, -0.333f, 1.0f, 0.5f, - -0.333f, 0.333f, 0.5f, 0.333f, -0.333f, 0.5f, 0.333f, 0.333f, 0.5f, -1.0f, 1.0f, 0.5f, -0.333f, 0.333f, 0.5f, - -1.0f, 0.333f, 0.5f, -1.0f, -1.0f, -0.5f, -1.0f, -1.0f, 0.5f, 0.333f, -0.333f, 0.5f, 0.333f, -1.0f, 0.5f, - 1.0f, -1.0f, 0.5f, 0.333f, -1.0f, 0.5f, 0.333f, 0.333f, 0.5f, 0.333f, -0.333f, 0.5f, 1.0f, -0.333f, 0.5f, - -0.333f, 0.333f, 0.5f, -0.333f, -0.333f, 0.5f, 0.333f, -0.333f, 0.5f, - }; - static constexpr AZStd::array s_indexes{ - uint32_t{0}, 1, 2, 3, 4, 5, 2, 6, 3, 7, 8, 9, 10, 5, 8, 11, 10, 7, 12, 3, 10, 13, 12, 11, 14, 2, 12, - 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 25, 29, 27, 24, 30, 31, 32, 33, 34, 29, 35, 17, 34, - 0, 36, 1, 3, 6, 4, 2, 1, 6, 7, 10, 8, 10, 3, 5, 11, 12, 10, 12, 2, 3, 13, 14, 12, 14, 0, 2, - 15, 37, 16, 38, 39, 40, 17, 16, 41, 24, 27, 25, 42, 43, 44, 29, 34, 27, 45, 46, 47, 33, 35, 34, 35, 15, 17, - }; - - // Ensure that the index buffer references all the positions in the position buffer - static constexpr inline auto minmaxElement = AZStd::minmax_element(begin(s_indexes), end(s_indexes)); - static_assert(*minmaxElement.second == (s_positions.size() / 3) - 1); }; - struct KdTreeIntersectParams + struct IntersectParams { float xpos; float ypos; float zpos; + float xdir; + float ydir; + float zdir; float expectedDistance; bool expectedShouldIntersect; - friend std::ostream& operator<<(std::ostream& os, const KdTreeIntersectParams& param) + friend std::ostream& operator<<(std::ostream& os, const IntersectParams& param) { return os << "xpos:" << param.xpos @@ -1076,13 +1094,15 @@ namespace UnitTest class KdTreeIntersectsParameterizedFixture : public ModelTests - , public ::testing::WithParamInterface + , public ::testing::WithParamInterface { }; TEST_P(KdTreeIntersectsParameterizedFixture, KdTreeIntersects) { - TwoSeparatedPlanesMesh mesh; + TestMesh mesh( + TwoSeparatedPlanesPositions.data(), TwoSeparatedPlanesPositions.size(), TwoSeparatedPlanesIndices.data(), + TwoSeparatedPlanesIndices.size()); AZ::RPI::ModelKdTree kdTree; ASSERT_TRUE(kdTree.Build(mesh.GetModel().Get())); @@ -1092,38 +1112,40 @@ namespace UnitTest EXPECT_THAT( kdTree.RayIntersection( - AZ::Vector3(GetParam().xpos, GetParam().ypos, GetParam().zpos), AZ::Vector3::CreateAxisZ(-1.0f), distance, normal), + AZ::Vector3(GetParam().xpos, GetParam().ypos, GetParam().zpos), + AZ::Vector3(GetParam().xdir, GetParam().ydir, GetParam().zdir), distance, normal), testing::Eq(GetParam().expectedShouldIntersect)); EXPECT_THAT(distance, testing::FloatEq(GetParam().expectedDistance)); } - static constexpr inline AZStd::array intersectTestData{ - KdTreeIntersectParams{ -0.1f, 0.0f, 1.0f, 0.5f, true }, - KdTreeIntersectParams{ 0.0f, 0.0f, 1.0f, 0.5f, true }, - KdTreeIntersectParams{ 0.1f, 0.0f, 1.0f, 0.5f, true }, + static constexpr AZStd::array KdTreeIntersectTestData{ + IntersectParams{ -0.1f, 0.0f, 1.0f, 0.0f, 0.0f, -1.0f, 0.5f, true }, + IntersectParams{ 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, -1.0f, 0.5f, true }, + IntersectParams{ 0.1f, 0.0f, 1.0f, 0.0f, 0.0f, -1.0f, 0.5f, true }, // Test the center of each triangle - KdTreeIntersectParams{-0.111f, -0.111f, 1.0f, 0.5f, true}, - KdTreeIntersectParams{-0.111f, -0.778f, 1.0f, 0.5f, true}, - KdTreeIntersectParams{-0.111f, 0.555f, 1.0f, 0.5f, true}, // Should intersect triangle with indices {29, 34, 27} and {11, 12, 10} - KdTreeIntersectParams{-0.555f, -0.555f, 1.0f, 0.5f, true}, - KdTreeIntersectParams{-0.555f, 0.111f, 1.0f, 0.5f, true}, - KdTreeIntersectParams{-0.555f, 0.778f, 1.0f, 0.5f, true}, - KdTreeIntersectParams{-0.778f, -0.111f, 1.0f, 0.5f, true}, - KdTreeIntersectParams{-0.778f, -0.778f, 1.0f, 0.5f, true}, - KdTreeIntersectParams{-0.778f, 0.555f, 1.0f, 0.5f, true}, - KdTreeIntersectParams{0.111f, -0.555f, 1.0f, 0.5f, true}, - KdTreeIntersectParams{0.111f, 0.111f, 1.0f, 0.5f, true}, - KdTreeIntersectParams{0.111f, 0.778f, 1.0f, 0.5f, true}, - KdTreeIntersectParams{0.555f, -0.111f, 1.0f, 0.5f, true}, - KdTreeIntersectParams{0.555f, -0.778f, 1.0f, 0.5f, true}, - KdTreeIntersectParams{0.555f, 0.555f, 1.0f, 0.5f, true}, - KdTreeIntersectParams{0.778f, -0.555f, 1.0f, 0.5f, true}, - KdTreeIntersectParams{0.778f, 0.111f, 1.0f, 0.5f, true}, - KdTreeIntersectParams{0.778f, 0.778f, 1.0f, 0.5f, true}, + IntersectParams{ -0.111f, -0.111f, 1.0f, 0.0f, 0.0f, -1.0f, 0.5f, true }, + IntersectParams{ -0.111f, -0.778f, 1.0f, 0.0f, 0.0f, -1.0f, 0.5f, true }, + IntersectParams{ -0.111f, 0.555f, 1.0f, 0.0f, 0.0f, -1.0f, 0.5f, + true }, // Should intersect triangle with indices {29, 34, 27} and {11, 12, 10} + IntersectParams{ -0.555f, -0.555f, 1.0f, 0.0f, 0.0f, -1.0f, 0.5f, true }, + IntersectParams{ -0.555f, 0.111f, 1.0f, 0.0f, 0.0f, -1.0f, 0.5f, true }, + IntersectParams{ -0.555f, 0.778f, 1.0f, 0.0f, 0.0f, -1.0f, 0.5f, true }, + IntersectParams{ -0.778f, -0.111f, 1.0f, 0.0f, 0.0f, -1.0f, 0.5f, true }, + IntersectParams{ -0.778f, -0.778f, 1.0f, 0.0f, 0.0f, -1.0f, 0.5f, true }, + IntersectParams{ -0.778f, 0.555f, 1.0f, 0.0f, 0.0f, -1.0f, 0.5f, true }, + IntersectParams{ 0.111f, -0.555f, 1.0f, 0.0f, 0.0f, -1.0f, 0.5f, true }, + IntersectParams{ 0.111f, 0.111f, 1.0f, 0.0f, 0.0f, -1.0f, 0.5f, true }, + IntersectParams{ 0.111f, 0.778f, 1.0f, 0.0f, 0.0f, -1.0f, 0.5f, true }, + IntersectParams{ 0.555f, -0.111f, 1.0f, 0.0f, 0.0f, -1.0f, 0.5f, true }, + IntersectParams{ 0.555f, -0.778f, 1.0f, 0.0f, 0.0f, -1.0f, 0.5f, true }, + IntersectParams{ 0.555f, 0.555f, 1.0f, 0.0f, 0.0f, -1.0f, 0.5f, true }, + IntersectParams{ 0.778f, -0.555f, 1.0f, 0.0f, 0.0f, -1.0f, 0.5f, true }, + IntersectParams{ 0.778f, 0.111f, 1.0f, 0.0f, 0.0f, -1.0f, 0.5f, true }, + IntersectParams{ 0.778f, 0.778f, 1.0f, 0.0f, 0.0f, -1.0f, 0.5f, true }, }; - INSTANTIATE_TEST_CASE_P(KdTreeIntersectsPlane, KdTreeIntersectsParameterizedFixture, ::testing::ValuesIn(intersectTestData)); + INSTANTIATE_TEST_CASE_P(KdTreeIntersectsPlane, KdTreeIntersectsParameterizedFixture, ::testing::ValuesIn(KdTreeIntersectTestData)); class KdTreeIntersectsFixture : public ModelTests @@ -1133,7 +1155,10 @@ namespace UnitTest { ModelTests::SetUp(); - m_mesh = AZStd::make_unique(); + m_mesh = AZStd::make_unique( + TwoSeparatedPlanesPositions.data(), TwoSeparatedPlanesPositions.size(), TwoSeparatedPlanesIndices.data(), + TwoSeparatedPlanesIndices.size()); + m_kdTree = AZStd::make_unique(); ASSERT_TRUE(m_kdTree->Build(m_mesh->GetModel().Get())); } @@ -1146,7 +1171,7 @@ namespace UnitTest ModelTests::TearDown(); } - AZStd::unique_ptr m_mesh; + AZStd::unique_ptr m_mesh; AZStd::unique_ptr m_kdTree; }; @@ -1154,7 +1179,7 @@ namespace UnitTest { float t = AZStd::numeric_limits::max(); AZ::Vector3 normal; - + constexpr float rayLength = 100.0f; EXPECT_THAT( m_kdTree->RayIntersection( @@ -1181,4 +1206,85 @@ namespace UnitTest EXPECT_THAT( m_kdTree->RayIntersection(AZ::Vector3::CreateAxisZ(5.0f), -AZ::Vector3::CreateAxisZ(), t, normal), testing::Eq(false)); } + + class BruteForceIntersectsParameterizedFixture + : public ModelTests + , public ::testing::WithParamInterface + { + }; + + TEST_P(BruteForceIntersectsParameterizedFixture, BruteForceIntersectsCube) + { + TestMesh mesh(CubePositions.data(), CubePositions.size(), CubeIndices.data(), CubeIndices.size()); + + float distance = AZStd::numeric_limits::max(); + AZ::Vector3 normal; + + EXPECT_THAT( + mesh.GetModel()->LocalRayIntersectionAgainstModel( + AZ::Vector3(GetParam().xpos, GetParam().ypos, GetParam().zpos), + AZ::Vector3(GetParam().xdir, GetParam().ydir, GetParam().zdir), distance, normal), + testing::Eq(GetParam().expectedShouldIntersect)); + EXPECT_THAT(distance, testing::FloatEq(GetParam().expectedDistance)); + } + + static constexpr AZStd::array BruteForceIntersectTestData{ + IntersectParams{ 5.0f, 0.0f, 5.0f, 0.0f, 0.0f, -1.0f, AZStd::numeric_limits::max(), false }, + IntersectParams{ 0.0f, 0.0f, 1.5f, 0.0f, 0.0f, -1.0f, 0.5f, true }, + IntersectParams{ 5.0f, 0.0f, 0.0f, -10.0f, 0.0f, 0.0f, 0.4f, true }, + IntersectParams{ -5.0f, 0.0f, 0.0f, 20.0f, 0.0f, 0.0f, 0.2f, true }, + IntersectParams{ 0.0f, -10.0f, 0.0f, 0.0f, 20.0f, 0.0f, 0.45f, true }, + IntersectParams{ 0.0f, 20.0f, 0.0f, 0.0f, -40.0f, 0.0f, 0.475f, true }, + IntersectParams{ 0.0f, 20.0f, 0.0f, 0.0f, -19.0f, 0.0f, 1.0f, true }, + }; + + INSTANTIATE_TEST_CASE_P( + BruteForceIntersects, BruteForceIntersectsParameterizedFixture, ::testing::ValuesIn(BruteForceIntersectTestData)); + + class BruteForceModelIntersectsFixture + : public ModelTests + { + public: + void SetUp() override + { + ModelTests::SetUp(); + m_mesh = AZStd::make_unique(CubePositions.data(), CubePositions.size(), CubeIndices.data(), CubeIndices.size()); + } + + void TearDown() override + { + m_mesh.reset(); + ModelTests::TearDown(); + } + + AZStd::unique_ptr m_mesh; + }; + + TEST_F(BruteForceModelIntersectsFixture, BruteForceIntersectionDetectedWithCube) + { + float t = 0.0f; + AZ::Vector3 normal; + + // firing down the negative z axis, positioned 5 units from cube (cube is 2x2x2 so intersection + // happens at 1 in z) + EXPECT_THAT( + m_mesh->GetModel()->LocalRayIntersectionAgainstModel( + AZ::Vector3::CreateAxisZ(5.0f), -AZ::Vector3::CreateAxisZ(10.0f), t, normal), + testing::Eq(true)); + EXPECT_THAT(t, testing::FloatEq(0.4f)); + } + + TEST_F(BruteForceModelIntersectsFixture, BruteForceIntersectionDetectedAndNormalSetAtEndOfRay) + { + float t = 0.0f; + AZ::Vector3 normal = AZ::Vector3::CreateOne(); // invalid starting normal + + // ensure the intersection happens right at the end of the ray + EXPECT_THAT( + m_mesh->GetModel()->LocalRayIntersectionAgainstModel( + AZ::Vector3::CreateAxisY(10.0f), -AZ::Vector3::CreateAxisY(9.0f), t, normal), + testing::Eq(true)); + EXPECT_THAT(t, testing::FloatEq(1.0f)); + EXPECT_THAT(normal, IsClose(AZ::Vector3::CreateAxisY())); + } } // namespace UnitTest