From 2812ec2024a7c884ad0110f84602ef54d98c9dd5 Mon Sep 17 00:00:00 2001 From: Tom Hulton-Harrop <82228511+hultonha@users.noreply.github.com> Date: Thu, 11 Nov 2021 16:33:08 +0000 Subject: [PATCH] Fix brute force mesh intersection function (#5447) * fix brute force mesh intersection function Signed-off-by: Tom Hulton-Harrop <82228511+hultonha@users.noreply.github.com> * add test for brute force ray intersection fix Signed-off-by: Tom Hulton-Harrop <82228511+hultonha@users.noreply.github.com> * refactor tests to remove as much duplication and provide API for future tests if required Signed-off-by: Tom Hulton-Harrop <82228511+hultonha@users.noreply.github.com> * small updates after review feedback Signed-off-by: Tom Hulton-Harrop <82228511+hultonha@users.noreply.github.com> * update following review feedback Signed-off-by: Tom Hulton-Harrop <82228511+hultonha@users.noreply.github.com> * fix for pointer offset Signed-off-by: Tom Hulton-Harrop <82228511+hultonha@users.noreply.github.com> --- Code/Framework/AzCore/AzCore/Math/Vector3.h | 2 +- Code/Framework/AzCore/AzCore/Math/Vector3.inl | 2 +- .../Model/ModelAssetBuilderComponent.cpp | 2 +- .../Source/RPI.Reflect/Model/ModelAsset.cpp | 56 +++--- Gems/Atom/RPI/Code/Tests/Model/ModelTests.cpp | 179 +++++++++++++++--- 5 files changed, 178 insertions(+), 63 deletions(-) diff --git a/Code/Framework/AzCore/AzCore/Math/Vector3.h b/Code/Framework/AzCore/AzCore/Math/Vector3.h index 821dc8292c..6b7c53266d 100644 --- a/Code/Framework/AzCore/AzCore/Math/Vector3.h +++ b/Code/Framework/AzCore/AzCore/Math/Vector3.h @@ -100,7 +100,7 @@ namespace AZ void Set(float x, float y, float z); //! Sets components from an array of 3 floats in xyz order. - void Set(float values[]); + void Set(const float values[]); //! Indexed access using operator(), just for convenience. float operator()(int32_t index) const; diff --git a/Code/Framework/AzCore/AzCore/Math/Vector3.inl b/Code/Framework/AzCore/AzCore/Math/Vector3.inl index 879ade38cf..6371c688b8 100644 --- a/Code/Framework/AzCore/AzCore/Math/Vector3.inl +++ b/Code/Framework/AzCore/AzCore/Math/Vector3.inl @@ -186,7 +186,7 @@ namespace AZ } - AZ_MATH_INLINE void Vector3::Set(float values[]) + AZ_MATH_INLINE void Vector3::Set(const float values[]) { m_value = Simd::Vec3::LoadImmediate(values[0], values[1], values[2]); } diff --git a/Gems/Atom/RPI/Code/Source/RPI.Builders/Model/ModelAssetBuilderComponent.cpp b/Gems/Atom/RPI/Code/Source/RPI.Builders/Model/ModelAssetBuilderComponent.cpp index 9fc99e3ea4..dbf0fea791 100644 --- a/Gems/Atom/RPI/Code/Source/RPI.Builders/Model/ModelAssetBuilderComponent.cpp +++ b/Gems/Atom/RPI/Code/Source/RPI.Builders/Model/ModelAssetBuilderComponent.cpp @@ -2088,7 +2088,7 @@ namespace AZ AZ::Vector3 vpos; //note: it seems to be fastest to reuse a local Vector3 rather than constructing new ones each loop iteration for (uint32_t i = 0; i < elementCount; ++i) { - vpos.Set(const_cast(reinterpret_cast(&buffer[i]))); + vpos.Set(reinterpret_cast(&buffer[i])); aabb.AddPoint(vpos); } } 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 9a432643d7..e362229d2d 100644 --- a/Gems/Atom/RPI/Code/Source/RPI.Reflect/Model/ModelAsset.cpp +++ b/Gems/Atom/RPI/Code/Source/RPI.Reflect/Model/ModelAsset.cpp @@ -201,23 +201,11 @@ namespace AZ AZ::Vector3& normal) const { const BufferAssetView& indexBufferView = mesh.GetIndexBufferAssetView(); - const AZStd::array_view& streamBufferList = mesh.GetStreamBufferInfoList(); + const BufferAssetView* positionBufferView = mesh.GetSemanticBufferAssetView(m_positionName); - // find position semantic - const ModelLodAsset::Mesh::StreamBufferInfo* positionBuffer = nullptr; - - for (const ModelLodAsset::Mesh::StreamBufferInfo& bufferInfo : streamBufferList) + if (positionBufferView && positionBufferView->GetBufferAsset().Get()) { - if (bufferInfo.m_semantic.m_name == m_positionName) - { - positionBuffer = &bufferInfo; - break; - } - } - - if (positionBuffer && positionBuffer->m_bufferAssetView.GetBufferAsset().Get()) - { - BufferAsset* bufferAssetViewPtr = positionBuffer->m_bufferAssetView.GetBufferAsset().Get(); + BufferAsset* bufferAssetViewPtr = positionBufferView->GetBufferAsset().Get(); BufferAsset* indexAssetViewPtr = indexBufferView.GetBufferAsset().Get(); if (!bufferAssetViewPtr || !indexAssetViewPtr) @@ -225,7 +213,7 @@ namespace AZ return false; } - RHI::BufferViewDescriptor positionBufferViewDesc = bufferAssetViewPtr->GetBufferViewDescriptor(); + RHI::BufferViewDescriptor positionBufferViewDesc = positionBufferView->GetBufferViewDescriptor(); AZStd::array_view positionRawBuffer = bufferAssetViewPtr->GetBuffer(); const uint32_t positionElementSize = positionBufferViewDesc.m_elementSize; @@ -234,22 +222,28 @@ namespace AZ // Position is 3 floats if (positionElementSize != sizeof(float) * 3) { - AZ_Warning("ModelAsset", false, "unsupported mesh posiiton format, only full 3 floats per vertex are supported at the moment"); + AZ_Warning( + "ModelAsset", false, "unsupported mesh posiiton format, only full 3 floats per vertex are supported at the moment"); return false; } + RHI::BufferViewDescriptor indexBufferViewDesc = indexBufferView.GetBufferViewDescriptor(); AZStd::array_view indexRawBuffer = indexAssetViewPtr->GetBuffer(); - RHI::BufferViewDescriptor indexRawDesc = indexAssetViewPtr->GetBufferViewDescriptor(); - - bool anyHit = false; const AZ::Vector3 rayEnd = rayStart + rayDir; AZ::Vector3 a, b, c; AZ::Vector3 intersectionNormal; + bool anyHit = false; 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) + + const AZ::u32* indexPtr = reinterpret_cast( + indexRawBuffer.data() + (indexBufferViewDesc.m_elementOffset * indexBufferViewDesc.m_elementSize)); + const float* positionPtr = reinterpret_cast( + positionRawBuffer.data() + (positionBufferViewDesc.m_elementOffset * positionBufferViewDesc.m_elementSize)); + + constexpr int StepSize = 3; // number of values per vertex (x, y, z) + for (uint32_t indexIter = 0; indexIter < indexBufferViewDesc.m_elementCount; indexIter += StepSize, indexPtr += StepSize) { AZ::u32 index0 = indexPtr[0]; AZ::u32 index1 = indexPtr[1]; @@ -261,17 +255,17 @@ namespace AZ return false; } - const float* p = reinterpret_cast(&positionRawBuffer[index0 * positionElementSize]); - a.Set(const_cast(p)); // faster than AZ::Vector3 c-tor - - p = reinterpret_cast(&positionRawBuffer[index1 * positionElementSize]); - b.Set(const_cast(p)); - - p = reinterpret_cast(&positionRawBuffer[index2 * positionElementSize]); - c.Set(const_cast(p)); + // faster than AZ::Vector3 c-tor + const float* aRef = &positionPtr[index0 * StepSize]; + a.Set(aRef); + const float* bRef = &positionPtr[index1 * StepSize]; + b.Set(bRef); + const float* cRef = &positionPtr[index2 * StepSize]; + c.Set(cRef); float currentDistanceNormalized; - if (AZ::Intersect::IntersectSegmentTriangleCCW(rayStart, rayEnd, a, b, c, intersectionNormal, currentDistanceNormalized)) + if (AZ::Intersect::IntersectSegmentTriangleCCW( + rayStart, rayEnd, a, b, c, intersectionNormal, currentDistanceNormalized)) { anyHit = true; diff --git a/Gems/Atom/RPI/Code/Tests/Model/ModelTests.cpp b/Gems/Atom/RPI/Code/Tests/Model/ModelTests.cpp index 7b07e14de0..81d773d8c0 100644 --- a/Gems/Atom/RPI/Code/Tests/Model/ModelTests.cpp +++ b/Gems/Atom/RPI/Code/Tests/Model/ModelTests.cpp @@ -38,7 +38,7 @@ namespace UnitTest bufferData.resize(bufferSize); //The actual data doesn't matter - const uint8_t bufferDataSize = static_cast(bufferData.size()); + const uint8_t bufferDataSize = aznumeric_cast(bufferData.size()); for (uint8_t i = 0; i < bufferDataSize; ++i) { bufferData[i] = i; @@ -248,7 +248,8 @@ namespace UnitTest return asset; } - AZ::Data::Asset BuildTestModel(const uint32_t lodCount, const uint32_t sharedMeshCount, const uint32_t separateMeshCount, ExpectedModel& expectedModel) + AZ::Data::Asset BuildTestModel( + const uint32_t lodCount, const uint32_t sharedMeshCount, const uint32_t separateMeshCount, ExpectedModel& expectedModel) { using namespace AZ; @@ -989,6 +990,9 @@ namespace UnitTest 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, }; + static constexpr AZStd::array QuadPositions = { -1.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f, -1.0f, -1.0f, 0.0f, 1.0f, -1.0f, 0.0f }; + static constexpr AZStd::array QuadIndices = { uint32_t{ 0 }, 2, 1, 1, 2, 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. @@ -1031,42 +1035,80 @@ namespace UnitTest static constexpr inline auto minmaxElement = AZStd::minmax_element(begin(TwoSeparatedPlanesIndices), end(TwoSeparatedPlanesIndices)); static_assert(*minmaxElement.second == (TwoSeparatedPlanesPositions.size() / 3) - 1); - template class TD; class TestMesh { public: + TestMesh() = default; + TestMesh(const float* positions, size_t positionCount, const uint32_t* indices, size_t indicesCount) { AZ::RPI::ModelLodAssetCreator lodCreator; - lodCreator.Begin(AZ::Data::AssetId(AZ::Uuid::CreateRandom())); + Begin(lodCreator); + Add(lodCreator, positions, positionCount, /*positionOffset=*/0, indices, indicesCount, /*indexOffset=*/0); + End(lodCreator); + } + // initiate the asset lod creation process (note: End must be called after meshes have been added). + void Begin(AZ::RPI::ModelLodAssetCreator& lodCreator) + { + lodCreator.Begin(AZ::Data::AssetId(AZ::Uuid::CreateRandom())); + } + + // add a sub mesh and reuse existing position/index buffer (be very careful with the offsets used) + void Add( + AZ::RPI::ModelLodAssetCreator& lodCreator, + const float* positions, + size_t positionCount, + size_t positionOffset, + AZ::Data::Asset positionBuffer, + const uint32_t* indices, + size_t indexCount, + size_t indexOffset, + AZ::Data::Asset indexBuffer) + { lodCreator.BeginMesh(); - lodCreator.SetMeshAabb(AZ::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.SetMeshMaterialSlot(AZ::Sfmt::GetInstance().Rand32()); - { - AZ::Data::Asset indexBuffer = BuildTestBuffer(static_cast(indicesCount), sizeof(uint32_t)); - AZStd::copy(indices, indices + indicesCount, reinterpret_cast(const_cast(indexBuffer->GetBuffer().data()))); - lodCreator.SetMeshIndexBuffer({ - indexBuffer, - AZ::RHI::BufferViewDescriptor::CreateStructured(0, static_cast(indicesCount), sizeof(uint32_t)) - }); - } + AZStd::copy( + indices, indices + indexCount, + reinterpret_cast(const_cast(indexBuffer->GetBuffer().data())) + indexOffset); + lodCreator.SetMeshIndexBuffer( + { indexBuffer, + AZ::RHI::BufferViewDescriptor::CreateStructured( + aznumeric_cast(indexOffset), aznumeric_cast(indexCount), sizeof(uint32_t)) }); + AZStd::copy( + positions, positions + positionCount, + reinterpret_cast(const_cast(positionBuffer->GetBuffer().data())) + positionOffset); + lodCreator.AddMeshStreamBuffer( + AZ::RHI::ShaderSemantic(AZ::Name("POSITION")), AZ::Name(), + { positionBuffer, + AZ::RHI::BufferViewDescriptor::CreateStructured( + aznumeric_cast(positionOffset / 3), aznumeric_cast(positionCount / 3), sizeof(float) * 3) }); - { - AZ::Data::Asset positionBuffer = BuildTestBuffer(static_cast(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, - AZ::RHI::BufferViewDescriptor::CreateStructured(0, static_cast(positionCount / 3), sizeof(float) * 3) - } - ); - } lodCreator.EndMesh(); + } + // overload of Add - here a new index/position buffer is created for the new data instead of potentially reusing an existing buffer + void Add( + AZ::RPI::ModelLodAssetCreator& lodCreator, + const float* positions, + size_t positionCount, + size_t positionOffset, + const uint32_t* indices, + size_t indexCount, + size_t indexOffset) + { + AZ::Data::Asset indexBuffer = BuildTestBuffer(aznumeric_cast(indexCount), sizeof(uint32_t)); + AZ::Data::Asset positionBuffer = + BuildTestBuffer(aznumeric_cast(positionCount / 3), sizeof(float) * 3); + + Add(lodCreator, positions, positionCount, positionOffset, positionBuffer, indices, indexCount, indexOffset, indexBuffer); + } + + // complete the asset lod creation process + void End(AZ::RPI::ModelLodAssetCreator& lodCreator) + { AZ::Data::Asset lodAsset; lodCreator.End(lodAsset); @@ -1199,7 +1241,7 @@ namespace UnitTest constexpr float rayLength = 100.0f; EXPECT_THAT( m_kdTree->RayIntersection( - AZ::Vector3::CreateZero(), AZ::Vector3::CreateAxisZ(-rayLength), t, normal), testing::Eq(true)); + AZ::Vector3::CreateZero(), AZ::Vector3::CreateAxisZ(-rayLength), t, normal), testing::IsTrue()); EXPECT_THAT(t, testing::FloatEq(0.005f)); } @@ -1210,7 +1252,7 @@ namespace UnitTest constexpr float rayLength = 10.0f; EXPECT_THAT( - m_kdTree->RayIntersection(AZ::Vector3::CreateAxisZ(0.75f), AZ::Vector3::CreateAxisZ(-rayLength), t, normal), testing::Eq(true)); + m_kdTree->RayIntersection(AZ::Vector3::CreateAxisZ(0.75f), AZ::Vector3::CreateAxisZ(-rayLength), t, normal), testing::IsTrue()); EXPECT_THAT(t, testing::FloatEq(0.025f)); } @@ -1288,7 +1330,7 @@ namespace UnitTest EXPECT_THAT( m_mesh->GetModel()->LocalRayIntersectionAgainstModel( AZ::Vector3::CreateAxisZ(5.0f), -AZ::Vector3::CreateAxisZ(10.0f), AllowBruteForce, t, normal), - testing::Eq(true)); + testing::IsTrue()); EXPECT_THAT(t, testing::FloatEq(0.4f)); } @@ -1302,8 +1344,87 @@ namespace UnitTest EXPECT_THAT( m_mesh->GetModel()->LocalRayIntersectionAgainstModel( AZ::Vector3::CreateAxisY(10.0f), -AZ::Vector3::CreateAxisY(9.0f), AllowBruteForce, t, normal), - testing::Eq(true)); + testing::IsTrue()); EXPECT_THAT(t, testing::FloatEq(1.0f)); EXPECT_THAT(normal, IsClose(AZ::Vector3::CreateAxisY())); } + + // test to verify that each secondary sub meshes are still intersected with correctly when using brute-force + // ray intersection + class BruteForceMultiModelIntersectsFixture : public ModelTests + { + public: + inline static const float QuadOffsetX = 15.0f; + + void SetUp() override + { + ModelTests::SetUp(); + m_mesh = AZStd::make_unique(); + + AZ::RPI::ModelLodAssetCreator lodCreator; + m_mesh->Begin(lodCreator); + + // take default quad positions and offset in X by set amount + AZStd::vector offsetQuadPositions; + offsetQuadPositions.resize(QuadPositions.size()); + AZStd::copy(QuadPositions.begin(), QuadPositions.end(), offsetQuadPositions.begin()); + for (size_t xVertIndex = 0; xVertIndex < offsetQuadPositions.size(); xVertIndex += 3) + { + offsetQuadPositions[xVertIndex] += QuadOffsetX; + } + + // create shared buffer to store cube and quad mesh in the same buffer + const size_t indicesCount = QuadIndices.size() + CubeIndices.size(); + const size_t positionCount = QuadPositions.size() + CubePositions.size(); + AZ::Data::Asset indexBuffer = BuildTestBuffer(aznumeric_cast(indicesCount), sizeof(uint32_t)); + AZ::Data::Asset positionBuffer = + BuildTestBuffer(aznumeric_cast(positionCount / 3), sizeof(float) * 3); + + // add the cube mesh + m_mesh->Add( + lodCreator, CubePositions.data(), CubePositions.size(), 0, positionBuffer, CubeIndices.data(), CubeIndices.size(), 0, + indexBuffer); + // add the quad mesh (offset by the cube position and index data into the same buffer) + m_mesh->Add( + lodCreator, offsetQuadPositions.data(), offsetQuadPositions.size(), /*offset=*/CubePositions.size(), positionBuffer, + QuadIndices.data(), QuadIndices.size(), /*offset=*/CubeIndices.size(), indexBuffer); + + m_mesh->End(lodCreator); + } + + void TearDown() override + { + m_mesh.reset(); + ModelTests::TearDown(); + } + + AZStd::unique_ptr m_mesh; + inline static constexpr bool AllowBruteForce = false; + }; + + TEST_F(BruteForceMultiModelIntersectsFixture, RayIntersectsWithFirstSubMesh) + { + float t = 0.0f; + AZ::Vector3 normal = AZ::Vector3::CreateOne(); // invalid starting normal + // fire a ray at the first sub mesh and ensure a successful hit is returned + EXPECT_THAT( + m_mesh->GetModel()->LocalRayIntersectionAgainstModel( + AZ::Vector3(0.0f, 0.0f, 5.0f), -AZ::Vector3::CreateAxisZ(10.0f), AllowBruteForce, t, normal), + testing::IsTrue()); + EXPECT_THAT(t, testing::FloatEq(0.4f)); + EXPECT_THAT(normal, IsClose(AZ::Vector3::CreateAxisZ())); + } + + TEST_F(BruteForceMultiModelIntersectsFixture, RayIntersectsWithSecondSubMesh) + { + float t = 0.0f; + AZ::Vector3 normal = AZ::Vector3::CreateOne(); // invalid starting normal + // fire a ray at the second sub mesh and ensure a successful hit is returned + EXPECT_THAT( + m_mesh->GetModel()->LocalRayIntersectionAgainstModel( + AZ::Vector3(QuadOffsetX, 0.0f, 5.0f), -AZ::Vector3::CreateAxisZ(10.0f), AllowBruteForce, t, normal), + testing::IsTrue()); + EXPECT_THAT(t, testing::FloatEq(0.5f)); + EXPECT_THAT(normal, IsClose(AZ::Vector3::CreateAxisZ())); + } } // namespace UnitTest