/* * Copyright (c) Contributors to the Open 3D Engine Project. * For complete copyright and license terms please see the LICENSE at the root of this distribution. * * SPDX-License-Identifier: Apache-2.0 OR MIT * */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace UnitTest { AZ::Data::Asset BuildTestBuffer(const uint32_t elementCount, const uint32_t elementSize) { using namespace AZ; const uint32_t bufferSize = elementCount * elementSize; AZStd::vector bufferData; bufferData.resize(bufferSize); //The actual data doesn't matter const uint8_t bufferDataSize = static_cast(bufferData.size()); for (uint8_t i = 0; i < bufferDataSize; ++i) { bufferData[i] = i; } Data::Asset bufferPoolAsset; { auto bufferPoolDesc = AZStd::make_unique(); bufferPoolDesc->m_bindFlags = RHI::BufferBindFlags::InputAssembly; bufferPoolDesc->m_heapMemoryLevel = RHI::HeapMemoryLevel::Host; RPI::ResourcePoolAssetCreator creator; creator.Begin(Uuid::CreateRandom()); creator.SetPoolDescriptor(AZStd::move(bufferPoolDesc)); creator.SetPoolName("TestPool"); EXPECT_TRUE(creator.End(bufferPoolAsset)); } Data::Asset asset; { RHI::BufferDescriptor bufferDescriptor; bufferDescriptor.m_bindFlags = RHI::BufferBindFlags::InputAssembly; bufferDescriptor.m_byteCount = bufferSize; RHI::BufferViewDescriptor bufferViewDescriptor = RHI::BufferViewDescriptor::CreateStructured(0, elementCount, elementSize); RPI::BufferAssetCreator creator; creator.Begin(AZ::Uuid::CreateRandom()); creator.SetPoolAsset(bufferPoolAsset); creator.SetBuffer(bufferData.data(), bufferDescriptor.m_byteCount, bufferDescriptor); creator.SetBufferViewDescriptor(bufferViewDescriptor); EXPECT_TRUE(creator.End(asset)); EXPECT_TRUE(asset.IsReady()); EXPECT_NE(asset.Get(), nullptr); } return asset; } class ModelTests : public RPITestFixture { protected: struct ExpectedMesh { AZ::Aabb m_aabb = AZ::Aabb::CreateNull(); uint32_t m_indexCount = 0; uint32_t m_vertexCount = 0; AZ::RPI::ModelMaterialSlot::StableId m_materialSlotId = AZ::RPI::ModelMaterialSlot::InvalidStableId; }; struct ExpectedLod { AZ::Aabb m_aabb = AZ::Aabb::CreateNull(); AZStd::vector m_meshes; }; struct ExpectedModel { AZ::Aabb m_aabb = AZ::Aabb::CreateNull(); AZStd::vector m_lods; }; void SetUp() override { RPITestFixture::SetUp(); auto assetId = AZ::Data::AssetId(AZ::Uuid::CreateRandom(), 0); auto typeId = AZ::AzTypeInfo::Uuid(); m_materialAsset = AZ::Data::Asset(assetId, typeId, ""); // Some tests attempt to serialize-in the model asset, which should not attempt to actually load this dummy asset reference. m_materialAsset.SetAutoLoadBehavior(AZ::Data::AssetLoadBehaviorNamespace::NoLoad); } AZ::RHI::ShaderSemantic GetPositionSemantic() const { return AZ::RHI::ShaderSemantic(AZ::Name("POSITION")); } bool CalculateAABB(const AZ::RHI::BufferViewDescriptor& bufferViewDesc, const AZ::RPI::BufferAsset& bufferAsset, AZ::Aabb& aabb) { const uint32_t elementSize = bufferViewDesc.m_elementSize; const uint32_t elementCount = bufferViewDesc.m_elementCount; // Position is 3 floats if (elementSize == sizeof(float) * 3) { const uint8_t* buffer = reinterpret_cast(&bufferAsset.GetBuffer()[0]); for (uint32_t i = 0; i < elementCount; ++i) { const uint8_t* p = buffer + (i * 3); aabb.AddPoint(AZ::Vector3(float(p[0]), float(p[1]), float(p[2]))); } } else { // No idea what type of position stream this is return false; } return true; } //! This function assumes the model has "sharedMeshCount + separateMeshCount" unique material slots, with incremental IDs starting at 0. AZ::Data::Asset BuildTestLod(const uint32_t sharedMeshCount, const uint32_t separateMeshCount, ExpectedLod& expectedLod) { using namespace AZ; //Create an Lod with a given number of meshes RPI::ModelLodAssetCreator creator; creator.Begin(Data::AssetId(AZ::Uuid::CreateRandom())); const uint32_t indexCount = 36; const uint32_t vertexCount = 36; RPI::ModelMaterialSlot::StableId materialSlotId = 0; if(sharedMeshCount > 0) { const uint32_t sharedIndexCount = indexCount * sharedMeshCount; const uint32_t sharedVertexCount = vertexCount * sharedMeshCount; Data::Asset sharedIndexBuffer = BuildTestBuffer(sharedIndexCount, sizeof(uint32_t)); Data::Asset sharedPositionBuffer = BuildTestBuffer(sharedVertexCount, sizeof(uint32_t)); creator.SetLodIndexBuffer(sharedIndexBuffer); creator.AddLodStreamBuffer(sharedPositionBuffer); for (uint32_t i = 0; i < sharedMeshCount; ++i) { ExpectedMesh expectedMesh; expectedMesh.m_indexCount = indexCount; expectedMesh.m_vertexCount = vertexCount; expectedMesh.m_materialSlotId = i; RHI::BufferViewDescriptor indexBufferViewDescriptor = RHI::BufferViewDescriptor::CreateStructured(i * indexCount, indexCount, sizeof(uint32_t)); RHI::BufferViewDescriptor vertexBufferViewDescriptor = RHI::BufferViewDescriptor::CreateStructured(i * vertexCount, vertexCount, sizeof(float) * 3); if (!CalculateAABB(vertexBufferViewDescriptor, *sharedPositionBuffer.Get(), expectedMesh.m_aabb)) { return {}; } creator.BeginMesh(); Aabb aabb = expectedMesh.m_aabb; creator.SetMeshAabb(AZStd::move(aabb)); creator.SetMeshMaterialSlot(materialSlotId++); creator.SetMeshIndexBuffer({ sharedIndexBuffer, indexBufferViewDescriptor }); creator.AddMeshStreamBuffer(GetPositionSemantic(), AZ::Name(), { sharedPositionBuffer, vertexBufferViewDescriptor }); creator.EndMesh(); expectedLod.m_aabb.AddAabb(expectedMesh.m_aabb); expectedLod.m_meshes.emplace_back(AZStd::move(expectedMesh)); } } for (uint32_t i = 0; i < separateMeshCount; ++i) { ExpectedMesh expectedMesh; expectedMesh.m_indexCount = indexCount; expectedMesh.m_vertexCount = vertexCount; expectedMesh.m_materialSlotId = sharedMeshCount + i; RHI::BufferViewDescriptor indexBufferViewDescriptor = RHI::BufferViewDescriptor::CreateStructured(0, indexCount, sizeof(uint32_t)); RHI::BufferViewDescriptor positionBufferViewDescriptor = RHI::BufferViewDescriptor::CreateStructured(0, vertexCount, sizeof(float) * 3); Data::Asset positonBuffer = BuildTestBuffer(vertexCount, sizeof(float) * 3); if (!CalculateAABB(positionBufferViewDescriptor, *positonBuffer.Get(), expectedMesh.m_aabb)) { return {}; } creator.BeginMesh(); Aabb aabb = expectedMesh.m_aabb; creator.SetMeshAabb(AZStd::move(aabb)); creator.SetMeshMaterialSlot(materialSlotId++); creator.SetMeshIndexBuffer({ BuildTestBuffer(indexCount, sizeof(uint32_t)), indexBufferViewDescriptor }); creator.AddMeshStreamBuffer(GetPositionSemantic(), AZ::Name(), { positonBuffer, positionBufferViewDescriptor }); creator.EndMesh(); expectedLod.m_aabb.AddAabb(expectedMesh.m_aabb); expectedLod.m_meshes.emplace_back(AZStd::move(expectedMesh)); } Data::Asset asset; EXPECT_TRUE(creator.End(asset)); EXPECT_TRUE(asset.IsReady()); EXPECT_NE(asset.Get(), nullptr); return asset; } AZ::Data::Asset BuildTestModel(const uint32_t lodCount, const uint32_t sharedMeshCount, const uint32_t separateMeshCount, ExpectedModel& expectedModel) { using namespace AZ; RPI::ModelAssetCreator creator; creator.Begin(Data::AssetId(AZ::Uuid::CreateRandom())); creator.SetName("TestModel"); for (RPI::ModelMaterialSlot::StableId materialSlotId = 0; materialSlotId < sharedMeshCount + separateMeshCount; ++materialSlotId) { RPI::ModelMaterialSlot slot; slot.m_defaultMaterialAsset = m_materialAsset; slot.m_displayName = AZStd::string::format("Slot%d", materialSlotId); slot.m_stableId = materialSlotId; creator.AddMaterialSlot(slot); } for (uint32_t i = 0; i < lodCount; ++i) { ExpectedLod expectedLod; creator.AddLodAsset(BuildTestLod(sharedMeshCount, separateMeshCount, expectedLod)); expectedModel.m_aabb.AddAabb(expectedLod.m_aabb); expectedModel.m_lods.emplace_back(AZStd::move(expectedLod)); } Data::Asset asset; EXPECT_TRUE(creator.End(asset)); EXPECT_TRUE(asset.IsReady()); EXPECT_NE(asset.Get(), nullptr); return asset; } void ValidateMesh(const AZ::RPI::ModelLodAsset::Mesh& mesh, const ExpectedMesh& expectedMesh) { EXPECT_TRUE(mesh.GetAabb() == expectedMesh.m_aabb); EXPECT_TRUE(mesh.GetIndexCount() == expectedMesh.m_indexCount); EXPECT_TRUE(mesh.GetVertexCount() == expectedMesh.m_vertexCount); EXPECT_TRUE(mesh.GetMaterialSlotId() == expectedMesh.m_materialSlotId); } void ValidateLodAsset(const AZ::RPI::ModelLodAsset* lodAsset, const ExpectedLod& expectedLod) { ASSERT_NE(lodAsset, nullptr); EXPECT_TRUE(lodAsset->GetAabb().IsValid()); EXPECT_TRUE(lodAsset->GetMeshes().size() == expectedLod.m_meshes.size()); EXPECT_TRUE(lodAsset->GetAabb() == expectedLod.m_aabb); for (size_t i = 0; i < lodAsset->GetMeshes().size(); ++i) { const AZ::RPI::ModelLodAsset::Mesh& mesh = lodAsset->GetMeshes()[i]; const ExpectedMesh& expectedMesh = expectedLod.m_meshes[i]; ValidateMesh(mesh, expectedMesh); } } void ValidateModelAsset(const AZ::RPI::ModelAsset* modelAsset, const ExpectedModel& expectedModel) { ASSERT_NE(modelAsset, nullptr); EXPECT_TRUE(modelAsset->GetAabb().IsValid()); EXPECT_TRUE(modelAsset->GetLodAssets().size() == expectedModel.m_lods.size()); EXPECT_TRUE(modelAsset->GetAabb() == expectedModel.m_aabb); for (size_t i = 0; i < modelAsset->GetLodAssets().size(); ++i) { const AZ::RPI::ModelLodAsset* lodAsset = modelAsset->GetLodAssets()[i].Get(); const ExpectedLod& expectedLod = expectedModel.m_lods[i]; ValidateLodAsset(lodAsset, expectedLod); } } const uint32_t m_manyMesh = 100; // Not too much to hold up the tests but enough to stress them AZ::Data::Asset m_materialAsset; }; TEST_F(ModelTests, SerializeModelOneLodOneSeparateMesh) { using namespace AZ; ExpectedModel expectedModel; const uint32_t lodCount = 1; const uint32_t sharedMeshCount = 0; const uint32_t separateMeshCount = 1; Data::Asset modelAsset = BuildTestModel(lodCount, sharedMeshCount, separateMeshCount, expectedModel); ValidateModelAsset(modelAsset.Get(), expectedModel); SerializeTester tester(GetSerializeContext()); tester.SerializeOut(modelAsset.Get()); Data::Asset serializedModelAsset = tester.SerializeIn(Data::AssetId(Uuid::CreateRandom())); ValidateModelAsset(serializedModelAsset.Get(), expectedModel); } TEST_F(ModelTests, SerializeModelOneLodOneSharedMesh) { using namespace AZ; ExpectedModel expectedModel; const uint32_t lodCount = 1; const uint32_t sharedMeshCount = 1; const uint32_t separateMeshCount = 0; Data::Asset modelAsset = BuildTestModel(lodCount, sharedMeshCount, separateMeshCount, expectedModel); ValidateModelAsset(modelAsset.Get(), expectedModel); SerializeTester tester(GetSerializeContext()); tester.SerializeOut(modelAsset.Get()); Data::Asset serializedModelAsset = tester.SerializeIn(Data::AssetId(Uuid::CreateRandom())); ValidateModelAsset(serializedModelAsset.Get(), expectedModel); } TEST_F(ModelTests, SerializeModelMaxLodOneSeparateMesh) { using namespace AZ; ExpectedModel expectedModel; const uint32_t lodCount = RPI::ModelLodAsset::LodCountMax; const uint32_t sharedMeshCount = 0; const uint32_t separateMeshCount = 1; Data::Asset modelAsset = BuildTestModel(lodCount, sharedMeshCount, separateMeshCount, expectedModel); ValidateModelAsset(modelAsset.Get(), expectedModel); SerializeTester tester(GetSerializeContext()); tester.SerializeOut(modelAsset.Get()); Data::Asset serializedModelAsset = tester.SerializeIn(Data::AssetId(Uuid::CreateRandom())); ValidateModelAsset(serializedModelAsset.Get(), expectedModel); } TEST_F(ModelTests, SerializeModelMaxLodOneSharedMesh) { using namespace AZ; ExpectedModel expectedModel; const uint32_t lodCount = RPI::ModelLodAsset::LodCountMax; const uint32_t sharedMeshCount = 1; const uint32_t separateMeshCount = 0; Data::Asset modelAsset = BuildTestModel(lodCount, sharedMeshCount, separateMeshCount, expectedModel); ValidateModelAsset(modelAsset.Get(), expectedModel); SerializeTester tester(GetSerializeContext()); tester.SerializeOut(modelAsset.Get()); Data::Asset serializedModelAsset = tester.SerializeIn(Data::AssetId(Uuid::CreateRandom())); ValidateModelAsset(serializedModelAsset.Get(), expectedModel); } TEST_F(ModelTests, SerializeModelOneLodManySeparateMeshes) { using namespace AZ; ExpectedModel expectedModel; const uint32_t lodCount = 1; const uint32_t sharedMeshCount = 0; const uint32_t separateMeshCount = m_manyMesh; Data::Asset modelAsset = BuildTestModel(lodCount, sharedMeshCount, separateMeshCount, expectedModel); ValidateModelAsset(modelAsset.Get(), expectedModel); SerializeTester tester(GetSerializeContext()); tester.SerializeOut(modelAsset.Get()); Data::Asset serializedModelAsset = tester.SerializeIn(Data::AssetId(Uuid::CreateRandom())); ValidateModelAsset(serializedModelAsset.Get(), expectedModel); } TEST_F(ModelTests, SerializeModelOneLodManySharedMeshes) { using namespace AZ; ExpectedModel expectedModel; const uint32_t lodCount = 1; const uint32_t sharedMeshCount = m_manyMesh; const uint32_t separateMeshCount = 0; Data::Asset modelAsset = BuildTestModel(lodCount, sharedMeshCount, separateMeshCount, expectedModel); ValidateModelAsset(modelAsset.Get(), expectedModel); SerializeTester tester(GetSerializeContext()); tester.SerializeOut(modelAsset.Get()); Data::Asset serializedModelAsset = tester.SerializeIn(Data::AssetId(Uuid::CreateRandom())); ValidateModelAsset(serializedModelAsset.Get(), expectedModel); } TEST_F(ModelTests, SerializeModelMaxLodManySeparateMeshes) { using namespace AZ; ExpectedModel expectedModel; const uint32_t lodCount = RPI::ModelLodAsset::LodCountMax; const uint32_t sharedMeshCount = 0; const uint32_t separateMeshCount = m_manyMesh; Data::Asset modelAsset = BuildTestModel(lodCount, sharedMeshCount, separateMeshCount, expectedModel); ValidateModelAsset(modelAsset.Get(), expectedModel); SerializeTester tester(GetSerializeContext()); tester.SerializeOut(modelAsset.Get()); Data::Asset serializedModelAsset = tester.SerializeIn(Data::AssetId(Uuid::CreateRandom())); ValidateModelAsset(serializedModelAsset.Get(), expectedModel); } TEST_F(ModelTests, SerializeModelMaxLodManySharedMeshes) { using namespace AZ; ExpectedModel expectedModel; const uint32_t lodCount = RPI::ModelLodAsset::LodCountMax; const uint32_t sharedMeshCount = m_manyMesh; const uint32_t separateMeshCount = 0; Data::Asset modelAsset = BuildTestModel(lodCount, sharedMeshCount, separateMeshCount, expectedModel); ValidateModelAsset(modelAsset.Get(), expectedModel); SerializeTester tester(GetSerializeContext()); tester.SerializeOut(modelAsset.Get()); Data::Asset serializedModelAsset = tester.SerializeIn(Data::AssetId(Uuid::CreateRandom())); ValidateModelAsset(serializedModelAsset.Get(), expectedModel); } TEST_F(ModelTests, SerializeModelOneLodOneSharedMeshOneSeparateMesh) { using namespace AZ; ExpectedModel expectedModel; const uint32_t lodCount = 1; const uint32_t sharedMeshCount = 1; const uint32_t separateMeshCount = 1; Data::Asset modelAsset = BuildTestModel(lodCount, sharedMeshCount, separateMeshCount, expectedModel); ValidateModelAsset(modelAsset.Get(), expectedModel); SerializeTester tester(GetSerializeContext()); tester.SerializeOut(modelAsset.Get()); Data::Asset serializedModelAsset = tester.SerializeIn(Data::AssetId(Uuid::CreateRandom())); ValidateModelAsset(serializedModelAsset.Get(), expectedModel); } TEST_F(ModelTests, SerializeModelMaxLodOneSharedMeshOneSeparateMesh) { using namespace AZ; ExpectedModel expectedModel; const uint32_t lodCount = RPI::ModelLodAsset::LodCountMax; const uint32_t sharedMeshCount = 1; const uint32_t separateMeshCount = 1; Data::Asset modelAsset = BuildTestModel(lodCount, sharedMeshCount, separateMeshCount, expectedModel); ValidateModelAsset(modelAsset.Get(), expectedModel); SerializeTester tester(GetSerializeContext()); tester.SerializeOut(modelAsset.Get()); Data::Asset serializedModelAsset = tester.SerializeIn(Data::AssetId(Uuid::CreateRandom())); ValidateModelAsset(serializedModelAsset.Get(), expectedModel); } TEST_F(ModelTests, SerializeModelMaxLodManySharedMeshOneSeparateMesh) { using namespace AZ; ExpectedModel expectedModel; const uint32_t lodCount = RPI::ModelLodAsset::LodCountMax; const uint32_t sharedMeshCount = m_manyMesh; const uint32_t separateMeshCount = 1; Data::Asset modelAsset = BuildTestModel(lodCount, sharedMeshCount, separateMeshCount, expectedModel); ValidateModelAsset(modelAsset.Get(), expectedModel); SerializeTester tester(GetSerializeContext()); tester.SerializeOut(modelAsset.Get()); Data::Asset serializedModelAsset = tester.SerializeIn(Data::AssetId(Uuid::CreateRandom())); ValidateModelAsset(serializedModelAsset.Get(), expectedModel); } TEST_F(ModelTests, SerializeModelMaxLodOneSharedMeshManySeparateMesh) { using namespace AZ; ExpectedModel expectedModel; const uint32_t lodCount = RPI::ModelLodAsset::LodCountMax; const uint32_t sharedMeshCount = 1; const uint32_t separateMeshCount = m_manyMesh; Data::Asset modelAsset = BuildTestModel(lodCount, sharedMeshCount, separateMeshCount, expectedModel); ValidateModelAsset(modelAsset.Get(), expectedModel); SerializeTester tester(GetSerializeContext()); tester.SerializeOut(modelAsset.Get()); Data::Asset serializedModelAsset = tester.SerializeIn(Data::AssetId(Uuid::CreateRandom())); ValidateModelAsset(serializedModelAsset.Get(), expectedModel); } TEST_F(ModelTests, SerializeModelMaxLodManySharedMeshManySeparateMesh) { using namespace AZ; ExpectedModel expectedModel; const uint32_t lodCount = RPI::ModelLodAsset::LodCountMax; const uint32_t sharedMeshCount = m_manyMesh; const uint32_t separateMeshCount = m_manyMesh; Data::Asset modelAsset = BuildTestModel(lodCount, sharedMeshCount, separateMeshCount, expectedModel); ValidateModelAsset(modelAsset.Get(), expectedModel); SerializeTester tester(GetSerializeContext()); tester.SerializeOut(modelAsset.Get()); Data::Asset serializedModelAsset = tester.SerializeIn(Data::AssetId(Uuid::CreateRandom())); ValidateModelAsset(serializedModelAsset.Get(), expectedModel); } // Tests that if we try to set the name on a Model // before calling Begin that it will fail. TEST_F(ModelTests, SetNameNoBegin) { using namespace AZ; RPI::ModelAssetCreator creator; ErrorMessageFinder messageFinder("Begin() was not called"); creator.SetName("TestName"); } // Tests that if we try to add a ModelLod to a Model // before calling Begin that it will fail. TEST_F(ModelTests, AddLodNoBegin) { using namespace AZ; RPI::ModelAssetCreator creator; //Build a valid lod ExpectedLod expectedLod; Data::Asset lod = BuildTestLod(0, 1, expectedLod); ErrorMessageFinder messageFinder("Begin() was not called"); creator.AddLodAsset(AZStd::move(lod)); } // 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) { using namespace AZ; RPI::ModelAssetCreator creator; creator.Begin(Data::AssetId(AZ::Uuid::CreateRandom())); ErrorMessageFinder messageFinder("No valid ModelLodAssets have been added to this ModelAsset."); // Since there are no LODs set on this model it // we should not be able to successfully end the creator Data::Asset asset; ASSERT_FALSE(creator.End(asset)); ASSERT_FALSE(asset.IsReady()); ASSERT_EQ(asset.Get(), nullptr); } // Tests that if we call SetLodIndexBuffer without calling // Begin first on the ModelLodAssetCreator that it // fails as expected. TEST_F(ModelTests, SetLodIndexBufferNoBegin) { using namespace AZ; Data::Asset validIndexBuffer = BuildTestBuffer(10, sizeof(uint32_t)); ErrorMessageFinder messageFinder("Begin() was not called"); RPI::ModelLodAssetCreator creator; creator.SetLodIndexBuffer(validIndexBuffer); } // Tests that if we call AddLodStreamBuffer without calling // Begin first on the ModelLodAssetCreator that it // fails as expected. TEST_F(ModelTests, AddLodStreamBufferNoBegin) { using namespace AZ; Data::Asset validStreamBuffer = BuildTestBuffer(10, sizeof(float) * 3); ErrorMessageFinder messageFinder("Begin() was not called"); RPI::ModelLodAssetCreator creator; creator.AddLodStreamBuffer(validStreamBuffer); } // Tests that if we call BeginMesh without calling // Begin first on the ModelLodAssetCreator that it // fails as expected. TEST_F(ModelTests, BeginMeshNoBegin) { using namespace AZ; ErrorMessageFinder messageFinder("Begin() was not called"); RPI::ModelLodAssetCreator creator; creator.BeginMesh(); } // Tests that if we try to set an AABB on a mesh // 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); ASSERT_TRUE(aabb.IsValid()); { ErrorMessageFinder messageFinder("Begin() was not called"); AZ::Aabb testAabb = aabb; creator.SetMeshAabb(AZStd::move(testAabb)); } creator.Begin(Data::AssetId(AZ::Uuid::CreateRandom())); //This should still fail even if we call Begin but not BeginMesh { ErrorMessageFinder messageFinder("BeginMesh() was not called"); AZ::Aabb testAabb = aabb; creator.SetMeshAabb(AZStd::move(testAabb)); } } // Tests that if we try to set the material slot on a mesh // 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, SetMaterialSlotNoBeginNoBeginMesh) { using namespace AZ; RPI::ModelLodAssetCreator creator; { ErrorMessageFinder messageFinder("Begin() was not called"); creator.SetMeshMaterialSlot(0); } creator.Begin(Data::AssetId(AZ::Uuid::CreateRandom())); //This should still fail even if we call Begin but not BeginMesh { ErrorMessageFinder messageFinder("BeginMesh() was not called"); creator.SetMeshMaterialSlot(0); } } // Tests that if we try to set the index buffer on a mesh // 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) { using namespace AZ; RPI::ModelLodAssetCreator creator; const uint32_t indexCount = 36; const uint32_t indexSize = sizeof(uint32_t); RHI::BufferViewDescriptor validIndexBufferViewDescriptor = RHI::BufferViewDescriptor::CreateStructured(0, indexCount, indexSize); Data::Asset validIndexBuffer = BuildTestBuffer(indexCount, indexSize); ASSERT_TRUE(validIndexBuffer.Get() != nullptr); { ErrorMessageFinder messageFinder("Begin() was not called"); creator.SetMeshIndexBuffer({ AZStd::move(validIndexBuffer), validIndexBufferViewDescriptor }); } creator.Begin(Data::AssetId(AZ::Uuid::CreateRandom())); //This should still fail even if we call Begin but not BeginMesh validIndexBuffer = BuildTestBuffer(indexCount, indexSize); ASSERT_TRUE(validIndexBuffer.Get() != nullptr); { ErrorMessageFinder messageFinder("BeginMesh() was not called"); creator.SetMeshIndexBuffer({ AZStd::move(validIndexBuffer), validIndexBufferViewDescriptor }); } } // Tests that if we try to add a stream buffer on a mesh // 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) { using namespace AZ; RPI::ModelLodAssetCreator creator; const uint32_t vertexCount = 36; const uint32_t vertexSize = sizeof(float) * 3; const uint32_t vertexBufferSize = vertexCount * vertexSize; RHI::BufferViewDescriptor validStreamBufferViewDescriptor = RHI::BufferViewDescriptor::CreateStructured(0, vertexCount, vertexSize); Data::Asset validStreamBuffer = BuildTestBuffer(vertexCount, vertexSize); { ErrorMessageFinder messageFinder("Begin() was not called"); creator.AddMeshStreamBuffer(GetPositionSemantic(), AZ::Name(), { AZStd::move(validStreamBuffer), validStreamBufferViewDescriptor }); } creator.Begin(Data::AssetId(AZ::Uuid::CreateRandom())); //This should still fail even if we call Begin but not BeginMesh validStreamBuffer = BuildTestBuffer(vertexCount, vertexSize); { ErrorMessageFinder messageFinder("BeginMesh() was not called"); creator.AddMeshStreamBuffer(GetPositionSemantic(), AZ::Name(), { AZStd::move(validStreamBuffer), validStreamBufferViewDescriptor }); } } // 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) { using namespace AZ; RPI::ModelLodAssetCreator creator; creator.Begin(Data::AssetId(AZ::Uuid::CreateRandom())); ErrorMessageFinder messageFinder("No meshes have been provided for this LOD"); Data::Asset asset; ASSERT_FALSE(creator.End(asset)); ASSERT_FALSE(asset.IsReady()); ASSERT_EQ(asset.Get(), nullptr); } // Tests that validation still fails when expected // even after producing a valid mesh due to a missing // BeginMesh call TEST_F(ModelTests, SecondMeshFailureNoBeginMesh) { using namespace AZ; RPI::ModelLodAssetCreator creator; creator.Begin(Data::AssetId(AZ::Uuid::CreateRandom())); uint32_t indexCount = 36; uint32_t vertexCount = 36; RHI::BufferViewDescriptor indexBufferViewDescriptor = RHI::BufferViewDescriptor::CreateStructured(0, indexCount, sizeof(uint32_t)); RHI::BufferViewDescriptor vertexBufferViewDescriptor = RHI::BufferViewDescriptor::CreateStructured(0, vertexCount, sizeof(float) * 3); //Creating this first mesh should work as expected { AZ::Aabb aabb = AZ::Aabb::CreateCenterRadius(Vector3::CreateZero(), 1.0f); creator.BeginMesh(); creator.SetMeshAabb(AZStd::move(aabb)); creator.SetMeshMaterialSlot(0); creator.SetMeshIndexBuffer({ BuildTestBuffer(indexCount, sizeof(uint32_t)), indexBufferViewDescriptor }); creator.AddMeshStreamBuffer(GetPositionSemantic(), AZ::Name(), { BuildTestBuffer(vertexCount, sizeof(float) * 3), vertexBufferViewDescriptor }); creator.EndMesh(); } // This second mesh should fail at every point since we have forgotten to // call BeginMesh again { AZ::Aabb aabb = AZ::Aabb::CreateCenterRadius(Vector3::CreateZero(), 1.0f); ErrorMessageFinder messageFinder("BeginMesh() was not called", 5); creator.SetMeshAabb(AZStd::move(aabb)); creator.SetMeshMaterialSlot(0); creator.SetMeshIndexBuffer({ BuildTestBuffer(indexCount, sizeof(uint32_t)), indexBufferViewDescriptor }); creator.AddMeshStreamBuffer(GetPositionSemantic(), AZ::Name(), { BuildTestBuffer(vertexCount, sizeof(float) * 3), vertexBufferViewDescriptor }); creator.EndMesh(); } // We should still be able to produce a valid asset however Data::Asset asset; EXPECT_TRUE(creator.End(asset)); EXPECT_TRUE(asset.IsReady()); EXPECT_NE(asset.Get(), nullptr); // Make sure that this lod only has one mesh like we expect ASSERT_EQ(asset->GetMeshes().size(), 1); } // Tests that validation still fails when expected // even after producing a valid mesh due to SetMeshX // calls coming after End TEST_F(ModelTests, SecondMeshAfterEnd) { using namespace AZ; RPI::ModelLodAssetCreator creator; creator.Begin(Data::AssetId(AZ::Uuid::CreateRandom())); uint32_t indexCount = 36; uint32_t vertexCount = 36; RHI::BufferViewDescriptor indexBufferViewDescriptor = RHI::BufferViewDescriptor::CreateStructured(0, indexCount, sizeof(uint32_t)); RHI::BufferViewDescriptor vertexBufferViewDescriptor = RHI::BufferViewDescriptor::CreateStructured(0, vertexCount, sizeof(float) * 3); //Creating this first mesh should work as expected { AZ::Aabb aabb = AZ::Aabb::CreateCenterRadius(Vector3::CreateZero(), 1.0f); creator.BeginMesh(); creator.SetMeshAabb(AZStd::move(aabb)); creator.SetMeshMaterialSlot(0); creator.SetMeshIndexBuffer({ BuildTestBuffer(indexCount, sizeof(uint32_t)), indexBufferViewDescriptor }); creator.AddMeshStreamBuffer(GetPositionSemantic(), AZ::Name(), { BuildTestBuffer(vertexCount, sizeof(float) * 3), vertexBufferViewDescriptor }); creator.EndMesh(); } // This asset creation should be valid Data::Asset asset; EXPECT_TRUE(creator.End(asset)); EXPECT_TRUE(asset.IsReady()); EXPECT_NE(asset.Get(), nullptr); // This second mesh should fail at every point since we have already // called End { 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.SetMeshMaterialSlot(0); creator.SetMeshIndexBuffer({ BuildTestBuffer(indexCount, sizeof(uint32_t)), indexBufferViewDescriptor }); creator.AddMeshStreamBuffer(GetPositionSemantic(), AZ::Name(), { BuildTestBuffer(vertexCount, sizeof(float) * 3), vertexBufferViewDescriptor }); creator.EndMesh(); } } TEST_F(ModelTests, UvStream) { AZ::RPI::UvStreamTangentBitmask uvStreamTangentBitmask; EXPECT_EQ(uvStreamTangentBitmask.GetFullTangentBitmask(), 0u); uvStreamTangentBitmask.ApplyTangent(1u); EXPECT_EQ(uvStreamTangentBitmask.GetTangentAtUv(0u), 1u); EXPECT_EQ(uvStreamTangentBitmask.GetFullTangentBitmask(), 0x10000001); EXPECT_EQ(uvStreamTangentBitmask.GetUvStreamCount(), 1u); uvStreamTangentBitmask.ApplyTangent(5u); EXPECT_EQ(uvStreamTangentBitmask.GetTangentAtUv(0u), 1u); EXPECT_EQ(uvStreamTangentBitmask.GetTangentAtUv(1u), 5u); EXPECT_EQ(uvStreamTangentBitmask.GetFullTangentBitmask(), 0x20000051); EXPECT_EQ(uvStreamTangentBitmask.GetUvStreamCount(), 2u); uvStreamTangentBitmask.ApplyTangent(100u); EXPECT_EQ(uvStreamTangentBitmask.GetTangentAtUv(0u), 1u); EXPECT_EQ(uvStreamTangentBitmask.GetTangentAtUv(1u), 5u); EXPECT_EQ(uvStreamTangentBitmask.GetTangentAtUv(2u), AZ::RPI::UvStreamTangentBitmask::UnassignedTangent); EXPECT_EQ(uvStreamTangentBitmask.GetFullTangentBitmask(), 0x30000F51); EXPECT_EQ(uvStreamTangentBitmask.GetUvStreamCount(), 3u); for (uint32_t i = 3; i < AZ::RPI::UvStreamTangentBitmask::MaxUvSlots; ++i) { uvStreamTangentBitmask.ApplyTangent(0u); } EXPECT_EQ(uvStreamTangentBitmask.GetFullTangentBitmask(), 0x70000F51); AZ_TEST_START_TRACE_SUPPRESSION; uvStreamTangentBitmask.ApplyTangent(0u); AZ_TEST_STOP_TRACE_SUPPRESSION(1); 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. // // -0.33 // -1 0.33 1 // 0.5 *---*---*---* // \ / \ / \ / \ // *---*---*---* // \ / \ / \ / \ // -0.5 *- *---*---*---* // \ \ / \ / \ / \ // *- *---*---*---* // \ \ \ \ // *---*---*---* // \ / \ / \ / \ // *---*---*---* 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 TestMesh { public: 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())); lodCreator.BeginMesh(); 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)) }); } { 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(); AZ::Data::Asset lodAsset; lodCreator.End(lodAsset); AZ::RPI::ModelAssetCreator modelCreator; modelCreator.Begin(AZ::Data::AssetId(AZ::Uuid::CreateRandom())); modelCreator.SetName("TestModel"); modelCreator.AddLodAsset(AZStd::move(lodAsset)); modelCreator.End(m_modelAsset); } [[nodiscard]] AZ::Data::Asset GetModel() const { return m_modelAsset; } private: AZ::Data::Asset m_modelAsset; }; 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 IntersectParams& param) { return os << "xpos:" << param.xpos << ", ypos:" << param.ypos << ", zpos:" << param.zpos << ", dist:" << param.expectedDistance << ", shouldIntersect:" << param.expectedShouldIntersect; } }; class KdTreeIntersectsParameterizedFixture : public ModelTests , public ::testing::WithParamInterface { }; TEST_P(KdTreeIntersectsParameterizedFixture, KdTreeIntersects) { TestMesh mesh( TwoSeparatedPlanesPositions.data(), TwoSeparatedPlanesPositions.size(), TwoSeparatedPlanesIndices.data(), TwoSeparatedPlanesIndices.size()); AZ::RPI::ModelKdTree kdTree; ASSERT_TRUE(kdTree.Build(mesh.GetModel().Get())); float distance = AZStd::numeric_limits::max(); AZ::Vector3 normal; EXPECT_THAT( kdTree.RayIntersection( 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 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 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(KdTreeIntersectTestData)); class KdTreeIntersectsFixture : public ModelTests { public: void SetUp() override { ModelTests::SetUp(); 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())); } void TearDown() override { m_kdTree.reset(); m_mesh.reset(); ModelTests::TearDown(); } AZStd::unique_ptr m_mesh; AZStd::unique_ptr m_kdTree; }; TEST_F(KdTreeIntersectsFixture, KdTreeIntersectionReturnsNormalizedDistance) { float t = AZStd::numeric_limits::max(); AZ::Vector3 normal; constexpr float rayLength = 100.0f; EXPECT_THAT( m_kdTree->RayIntersection( AZ::Vector3::CreateZero(), AZ::Vector3::CreateAxisZ(-rayLength), t, normal), testing::Eq(true)); EXPECT_THAT(t, testing::FloatEq(0.005f)); } TEST_F(KdTreeIntersectsFixture, KdTreeIntersectionHandlesInvalidStartingNormalizedDistance) { float t = -0.5f; // invalid starting distance AZ::Vector3 normal; constexpr float rayLength = 10.0f; EXPECT_THAT( m_kdTree->RayIntersection(AZ::Vector3::CreateAxisZ(0.75f), AZ::Vector3::CreateAxisZ(-rayLength), t, normal), testing::Eq(true)); EXPECT_THAT(t, testing::FloatEq(0.025f)); } TEST_F(KdTreeIntersectsFixture, KdTreeIntersectionDoesNotScaleRayByStartingDistance) { float t = 10.0f; // starting distance (used to check it is not read from initially by RayIntersection) AZ::Vector3 normal; 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; constexpr bool AllowBruteForce = false; EXPECT_THAT( mesh.GetModel()->LocalRayIntersectionAgainstModel( AZ::Vector3(GetParam().xpos, GetParam().ypos, GetParam().zpos), AZ::Vector3(GetParam().xdir, GetParam().ydir, GetParam().zdir), AllowBruteForce, 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) constexpr bool AllowBruteForce = false; EXPECT_THAT( m_mesh->GetModel()->LocalRayIntersectionAgainstModel( AZ::Vector3::CreateAxisZ(5.0f), -AZ::Vector3::CreateAxisZ(10.0f), AllowBruteForce, 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 constexpr bool AllowBruteForce = false; EXPECT_THAT( m_mesh->GetModel()->LocalRayIntersectionAgainstModel( AZ::Vector3::CreateAxisY(10.0f), -AZ::Vector3::CreateAxisY(9.0f), AllowBruteForce, t, normal), testing::Eq(true)); EXPECT_THAT(t, testing::FloatEq(1.0f)); EXPECT_THAT(normal, IsClose(AZ::Vector3::CreateAxisY())); } } // namespace UnitTest