From e4e1946018b55492d040ba023356ee2e1410f952 Mon Sep 17 00:00:00 2001 From: Chris Burel Date: Thu, 24 Jun 2021 10:32:04 -0700 Subject: [PATCH 1/8] Determine the original vertex index based on the position The Assimp library does not expose the FBX control point indices. This change causes vertices that are close enough in their position to be considered as coming from the same control point. This allows the mesh optimizer to consider vertices with the same control point index (or "original vertex index" as it is called in the code) for deduplication. Signed-off-by: Chris Burel --- .../MeshOptimizer/MeshOptimizerComponent.cpp | 24 ++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/Gems/SceneProcessing/Code/Source/Generation/Components/MeshOptimizer/MeshOptimizerComponent.cpp b/Gems/SceneProcessing/Code/Source/Generation/Components/MeshOptimizer/MeshOptimizerComponent.cpp index 58be460a44..4321cb0108 100644 --- a/Gems/SceneProcessing/Code/Source/Generation/Components/MeshOptimizer/MeshOptimizerComponent.cpp +++ b/Gems/SceneProcessing/Code/Source/Generation/Components/MeshOptimizer/MeshOptimizerComponent.cpp @@ -107,7 +107,7 @@ namespace AZ::SceneGenerationComponents auto* serializeContext = azrtti_cast(context); if (serializeContext) { - serializeContext->Class()->Version(2); + serializeContext->Class()->Version(3); } } @@ -434,6 +434,12 @@ namespace AZ::SceneGenerationComponents const float weightThreshold = skinRule ? skinRule->GetWeightThreshold() : 0.001f; meshBuilder.SetSkinningInfo(ExtractSkinningInfo(meshData, skinWeights, maxWeightsPerVertex, weightThreshold)); + constexpr float positionTolerance = 0.0001f; + constexpr float positionToleranceReciprocal = 1.0f / positionTolerance; + AZStd::unordered_map positionMap{}; + + AZ::u32 currentOriginalVertexIndex = 0; + // Add the vertex data to all the layers const AZ::u32 faceCount = meshData->GetFaceCount(); for (AZ::u32 faceIndex = 0; faceIndex < faceCount; ++faceIndex) @@ -441,8 +447,20 @@ namespace AZ::SceneGenerationComponents meshBuilder.BeginPolygon(baseMesh->GetFaceMaterialId(faceIndex)); for (const AZ::u32 vertexIndex : meshData->GetFaceInfo(faceIndex).vertexIndex) { - const int orgVertexNumber = meshData->GetUsedPointIndexForControlPoint(meshData->GetControlPointIndex(vertexIndex)); - AZ_Assert(orgVertexNumber >= 0, "Invalid vertex number"); + // Round the vertex position so that a float comparison can be made with entires in the positionMap + AZ::Vector3 position = meshData->GetPosition(vertexIndex); + position *= positionToleranceReciprocal; + position += AZ::Vector3(0.5f); + position = AZ::Vector3(AZ::Simd::Vec3::Floor(position.GetSimdValue())); + position *= positionTolerance; + + const auto& [iter, didInsert] = positionMap.try_emplace(position, currentOriginalVertexIndex); + if (didInsert) + { + ++currentOriginalVertexIndex; + } + const AZ::u32 orgVertexNumber = iter->second; + orgVtxLayer->SetCurrentVertexValue(orgVertexNumber); posLayer->SetCurrentVertexValue(meshData->GetPosition(vertexIndex)); From cbb4778ef5d7154fbe890e46a4315afcfe1ea67a Mon Sep 17 00:00:00 2001 From: Chris Burel Date: Mon, 21 Jun 2021 16:04:08 -0700 Subject: [PATCH 2/8] Use a filter view instead of reimplementing a filter view Signed-off-by: Chris Burel --- .../MeshOptimizer/MeshOptimizerComponent.cpp | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/Gems/SceneProcessing/Code/Source/Generation/Components/MeshOptimizer/MeshOptimizerComponent.cpp b/Gems/SceneProcessing/Code/Source/Generation/Components/MeshOptimizer/MeshOptimizerComponent.cpp index 4321cb0108..e58b916995 100644 --- a/Gems/SceneProcessing/Code/Source/Generation/Components/MeshOptimizer/MeshOptimizerComponent.cpp +++ b/Gems/SceneProcessing/Code/Source/Generation/Components/MeshOptimizer/MeshOptimizerComponent.cpp @@ -193,17 +193,12 @@ namespace AZ::SceneGenerationComponents const AZStd::vector> meshes = [](const SceneGraph& graph) { AZStd::vector> meshes; - for (auto it = graph.GetContentStorage().cbegin(); it != graph.GetContentStorage().cend(); ++it) + const auto meshNodes = Containers::MakeDerivedFilterView(graph.GetContentStorage()); + for (auto it = meshNodes.cbegin(); it != meshNodes.cend(); ++it) { - // Skip anything that isn't a mesh. - const auto* mesh = azdynamic_cast(it->get()); - if (!mesh) - { - continue; - } - // Get the mesh data and node index and store them in the vector as a pair, so we can iterate over them later. - meshes.emplace_back(mesh, graph.ConvertToNodeIndex(it)); + // The sequential calls to GetBaseIterator unwrap the layers of FilterIterators from the MakeDerivedFilterView + meshes.emplace_back(&(*it), graph.ConvertToNodeIndex(it.GetBaseIterator().GetBaseIterator().GetBaseIterator())); } return meshes; }(graph); From 38046bd0c9da25a9acb1491b2815107875e034dd Mon Sep 17 00:00:00 2001 From: Chris Burel Date: Thu, 24 Jun 2021 11:23:51 -0700 Subject: [PATCH 3/8] Don't attempt to weld similar vertices if there's blendshapes Signed-off-by: Chris Burel --- .../MeshOptimizer/MeshOptimizerComponent.cpp | 36 ++++++++++++------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/Gems/SceneProcessing/Code/Source/Generation/Components/MeshOptimizer/MeshOptimizerComponent.cpp b/Gems/SceneProcessing/Code/Source/Generation/Components/MeshOptimizer/MeshOptimizerComponent.cpp index e58b916995..888ba5124b 100644 --- a/Gems/SceneProcessing/Code/Source/Generation/Components/MeshOptimizer/MeshOptimizerComponent.cpp +++ b/Gems/SceneProcessing/Code/Source/Generation/Components/MeshOptimizer/MeshOptimizerComponent.cpp @@ -442,19 +442,31 @@ namespace AZ::SceneGenerationComponents meshBuilder.BeginPolygon(baseMesh->GetFaceMaterialId(faceIndex)); for (const AZ::u32 vertexIndex : meshData->GetFaceInfo(faceIndex).vertexIndex) { - // Round the vertex position so that a float comparison can be made with entires in the positionMap - AZ::Vector3 position = meshData->GetPosition(vertexIndex); - position *= positionToleranceReciprocal; - position += AZ::Vector3(0.5f); - position = AZ::Vector3(AZ::Simd::Vec3::Floor(position.GetSimdValue())); - position *= positionTolerance; - - const auto& [iter, didInsert] = positionMap.try_emplace(position, currentOriginalVertexIndex); - if (didInsert) + const AZ::u32 orgVertexNumber = [&meshData, &hasBlendShapes, &vertexIndex, &positionMap, ¤tOriginalVertexIndex, positionTolerance, positionToleranceReciprocal]() -> AZ::u32 { - ++currentOriginalVertexIndex; - } - const AZ::u32 orgVertexNumber = iter->second; + if (hasBlendShapes) + { + // Don't attempt to weld similar vertices if there's blendshapes + // Welding the vertices here based on position could cause the vertices of a base shape to be + // welded, and the vertices of the blendshape to not be welded, resulting in a vertex count + // mismatch between the two + return meshData->GetUsedPointIndexForControlPoint(meshData->GetControlPointIndex(vertexIndex)); + } + + // Round the vertex position so that a float comparison can be made with entires in the positionMap + AZ::Vector3 position = meshData->GetPosition(vertexIndex); + position *= positionToleranceReciprocal; + position += AZ::Vector3(0.5f); + position = AZ::Vector3(AZ::Simd::Vec3::Floor(position.GetSimdValue())); + position *= positionTolerance; + + const auto& [iter, didInsert] = positionMap.try_emplace(position, currentOriginalVertexIndex); + if (didInsert) + { + ++currentOriginalVertexIndex; + } + return iter->second; + }(); orgVtxLayer->SetCurrentVertexValue(orgVertexNumber); From 8702f0cd2ed86bdeaa7475d77f0783a7276c1d5a Mon Sep 17 00:00:00 2001 From: Chris Burel Date: Fri, 25 Jun 2021 09:35:34 -0700 Subject: [PATCH 4/8] Add test for the mesh optimizer's ability to weld nearby vertices Signed-off-by: Chris Burel --- .../SceneAPI/SceneData/Groups/MeshGroup.h | 24 ++--- Gems/SceneProcessing/Code/CMakeLists.txt | 1 + .../MeshOptimizerComponentTests.cpp | 100 ++++++++++++++++++ .../sceneprocessing_editor_tests_files.cmake | 1 + 4 files changed, 114 insertions(+), 12 deletions(-) create mode 100644 Gems/SceneProcessing/Code/Tests/MeshBuilder/MeshOptimizerComponentTests.cpp diff --git a/Code/Tools/SceneAPI/SceneData/Groups/MeshGroup.h b/Code/Tools/SceneAPI/SceneData/Groups/MeshGroup.h index 2bc62b8045..32418d88df 100644 --- a/Code/Tools/SceneAPI/SceneData/Groups/MeshGroup.h +++ b/Code/Tools/SceneAPI/SceneData/Groups/MeshGroup.h @@ -29,27 +29,27 @@ namespace AZ } namespace SceneData { - class MeshGroup + class SCENE_DATA_CLASS MeshGroup : public DataTypes::IMeshGroup { public: AZ_RTTI(MeshGroup, "{07B356B7-3635-40B5-878A-FAC4EFD5AD86}", DataTypes::IMeshGroup); AZ_CLASS_ALLOCATOR(MeshGroup, SystemAllocator, 0) - MeshGroup(); - ~MeshGroup() override = default; + SCENE_DATA_API MeshGroup(); + SCENE_DATA_API ~MeshGroup() override = default; - const AZStd::string& GetName() const override; - void SetName(const AZStd::string& name); - void SetName(AZStd::string&& name) override; - const Uuid& GetId() const override; - void OverrideId(const Uuid& id) override; + SCENE_DATA_API const AZStd::string& GetName() const override; + SCENE_DATA_API void SetName(const AZStd::string& name); + SCENE_DATA_API void SetName(AZStd::string&& name) override; + SCENE_DATA_API const Uuid& GetId() const override; + SCENE_DATA_API void OverrideId(const Uuid& id) override; - Containers::RuleContainer& GetRuleContainer() override; - const Containers::RuleContainer& GetRuleContainerConst() const override; + SCENE_DATA_API Containers::RuleContainer& GetRuleContainer() override; + SCENE_DATA_API const Containers::RuleContainer& GetRuleContainerConst() const override; - DataTypes::ISceneNodeSelectionList& GetSceneNodeSelectionList() override; - const DataTypes::ISceneNodeSelectionList& GetSceneNodeSelectionList() const override; + SCENE_DATA_API DataTypes::ISceneNodeSelectionList& GetSceneNodeSelectionList() override; + SCENE_DATA_API const DataTypes::ISceneNodeSelectionList& GetSceneNodeSelectionList() const override; static void Reflect(AZ::ReflectContext* context); static bool VersionConverter(SerializeContext& context, SerializeContext::DataElementNode& classElement); diff --git a/Gems/SceneProcessing/Code/CMakeLists.txt b/Gems/SceneProcessing/Code/CMakeLists.txt index 319c7dc170..6602c1d7d6 100644 --- a/Gems/SceneProcessing/Code/CMakeLists.txt +++ b/Gems/SceneProcessing/Code/CMakeLists.txt @@ -113,6 +113,7 @@ if(PAL_TRAIT_BUILD_TESTS_SUPPORTED) PRIVATE Gem::SceneProcessing.Editor.Static AZ::AzTest + AZ::SceneData ) ly_add_googletest( NAME Gem::SceneProcessing.Editor.Tests diff --git a/Gems/SceneProcessing/Code/Tests/MeshBuilder/MeshOptimizerComponentTests.cpp b/Gems/SceneProcessing/Code/Tests/MeshBuilder/MeshOptimizerComponentTests.cpp new file mode 100644 index 0000000000..f374cc833c --- /dev/null +++ b/Gems/SceneProcessing/Code/Tests/MeshBuilder/MeshOptimizerComponentTests.cpp @@ -0,0 +1,100 @@ +/* + * 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 SceneProcessing +{ + using VertexDeduplicationFixture = SceneProcessing::InitSceneAPIFixture; + + TEST_F(VertexDeduplicationFixture, CanDeduplicateVertices) + { + AZ::ComponentApplication app; + AZ::Entity* systemEntity = app.Create({}, {}); + systemEntity->AddComponent(aznew AZ::MemoryComponent()); + systemEntity->AddComponent(aznew AZ::JobManagerComponent()); + systemEntity->Init(); + systemEntity->Activate(); + + AZ::SceneAPI::Containers::Scene scene("testScene"); + AZ::SceneAPI::Containers::SceneGraph& graph = scene.GetGraph(); + + // Create a simple plane with 2 triangles, 6 total vertices, 2 shared vertices + // 0 --- 1 + // | / | + // | / | + // | / | + // 2 --- 3 + const AZStd::array planeVertexPositions = { + AZ::Vector3{0.0f, 0.0f, 0.0f}, + AZ::Vector3{0.0f, 0.0f, 1.0f}, + AZ::Vector3{1.0f, 0.0f, 1.0f}, + AZ::Vector3{1.0f, 0.0f, 1.0f}, + AZ::Vector3{1.0f, 0.0f, 0.0f}, + AZ::Vector3{0.0f, 0.0f, 0.0f}, + }; + { + auto mesh = AZStd::make_unique(); + { + int i = 0; + for (const AZ::Vector3& position : planeVertexPositions) + { + mesh->AddPosition(position); + mesh->AddNormal(AZ::Vector3::CreateAxisY()); + + // This assumes that the data coming from the import process gives a unique control point + // index to every vertex. This follows the behavior of the AssImp library. + mesh->SetVertexIndexToControlPointIndexMap(i, i); + ++i; + } + } + mesh->AddFace({0, 1, 2}, 0); + mesh->AddFace({3, 4, 5}, 0); + + // The original source mesh should have 6 vertices + EXPECT_EQ(mesh->GetVertexCount(), planeVertexPositions.size()); + + graph.AddChild(graph.GetRoot(), "testMesh", AZStd::move(mesh)); + } + + auto meshGroup = AZStd::make_unique(); + meshGroup->GetSceneNodeSelectionList().AddSelectedNode("testMesh"); + scene.GetManifest().AddEntry(AZStd::move(meshGroup)); + + AZ::SceneGenerationComponents::MeshOptimizerComponent component; + AZ::SceneAPI::Events::GenerateSimplificationEventContext context(scene, "pc"); + component.OptimizeMeshes(context); + + AZ::SceneAPI::Containers::SceneGraph::NodeIndex optimizedNodeIndex = graph.Find(AZStd::string("testMesh").append(AZ::SceneAPI::Utilities::OptimizedMeshSuffix)); + ASSERT_TRUE(optimizedNodeIndex.IsValid()) << "Mesh optimizer did not add an optimized version of the mesh"; + + const auto& optimizedMesh = AZStd::rtti_pointer_cast(graph.GetNodeContent(optimizedNodeIndex)); + ASSERT_TRUE(optimizedMesh); + + // The optimized mesh should have 4 vertices, the 2 shared vertices are welded together + EXPECT_EQ(optimizedMesh->GetVertexCount(), 4); + + systemEntity->Deactivate(); + } +} // namespace SceneProcessing diff --git a/Gems/SceneProcessing/Code/sceneprocessing_editor_tests_files.cmake b/Gems/SceneProcessing/Code/sceneprocessing_editor_tests_files.cmake index 898bf06233..d3ebf1c21f 100644 --- a/Gems/SceneProcessing/Code/sceneprocessing_editor_tests_files.cmake +++ b/Gems/SceneProcessing/Code/sceneprocessing_editor_tests_files.cmake @@ -8,6 +8,7 @@ set(FILES Tests/InitSceneAPIFixture.h + Tests/MeshBuilder/MeshOptimizerComponentTests.cpp Tests/MeshBuilder/MeshBuilderTests.cpp Tests/MeshBuilder/MeshVerticesTests.cpp Tests/MeshBuilder/SkinInfluencesTests.cpp From 70a8214c2dba9ee65d29e8f91810b1fbc2bc7903 Mon Sep 17 00:00:00 2001 From: Chris Burel Date: Fri, 25 Jun 2021 10:42:52 -0700 Subject: [PATCH 5/8] Add logging call to show mesh optimizer effect on vertex count Signed-off-by: Chris Burel --- .../Components/MeshOptimizer/MeshOptimizerComponent.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Gems/SceneProcessing/Code/Source/Generation/Components/MeshOptimizer/MeshOptimizerComponent.cpp b/Gems/SceneProcessing/Code/Source/Generation/Components/MeshOptimizer/MeshOptimizerComponent.cpp index 888ba5124b..2e509a8708 100644 --- a/Gems/SceneProcessing/Code/Source/Generation/Components/MeshOptimizer/MeshOptimizerComponent.cpp +++ b/Gems/SceneProcessing/Code/Source/Generation/Components/MeshOptimizer/MeshOptimizerComponent.cpp @@ -283,6 +283,12 @@ namespace AZ::SceneGenerationComponents auto [optimizedMesh, optimizedUVs, optimizedTangents, optimizedBitangents, optimizedVertexColors, optimizedSkinWeights] = OptimizeMesh(mesh, mesh, uvDatas, tangentDatas, bitangentDatas, colorDatas, skinWeightDatas, meshGroup, hasBlendShapes); + AZ_TracePrintf(AZ::SceneAPI::Utilities::LogWindow, "Base mesh: %zu vertices, optimized mesh: %zu vertices, %0.02f%% of the original", + mesh->GetUsedControlPointCount(), + optimizedMesh->GetUsedControlPointCount(), + ((float)optimizedMesh->GetUsedControlPointCount() / (float)mesh->GetUsedControlPointCount()) * 100.0f + ); + const NodeIndex optimizedMeshNodeIndex = graph.AddChild(graph.GetNodeParent(nodeIndex), name.c_str(), AZStd::move(optimizedMesh)); auto addOptimizedNodes = [&graph, &optimizedMeshNodeIndex](const auto& originalNodeIndexes, auto& optimizedNodes) From 7bcd4baec4bb353281971b39081bb0a1bbfb0abb Mon Sep 17 00:00:00 2001 From: Chris Burel Date: Fri, 25 Jun 2021 11:13:25 -0700 Subject: [PATCH 6/8] Use a bunch of temporaries in order to make `position` `const` Signed-off-by: Chris Burel --- .../MeshOptimizer/MeshOptimizerComponent.cpp | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/Gems/SceneProcessing/Code/Source/Generation/Components/MeshOptimizer/MeshOptimizerComponent.cpp b/Gems/SceneProcessing/Code/Source/Generation/Components/MeshOptimizer/MeshOptimizerComponent.cpp index 2e509a8708..f252989d2e 100644 --- a/Gems/SceneProcessing/Code/Source/Generation/Components/MeshOptimizer/MeshOptimizerComponent.cpp +++ b/Gems/SceneProcessing/Code/Source/Generation/Components/MeshOptimizer/MeshOptimizerComponent.cpp @@ -460,11 +460,12 @@ namespace AZ::SceneGenerationComponents } // Round the vertex position so that a float comparison can be made with entires in the positionMap - AZ::Vector3 position = meshData->GetPosition(vertexIndex); - position *= positionToleranceReciprocal; - position += AZ::Vector3(0.5f); - position = AZ::Vector3(AZ::Simd::Vec3::Floor(position.GetSimdValue())); - position *= positionTolerance; + // pos = floor( x * 10 + 0.5) * 0.1 + const AZ::Vector3 position = AZ::Vector3( + AZ::Simd::Vec3::Floor( + (meshData->GetPosition(vertexIndex) * positionToleranceReciprocal + AZ::Vector3(0.5f)).GetSimdValue() + ) + ) * positionTolerance; const auto& [iter, didInsert] = positionMap.try_emplace(position, currentOriginalVertexIndex); if (didInsert) From 9ea46bad83dcc903b811d861451c52c4935dc237 Mon Sep 17 00:00:00 2001 From: Chris Burel Date: Thu, 8 Jul 2021 16:16:47 -0700 Subject: [PATCH 7/8] Supply the vertex index remapping to the optimized skin weights This ensures that the optimized skin weights use the vertex indexes from the optimized mesh Signed-off-by: Chris Burel --- .../MeshOptimizer/MeshOptimizerComponent.cpp | 135 +++++++---- .../MeshOptimizerComponentTests.cpp | 209 ++++++++++++++---- 2 files changed, 258 insertions(+), 86 deletions(-) diff --git a/Gems/SceneProcessing/Code/Source/Generation/Components/MeshOptimizer/MeshOptimizerComponent.cpp b/Gems/SceneProcessing/Code/Source/Generation/Components/MeshOptimizer/MeshOptimizerComponent.cpp index f252989d2e..084715b83a 100644 --- a/Gems/SceneProcessing/Code/Source/Generation/Components/MeshOptimizer/MeshOptimizerComponent.cpp +++ b/Gems/SceneProcessing/Code/Source/Generation/Components/MeshOptimizer/MeshOptimizerComponent.cpp @@ -97,6 +97,85 @@ namespace AZ::SceneGenerationComponents namespace Containers = AZ::SceneAPI::Containers; namespace Views = Containers::Views; + // @brief A class to map from a mesh's vertex index to it's welded vertex index + // + // When the mesh optimizer runs, it welds nearby vertices (if there are no blendshapes). This class provides a + // constant time lookup to map from an unwelded vertex index to the welded one. + // The welding works by rounding the vertex's position to the given position tolerance, then uses that rounded + // Vector3 as a key into a unordered_map. + template + class Vector3Map + : private AZStd::unordered_map + { + public: + Vector3Map(const MeshDataType* meshData, bool hasBlendShapes, float positionTolerance) + : m_meshData(meshData) + , m_hasBlendShapes(hasBlendShapes) + , m_positionTolerance(positionTolerance) + , m_positionToleranceReciprocal(1.0f / positionTolerance) + { + } + + using AZStd::unordered_map::reserve; + + AZ::u32 operator[](const AZ::u32 vertexIndex) + { + if (m_hasBlendShapes) + { + // Don't attempt to weld similar vertices if there's blendshapes + // Welding the vertices here based on position could cause the vertices of a base shape to be welded, + // and the vertices of the blendshape to not be welded, resulting in a vertex count mismatch between + // the two + return m_meshData->GetUsedPointIndexForControlPoint(m_meshData->GetControlPointIndex(vertexIndex)); + } + + const auto& [iter, didInsert] = try_emplace(GetPositionForIndex(vertexIndex), m_currentOriginalVertexIndex); + if (didInsert) + { + ++m_currentOriginalVertexIndex; + } + return iter->second; + } + + [[nodiscard]] AZ::u32 at(const AZ::u32 vertexIndex) const + { + if (m_hasBlendShapes) + { + // Don't attempt to weld similar vertices if there's blendshapes + // Welding the vertices here based on position could cause the vertices of a base shape to be welded, + // and the vertices of the blendshape to not be welded, resulting in a vertex count mismatch between + // the two + return m_meshData->GetUsedPointIndexForControlPoint(m_meshData->GetControlPointIndex(vertexIndex)); + } + + auto iter = find(GetPositionForIndex(vertexIndex)); + AZSTD_CONTAINER_ASSERT(iter != end(), "Element with key is not present"); + return iter->second; + } + + private: + + AZ::Vector3 GetPositionForIndex(const AZ::u32 vertexIndex) const + { + // Round the vertex position so that a float comparison can be made with entires in the map + // pos = floor( x * 10 + 0.5) * 0.1 + return AZ::Vector3( + AZ::Simd::Vec3::Floor( + (m_meshData->GetPosition(vertexIndex) * m_positionToleranceReciprocal + AZ::Vector3(0.5f)).GetSimdValue() + ) + ) * m_positionTolerance; + } + + const MeshDataType* m_meshData; + bool m_hasBlendShapes; + float m_positionTolerance; + float m_positionToleranceReciprocal; + AZ::u32 m_currentOriginalVertexIndex = 0; + }; + + template + Vector3Map(const MeshDataType*) -> Vector3Map; + MeshOptimizerComponent::MeshOptimizerComponent() { BindToCall(&MeshOptimizerComponent::OptimizeMeshes); @@ -107,7 +186,7 @@ namespace AZ::SceneGenerationComponents auto* serializeContext = azrtti_cast(context); if (serializeContext) { - serializeContext->Class()->Version(3); + serializeContext->Class()->Version(4); } } @@ -116,7 +195,8 @@ namespace AZ::SceneGenerationComponents const MeshDataType* meshData, const SkinWeightDataView& skinWeights, AZ::u32 maxWeightsPerVertex, - float weightThreshold) + float weightThreshold, + const Vector3Map& positionMap) { if (skinWeights.empty()) { @@ -142,15 +222,12 @@ namespace AZ::SceneGenerationComponents for (size_t linkIndex = 0; linkIndex < linkCount; ++linkIndex) { const ISkinWeightData::Link& link = skinData.get().GetLink(controlPointIndex, linkIndex); - skinningInfo->AddInfluence(usedPointIndex, {aznumeric_caster(link.boneId), link.weight}); + skinningInfo->AddInfluence(positionMap.at(usedPointIndex), {aznumeric_caster(link.boneId), link.weight}); } } } - if (skinningInfo) - { - skinningInfo->Optimize(maxWeightsPerVertex, weightThreshold); - } + skinningInfo->Optimize(maxWeightsPerVertex, weightThreshold); return skinningInfo; } @@ -430,16 +507,9 @@ namespace AZ::SceneGenerationComponents const AZStd::vector bitangentLayers = makeLayersForData(bitangents); const AZStd::vector vertexColorLayers = makeLayersForData(vertexColors); - const auto* skinRule = meshGroup.GetRuleContainerConst().FindFirstByType().get(); - const AZ::u32 maxWeightsPerVertex = skinRule ? skinRule->GetMaxWeightsPerVertex() : 4; - const float weightThreshold = skinRule ? skinRule->GetWeightThreshold() : 0.001f; - meshBuilder.SetSkinningInfo(ExtractSkinningInfo(meshData, skinWeights, maxWeightsPerVertex, weightThreshold)); - constexpr float positionTolerance = 0.0001f; - constexpr float positionToleranceReciprocal = 1.0f / positionTolerance; - AZStd::unordered_map positionMap{}; - - AZ::u32 currentOriginalVertexIndex = 0; + Vector3Map positionMap(meshData, hasBlendShapes, positionTolerance); + positionMap.reserve(vertexCount); // Add the vertex data to all the layers const AZ::u32 faceCount = meshData->GetFaceCount(); @@ -448,32 +518,7 @@ namespace AZ::SceneGenerationComponents meshBuilder.BeginPolygon(baseMesh->GetFaceMaterialId(faceIndex)); for (const AZ::u32 vertexIndex : meshData->GetFaceInfo(faceIndex).vertexIndex) { - const AZ::u32 orgVertexNumber = [&meshData, &hasBlendShapes, &vertexIndex, &positionMap, ¤tOriginalVertexIndex, positionTolerance, positionToleranceReciprocal]() -> AZ::u32 - { - if (hasBlendShapes) - { - // Don't attempt to weld similar vertices if there's blendshapes - // Welding the vertices here based on position could cause the vertices of a base shape to be - // welded, and the vertices of the blendshape to not be welded, resulting in a vertex count - // mismatch between the two - return meshData->GetUsedPointIndexForControlPoint(meshData->GetControlPointIndex(vertexIndex)); - } - - // Round the vertex position so that a float comparison can be made with entires in the positionMap - // pos = floor( x * 10 + 0.5) * 0.1 - const AZ::Vector3 position = AZ::Vector3( - AZ::Simd::Vec3::Floor( - (meshData->GetPosition(vertexIndex) * positionToleranceReciprocal + AZ::Vector3(0.5f)).GetSimdValue() - ) - ) * positionTolerance; - - const auto& [iter, didInsert] = positionMap.try_emplace(position, currentOriginalVertexIndex); - if (didInsert) - { - ++currentOriginalVertexIndex; - } - return iter->second; - }(); + const AZ::u32 orgVertexNumber = positionMap[vertexIndex]; orgVtxLayer->SetCurrentVertexValue(orgVertexNumber); @@ -504,6 +549,12 @@ namespace AZ::SceneGenerationComponents meshBuilder.EndPolygon(); } + + const auto* skinRule = meshGroup.GetRuleContainerConst().FindFirstByType().get(); + const AZ::u32 maxWeightsPerVertex = skinRule ? skinRule->GetMaxWeightsPerVertex() : 4; + const float weightThreshold = skinRule ? skinRule->GetWeightThreshold() : 0.001f; + meshBuilder.SetSkinningInfo(ExtractSkinningInfo(meshData, skinWeights, maxWeightsPerVertex, weightThreshold, positionMap)); + meshBuilder.GenerateSubMeshVertexOrders(); // Create the resulting nodes diff --git a/Gems/SceneProcessing/Code/Tests/MeshBuilder/MeshOptimizerComponentTests.cpp b/Gems/SceneProcessing/Code/Tests/MeshBuilder/MeshOptimizerComponentTests.cpp index f374cc833c..81528fc3e4 100644 --- a/Gems/SceneProcessing/Code/Tests/MeshBuilder/MeshOptimizerComponentTests.cpp +++ b/Gems/SceneProcessing/Code/Tests/MeshBuilder/MeshOptimizerComponentTests.cpp @@ -16,68 +16,119 @@ #include #include #include +#include +#include #include #include #include +#include #include #include #include +namespace AZ::SceneAPI::DataTypes +{ + void PrintTo(const ISkinWeightData::Link& link, ::std::ostream* os) + { + *os << '{' << link.boneId << ", " << link.weight << '}'; + } +} + namespace SceneProcessing { - using VertexDeduplicationFixture = SceneProcessing::InitSceneAPIFixture; - - TEST_F(VertexDeduplicationFixture, CanDeduplicateVertices) + class VertexDeduplicationFixture + : public SceneProcessing::InitSceneAPIFixture { - AZ::ComponentApplication app; - AZ::Entity* systemEntity = app.Create({}, {}); - systemEntity->AddComponent(aznew AZ::MemoryComponent()); - systemEntity->AddComponent(aznew AZ::JobManagerComponent()); - systemEntity->Init(); - systemEntity->Activate(); - - AZ::SceneAPI::Containers::Scene scene("testScene"); - AZ::SceneAPI::Containers::SceneGraph& graph = scene.GetGraph(); - - // Create a simple plane with 2 triangles, 6 total vertices, 2 shared vertices - // 0 --- 1 - // | / | - // | / | - // | / | - // 2 --- 3 - const AZStd::array planeVertexPositions = { - AZ::Vector3{0.0f, 0.0f, 0.0f}, - AZ::Vector3{0.0f, 0.0f, 1.0f}, - AZ::Vector3{1.0f, 0.0f, 1.0f}, - AZ::Vector3{1.0f, 0.0f, 1.0f}, - AZ::Vector3{1.0f, 0.0f, 0.0f}, - AZ::Vector3{0.0f, 0.0f, 0.0f}, - }; + public: + void SetUp() override { - auto mesh = AZStd::make_unique(); - { - int i = 0; - for (const AZ::Vector3& position : planeVertexPositions) - { - mesh->AddPosition(position); - mesh->AddNormal(AZ::Vector3::CreateAxisY()); + SceneProcessing::InitSceneAPIFixture::SetUp(); - // This assumes that the data coming from the import process gives a unique control point - // index to every vertex. This follows the behavior of the AssImp library. - mesh->SetVertexIndexToControlPointIndexMap(i, i); - ++i; - } + m_systemEntity = m_app.Create({}, {}); + m_systemEntity->AddComponent(aznew AZ::MemoryComponent()); + m_systemEntity->AddComponent(aznew AZ::JobManagerComponent()); + m_systemEntity->Init(); + m_systemEntity->Activate(); + } + void TearDown() override + { + m_systemEntity->Deactivate(); + SceneProcessing::InitSceneAPIFixture::TearDown(); + } + + static AZStd::unique_ptr MakePlaneMesh() + { + // Create a simple plane with 2 triangles, 6 total vertices, 2 shared vertices + // 0 --- 1 + // | / | + // | / | + // | / | + // 2 --- 3 + const AZStd::array planeVertexPositions = { + AZ::Vector3{0.0f, 0.0f, 0.0f}, + AZ::Vector3{0.0f, 0.0f, 1.0f}, + AZ::Vector3{1.0f, 0.0f, 1.0f}, + AZ::Vector3{1.0f, 0.0f, 1.0f}, + AZ::Vector3{1.0f, 0.0f, 0.0f}, + AZ::Vector3{0.0f, 0.0f, 0.0f}, + }; + + auto mesh = AZStd::make_unique(); + + int i = 0; + for (const AZ::Vector3& position : planeVertexPositions) + { + mesh->AddPosition(position); + mesh->AddNormal(AZ::Vector3::CreateAxisY()); + + // This assumes that the data coming from the import process gives a unique control point + // index to every vertex. This follows the behavior of the AssImp library. + mesh->SetVertexIndexToControlPointIndexMap(i, i); + ++i; } + mesh->AddFace({0, 1, 2}, 0); mesh->AddFace({3, 4, 5}, 0); - // The original source mesh should have 6 vertices - EXPECT_EQ(mesh->GetVertexCount(), planeVertexPositions.size()); - - graph.AddChild(graph.GetRoot(), "testMesh", AZStd::move(mesh)); + return mesh; } + static AZStd::unique_ptr MakeSkinData() + { + auto skinWeights = AZStd::make_unique(); + + skinWeights->ResizeContainerSpace(6); + + // Add bones 0 and 1 to the skin weights + skinWeights->GetBoneId("0"); + skinWeights->GetBoneId("1"); + + skinWeights->AppendLink(0, {/*.boneId=*/0, /*.weight=*/1}); + skinWeights->AppendLink(1, {/*.boneId=*/0, /*.weight=*/1}); + skinWeights->AppendLink(2, {/*.boneId=*/0, /*.weight=*/1}); + skinWeights->AppendLink(3, {/*.boneId=*/1, /*.weight=*/1}); + skinWeights->AppendLink(4, {/*.boneId=*/1, /*.weight=*/1}); + skinWeights->AppendLink(5, {/*.boneId=*/1, /*.weight=*/1}); + + return skinWeights; + } + + private: + AZ::ComponentApplication m_app; + AZ::Entity* m_systemEntity; + }; + + TEST_F(VertexDeduplicationFixture, CanDeduplicateVertices) + { + AZ::SceneAPI::Containers::Scene scene("testScene"); + AZ::SceneAPI::Containers::SceneGraph& graph = scene.GetGraph(); + + const auto meshNodeIndex = graph.AddChild(graph.GetRoot(), "testMesh", MakePlaneMesh()); + + // The original source mesh should have 6 vertices + EXPECT_EQ(AZStd::rtti_pointer_cast(graph.GetNodeContent(meshNodeIndex))->GetVertexCount(), 6); + auto meshGroup = AZStd::make_unique(); meshGroup->GetSceneNodeSelectionList().AddSelectedNode("testMesh"); scene.GetManifest().AddEntry(AZStd::move(meshGroup)); @@ -94,7 +145,77 @@ namespace SceneProcessing // The optimized mesh should have 4 vertices, the 2 shared vertices are welded together EXPECT_EQ(optimizedMesh->GetVertexCount(), 4); + } - systemEntity->Deactivate(); + MATCHER(VectorOfLinksEq, "") + { + return testing::ExplainMatchResult( + testing::AllOf( + testing::Field(&AZ::SceneData::GraphData::SkinWeightData::Link::boneId, testing::Eq(testing::get<0>(arg).boneId)), + testing::Field(&AZ::SceneData::GraphData::SkinWeightData::Link::weight, testing::FloatEq(testing::get<0>(arg).weight)) + ), + testing::get<1>(arg), + result_listener + ); + } + + MATCHER(VectorOfVectorOfLinksEq, "") + { + return testing::ExplainMatchResult( + testing::UnorderedPointwise(VectorOfLinksEq(), testing::get<0>(arg)), + testing::get<1>(arg), + result_listener + ); + } + + TEST_F(VertexDeduplicationFixture, DeduplicatedVerticesRemapSkinning) + { + AZ::SceneAPI::Containers::Scene scene("testScene"); + AZ::SceneAPI::Containers::SceneGraph& graph = scene.GetGraph(); + + const auto meshNodeIndex = graph.AddChild(graph.GetRoot(), "testMesh", MakePlaneMesh()); + const auto skinDataNodeIndex = graph.AddChild(meshNodeIndex, "skinData", MakeSkinData()); + graph.MakeEndPoint(skinDataNodeIndex); + + // The original source mesh should have 6 vertices + EXPECT_EQ(AZStd::rtti_pointer_cast(graph.GetNodeContent(meshNodeIndex))->GetVertexCount(), 6); + + auto meshGroup = AZStd::make_unique(); + meshGroup->GetSceneNodeSelectionList().AddSelectedNode("testMesh"); + scene.GetManifest().AddEntry(AZStd::move(meshGroup)); + + AZ::SceneGenerationComponents::MeshOptimizerComponent component; + AZ::SceneAPI::Events::GenerateSimplificationEventContext context(scene, "pc"); + component.OptimizeMeshes(context); + + AZ::SceneAPI::Containers::SceneGraph::NodeIndex optimizedNodeIndex = graph.Find(AZStd::string("testMesh").append(AZ::SceneAPI::Utilities::OptimizedMeshSuffix)); + ASSERT_TRUE(optimizedNodeIndex.IsValid()) << "Mesh optimizer did not add an optimized version of the mesh"; + + const auto& optimizedMesh = AZStd::rtti_pointer_cast(graph.GetNodeContent(optimizedNodeIndex)); + ASSERT_TRUE(optimizedMesh); + + AZ::SceneAPI::Containers::SceneGraph::NodeIndex optimizedSkinDataNodeIndex = graph.Find(AZStd::string("testMesh").append(AZ::SceneAPI::Utilities::OptimizedMeshSuffix).append(".skinWeights")); + ASSERT_TRUE(optimizedSkinDataNodeIndex.IsValid()) << "Mesh optimizer did not add an optimized version of the skin data"; + + const auto& optimizedSkinWeights = AZStd::rtti_pointer_cast(graph.GetNodeContent(optimizedSkinDataNodeIndex)); + ASSERT_TRUE(optimizedSkinWeights); + + const AZStd::vector> expectedLinks + { + /*0*/ { {0, 0.5f}, {1, 0.5f} }, + /*1*/ { {0, 1.0f} }, + /*2*/ { {0, 0.5f}, {1, 0.5f} }, + /*3*/ { {1, 1.0f} }, + }; + + AZStd::vector> gotLinks(optimizedMesh->GetVertexCount()); + for (unsigned int vertexIndex = 0; vertexIndex < optimizedMesh->GetVertexCount(); ++vertexIndex) + { + for (size_t linkIndex = 0; linkIndex < optimizedSkinWeights->GetLinkCount(vertexIndex); ++linkIndex) + { + gotLinks[vertexIndex].emplace_back(optimizedSkinWeights->GetLink(vertexIndex, linkIndex)); + } + } + EXPECT_THAT(gotLinks, testing::Pointwise(VectorOfVectorOfLinksEq(), expectedLinks)); } } // namespace SceneProcessing From 2355581d79abdb2e41417c0bcfb4adc1ce6f2f9a Mon Sep 17 00:00:00 2001 From: Chris Burel Date: Fri, 16 Jul 2021 16:48:38 -0700 Subject: [PATCH 8/8] Replace Array2D with vector> The thought behind Array2D was that it would be more efficient from a memory allocation perspective to have one fixed buffer that grows than it would be to have a vector of vectors. In reality, the runtime of inserting into the middle of one large buffer, and shifting all the resulting elements, ends up far outweighing any memory allocation overhead. In the mesh optimizer, things are added to the end of each sub-vector one by one. It isn't known up front how many elements each sub-vector will have. With vertex welding enabled, it is far more likely that a given vertex will be influenced by a large number of joints. When using the Array2D to store the influences, and a vertex with a low index has more than 4 influences, those influences have to be inserted into close to the front of the big vector, and all the other elements shifted. Array2D was doing this with a linear shift, shifting the elements by 1 at a time, and not providing any exponential growth on the amount of elements reserved by each sub-vector. The result is a linear insertion time. By contrast, using vector> instead gives us back the amortized constant insertion time. Since the skin influences are just added to the end of each sub-vector, no shifting of the elements is necessary. And since the amount of original vertex indices is known up front, the number of sub-vectors can still be pre-created, so the sub-vectors themselves never need to shift. Signed-off-by: Chris Burel --- .../Components/MeshOptimizer/Array2D.h | 264 ---------------- .../Components/MeshOptimizer/Array2D.inl | 289 ------------------ .../MeshOptimizer/MeshBuilderSkinningInfo.cpp | 11 +- .../MeshOptimizer/MeshBuilderSkinningInfo.h | 27 +- .../MeshOptimizer/MeshOptimizerComponent.cpp | 3 +- .../Tests/MeshBuilder/SkinInfluencesTests.cpp | 2 +- .../sceneprocessing_editor_static_files.cmake | 2 - 7 files changed, 26 insertions(+), 572 deletions(-) delete mode 100644 Gems/SceneProcessing/Code/Source/Generation/Components/MeshOptimizer/Array2D.h delete mode 100644 Gems/SceneProcessing/Code/Source/Generation/Components/MeshOptimizer/Array2D.inl diff --git a/Gems/SceneProcessing/Code/Source/Generation/Components/MeshOptimizer/Array2D.h b/Gems/SceneProcessing/Code/Source/Generation/Components/MeshOptimizer/Array2D.h deleted file mode 100644 index a2bad5a5c2..0000000000 --- a/Gems/SceneProcessing/Code/Source/Generation/Components/MeshOptimizer/Array2D.h +++ /dev/null @@ -1,264 +0,0 @@ -/* - * 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 - * - */ - -#pragma once - -#include - -namespace MCore -{ - /** - * A dynamic 2D array template. - * This would be a better solution than "Array< Array< T > >", because the Array inside Array will perform many allocations, - * while this specialized 2D array will only perform two similar allocations. - * What it does is keep one big array of data elements, and maintain a table that indices inside this big array. - * We advise you to call the Shrink function after you performed a number of operations on the array, to maximize its memory usage efficiency. - * - * The layout of the array is as following: - * - *
-     *
-     * [ROW0]: [E0][E1][E2]
-     * [ROW1]: [E0][E1]
-     * [ROW2]: [E0][E1][E2][E3]
-     * [ROW3]: [E0]
-     *
-     * 
- * - * Where E0, E1, E2, etc are elements of the specified type T. - * Each row can have a different amount of elements that can be added or removed dynamically. Also rows can be deleted - * or added when desired. - */ - template - class Array2D - { - public: - /** - * An index table entry. - * Each row in the 2D array will get a table entry, which tells us where in the data array - * the element data starts for the given row, and how many elements will follow for the given row. - */ - struct TableEntry - { - size_t mStartIndex; /**< The index offset where the data for this row starts. */ - size_t mNumElements; /**< The number of elements to follow. */ - }; - - /** - * The default constructor. - * The number of pre-cached/allocated elements per row is set to a value of 2 on default. - * You can use the SetNumPreCachedElements(...) method to adjust this value. Make sure you adjust this value - * before you call the Resize method though, otherwise it will have no immediate effect. - */ - Array2D() = default; - - /** - * Extended constructor which will automatically initialize the array dimensions. - * Basically this will initialize the array dimensions at (numRows x numPreAllocatedElemsPerRow) elements. - * Please note though, that this will NOT add actual elements. So you can't get values from the elements yet. - * This would just pre-allocate data. You have to use the Add method to actually fill the items. - * @param numRows The number of rows the array should have. - * @param numPreAllocatedElemsPerRow The number of pre-cached/allocated elements per row. - * - */ - Array2D(size_t numRows, size_t numPreAllocatedElemsPerRow = 2) - : mNumPreCachedElements(numPreAllocatedElemsPerRow) { Resize(numRows); } - - /** - * Resize the array in one dimension (the number of rows). - * Rows that will be added willl automatically get [n] number of elements pre-allocated. - * The number of [n] can be set with the SetNumPreCachedElements(...) method. - * Please note that the pre-allocated/cached elements are not valid to be used yet. You have to use the Add method first. - * @param numRows The number of rows you wish to init for. - * @param autoShrink When set to true, after execution of this method the Shrink method will automatically be called in order - * to optimize the memory usage. This only happens when resizing to a lower amount of rows, so when making the array smaller. - */ - void Resize(size_t numRows, bool autoShrink = false); - - /** - * Add an element to the list of elements in a given row. - * @param rowIndex The row number to add the element to. - * @param element The value of the element to add. - */ - void Add(size_t rowIndex, const T& element); - - /** - * Remove an element from the array. - * @param rowIndex The row number where the element is stored. - * @param elementIndex The element number inside this row to remove. - */ - void Remove(size_t rowIndex, size_t elementIndex); - - /** - * Remove a given row, including all its elements. - * This will decrease the number of rows. - * @param rowIndex The row number to remove. - * @param autoShrink When set to true, the array's memory usage will be optimized and minimized as much as possible. - */ - void RemoveRow(size_t rowIndex, bool autoShrink = false); - - /** - * Remove a given range of rows and all their elements. - * All rows from the specified start row until the end row will be removed, with the start and end rows included. - * @param startRow The start row number to start removing from (so this one will also be removed). - * @param endRow The end row number (which will also be removed). - * @param autoShrink When set to true, the array's memory usage will be optimized and minimized as much as possible. - */ - void RemoveRows(size_t startRow, size_t endRow, bool autoShrink = false); - - /** - * Optimize (minimize) the memory usage of the array. - * This will move all elements around, removing all gaps and unused pre-cached/allocated items. - * It is advised to call this method after you applied some heavy modifications to the array, such as - * removing rows or many elements. When your array data is fairly static, and you won't be adding or removing - * data from it very frequently, you should definitely call this method after you have filled the array with data. - */ - void Shrink(); - - /** - * Set the number of elements per row that should be pre-allocated/cached when creating / adding new rows. - * This doesn't actually increase the number of elements for a given row, but just reserves memory for the elements, which can - * speedup adding of new elements and prevent memory reallocs. The default value is set to 2 when creating an array, unless specified differently. - * @param numElemsPerRow The number of elements per row that should be pre-allocated. - */ - void SetNumPreCachedElements(size_t numElemsPerRow) { mNumPreCachedElements = numElemsPerRow; } - - /** - * Get the number of pre-cached/allocated elements per row, when creating new rows. - * See the SetNumPreCachedElements for more information. - * @result The number of elements per row that will be pre-allocated/cached when adding a new row. - * @see SetNumPreCachedElements. - */ - size_t GetNumPreCachedElements() const { return mNumPreCachedElements; } - - /** - * Get the number of stored elements inside a given row. - * @param rowIndex The row number. - * @result The number of elements stored inside this row. - */ - size_t GetNumElements(size_t rowIndex) const { return mIndexTable[rowIndex].mNumElements; } - - /** - * Get a pointer to the element data stored in a given row. - * Use this method with care as it can easily overwrite data from other elements. - * All element data for a given row is stored sequential, so right after eachother in one continuous piece of memory. - * The next row's element data however might not be connected to the memory of row before that! - * Also only use this method when the GetNumElements(...) method for this row returns a value greater than zero. - * @param rowIndex the row number. - * @result A pointer to the element data for the given row. - */ - T* GetElements(size_t rowIndex) { return &mData[ mIndexTable[rowIndex].mStartIndex ]; } - - /** - * Get the data of a given element. - * @param rowIndex The row number where the element is stored. - * @param elementNr The element number inside this row to retrieve. - * @result A reference to the element data. - */ - T& GetElement(size_t rowIndex, size_t elementNr) { return mData[ mIndexTable[rowIndex].mStartIndex + elementNr ]; } - - /** - * Get the data of a given element. - * @param rowIndex The row number where the element is stored. - * @param elementNr The element number inside this row to retrieve. - * @result A const reference to the element data. - */ - const T& GetElement(size_t rowIndex, size_t elementNr) const { return mData[ mIndexTable[rowIndex].mStartIndex + elementNr ]; } - - /** - * Set the value for a given element in the array. - * @param rowIndex The row where the element is stored in. - * @param elementNr The element number to set the value for. - * @param value The value to set the element to. - */ - void SetElement(size_t rowIndex, size_t elementNr, const T& value) { MCORE_ASSERT(rowIndex < mIndexTable.GetLength()); MCORE_ASSERT(elementNr < mIndexTable[rowIndex].mNumElements); mData[ mIndexTable[rowIndex].mStartIndex + elementNr ] = value; } - - /** - * Get the number of rows in the 2D array. - * @result The number of rows. - */ - size_t GetNumRows() const { return mIndexTable.size(); } - - /** - * Calculate the percentage of memory that is filled with element data. - * When this is 100%, then all allocated element data is filled and used. - * When it would be 25% then only 25% of all allocated element data is used. This is an indication that - * you should most likely use the Shrink method, which will ensure that the memory usage will become 100% again, which - * would be most optimal. - * @result The percentage (in range of 0..100) of used element memory. - */ - float CalcUsedElementMemoryPercentage() const { return (mData.GetLength() ? (CalcTotalNumElements() / (float)mData.GetLength()) * 100.0f : 0); } - - /** - * Swap the element data of two rows. - * Beware, this is pretty slow! - * @param rowA The first row. - * @param rowB The second row. - */ - void Swap(size_t rowA, size_t rowB); - - /** - * Calculate the total number of used elements. - * A used element is an element that has been added and that has a valid value stored. - * This excludes pre-allocated/cached elements. - * @result The total number of elements stored in the array. - */ - size_t CalcTotalNumElements() const; - - /** - * Clear all contents. - * This deletes all rows and clears all their their elements as well. - * Please keep in mind though, that when you have an array of pointers to objects you allocated, that - * you still have to delete those objects by hand! The Clear function will not delete those. - * @param freeMem When set to true, all memory used by the array internally will be deleted. If set to false, the memory - * will not be deleted and can be reused later on again without doing any memory realloc when possible. - */ - void Clear(bool freeMem = true) - { - mIndexTable.clear(); - mData.clear(); - if (freeMem) - { - mIndexTable.shrink_to_fit(); - mData.shrink_to_fit(); - } - } - - /** - * Log all array contents. - * This will log the number of rows, number of elements, used element memory percentage, as well - * as some details about each row. - */ - void LogContents(); - - /** - * Get the index table. - * This table describes for each row the start index and number of elements for the row. - * The length of the array equals the value returned by GetNumRows(). - * @result The array of index table entries, which specify the start indices and number of entries per row. - */ - AZStd::vector& GetIndexTable() { return mIndexTable; } - - /** - * Get the data array. - * This contains the data array in which the index table points. - * Normally you shouldn't be using this method. However it is useful in some specific cases. - * @result The data array that contains all elements. - */ - AZStd::vector& GetData() { return mData; } - - private: - AZStd::vector mData; /**< The element data. */ - AZStd::vector mIndexTable; /**< The index table that let's us know where what data is inside the element data array. */ - size_t mNumPreCachedElements = 2; /**< The number of elements per row to pre-allocate when resizing this array. This prevents some re-allocs. */ - }; - - - // include inline code -#include "Array2D.inl" -} // namespace MCore diff --git a/Gems/SceneProcessing/Code/Source/Generation/Components/MeshOptimizer/Array2D.inl b/Gems/SceneProcessing/Code/Source/Generation/Components/MeshOptimizer/Array2D.inl deleted file mode 100644 index 47a3fd7015..0000000000 --- a/Gems/SceneProcessing/Code/Source/Generation/Components/MeshOptimizer/Array2D.inl +++ /dev/null @@ -1,289 +0,0 @@ -/* - * 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 - * - */ - -// resize the array's number of rows -template -void Array2D::Resize(size_t numRows, bool autoShrink) -{ - // get the current (old) number of rows - const size_t oldNumRows = mIndexTable.size(); - - // don't do anything when we don't need to - if (numRows == oldNumRows) - { - return; - } - - // resize the index table - mIndexTable.resize(numRows); - - // check if we decreased the number of rows or not - if (numRows < oldNumRows) - { - // pack memory as tight as possible - if (autoShrink) - { - Shrink(); - } - } - else // we added new entries - { - // init the new table entries - for (size_t i = oldNumRows; i < numRows; ++i) - { - mIndexTable[i].mStartIndex = mData.size() + (i * mNumPreCachedElements); - mIndexTable[i].mNumElements = 0; - } - - // grow the data array - const size_t numNewRows = numRows - oldNumRows; - mData.resize(mData.size() + numNewRows * mNumPreCachedElements); - } -} - - -// add an element -template -void Array2D::Add(size_t rowIndex, const T& element) -{ - AZ_Assert(rowIndex < mIndexTable.size(), "Array index out of bounds"); - - // find the insert location inside the data array - size_t insertPos = mIndexTable[rowIndex].mStartIndex + mIndexTable[rowIndex].mNumElements; - if (insertPos >= mData.size()) - { - mData.resize(insertPos + 1); - } - - // check if we need to insert for real - bool needRealInsert = true; - if (rowIndex < mIndexTable.size() - 1) // if there are still entries coming after the one we have to add to - { - if (insertPos < mIndexTable[rowIndex + 1].mStartIndex) // if basically there are empty unused element we can use - { - needRealInsert = false; // then we don't need to do any reallocs - } - } - else - { - // if we're dealing with the last row - if (rowIndex == mIndexTable.size() - 1) - { - if (insertPos < mData.size()) // if basically there are empty unused element we can use - { - needRealInsert = false; - } - } - } - - // perform the insertion - if (needRealInsert) - { - // insert the element inside the data array - mData.insert(AZStd::next(begin(mData), insertPos), element); - - // adjust the index table entries - const size_t numRows = mIndexTable.size(); - for (size_t i = rowIndex + 1; i < numRows; ++i) - { - mIndexTable[i].mStartIndex++; - } - } - else - { - mData[insertPos] = element; - } - - // increase the number of elements in the index table - mIndexTable[rowIndex].mNumElements++; -} - - -// remove a given element -template -void Array2D::Remove(size_t rowIndex, size_t elementIndex) -{ - AZ_Assert(rowIndex < mIndexTable.size(), "Array2D<>::Remove: array index out of bounds"); - AZ_Assert(elementIndex < mIndexTable[rowIndex].mNumElements, "Array2D<>::Remove: element index out of bounds"); - AZ_Assert(mIndexTable[rowIndex].mNumElements > 0, "Array2D<>::Remove: array index out of bounds"); - - const size_t startIndex = mIndexTable[rowIndex].mStartIndex; - const size_t maxElementIndex = mIndexTable[rowIndex].mNumElements - 1; - - // swap the last element with the one to be removed - if (elementIndex != maxElementIndex) - { - mData[startIndex + elementIndex] = mData[startIndex + maxElementIndex]; - } - - // decrease the number of elements - mIndexTable[rowIndex].mNumElements--; -} - - -// remove a given row -template -void Array2D::RemoveRow(size_t rowIndex, bool autoShrink) -{ - AZ_Assert(rowIndex < mIndexTable.GetLength(), "Array2D<>::RemoveRow: rowIndex out of bounds"); - mIndexTable.Remove(rowIndex); - - // optimize memory usage when desired - if (autoShrink) - { - Shrink(); - } -} - - -// remove a set of rows -template -void Array2D::RemoveRows(size_t startRow, size_t endRow, bool autoShrink) -{ - AZ_Assert(startRow < mIndexTable.size(), "Array2D<>::RemoveRows: startRow out of bounds"); - AZ_Assert(endRow < mIndexTable.size(), "Array2D<>::RemoveRows: endRow out of bounds"); - - // check if the start row is smaller than the end row - if (startRow < endRow) - { - const size_t numToRemove = (endRow - startRow) + 1; - mIndexTable.erase(AZStd::next(begin(mIndexTable), startRow), AZStd::next(AZStd::next(begin(mIndexTable), startRow), numToRemove)); - } - else // if the end row is smaller than the start row - { - const size_t numToRemove = (startRow - endRow) + 1; - mIndexTable.erase(AZStd::next(begin(mIndexTable), endRow), AZStd::next(AZStd::next(begin(mIndexTable), endRow), numToRemove)); - } - - // optimize memory usage when desired - if (autoShrink) - { - Shrink(); - } -} - - -// optimize memory usage -template -void Array2D::Shrink() -{ - // for all attributes, except for the last one - const size_t numRows = mIndexTable.size(); - if (numRows == 0) - { - return; - } - - // remove all unused items between the rows (unused element data per row) - const size_t numRowsMinusOne = numRows - 1; - for (size_t a = 0; a < numRowsMinusOne; ++a) - { - const size_t firstUnusedIndex = mIndexTable[a ].mStartIndex + mIndexTable[a].mNumElements; - const size_t numUnusedElements = mIndexTable[a + 1].mStartIndex - firstUnusedIndex; - - // if we have pre-cached/unused elements, remove those by moving memory to remove the "holes" - if (numUnusedElements > 0) - { - // remove the unused elements from the array - mData.erase(AZStd::next(begin(mData), firstUnusedIndex), AZStd::next(AZStd::next(begin(mData), firstUnusedIndex), numUnusedElements)); - - // change the start indices for all the rows coming after the current one - const size_t numTotalRows = mIndexTable.size(); - for (size_t i = a + 1; i < numTotalRows; ++i) - { - mIndexTable[i].mStartIndex -= numUnusedElements; - } - } - } - - // now move all start index values and all data to the front of the data array as much as possible - // like data on row 0 starting at data element 7, would be moved to data element 0 - size_t dataPos = 0; - for (size_t row = 0; row < numRows; ++row) - { - // if the data starts after the place where it could start, move it to the place where it could start - if (mIndexTable[row].mStartIndex > dataPos) - { - AZStd::move(AZStd::next(begin(mData), this->mIndexTable[row].mStartIndex), AZStd::next(AZStd::next(begin(mData), this->mIndexTable[row].mStartIndex), this->mIndexTable[row].mNumElements), AZStd::next(begin(mData), dataPos)); - mIndexTable[row].mStartIndex = dataPos; - } - - // increase the data pos - dataPos += mIndexTable[row].mNumElements; - } - - // remove all unused data items - if (dataPos < mData.size()) - { - mData.erase(AZStd::next(begin(mData), dataPos), end(mData)); - } - - // shrink the arrays - mData.shrink_to_fit(); - mIndexTable.shrink_to_fit(); -} - - -// calculate the number of used elements -template -size_t Array2D::CalcTotalNumElements() const -{ - size_t totalElements = 0; - - // add all number of row elements together - const size_t numRows = mIndexTable.size(); - for (size_t i = 0; i < numRows; ++i) - { - totalElements += mIndexTable[i].mNumElements; - } - - return totalElements; -} - - -// swap the contents of two rows -template -void Array2D::Swap(size_t rowA, size_t rowB) -{ - // get the original number of elements from both rows - const size_t numElementsA = mIndexTable[rowA].mNumElements; - const size_t numElementsB = mIndexTable[rowB].mNumElements; - - // move the element data of rowA into a temp buffer - AZStd::vector tempData(numElementsA); - AZStd::move( - AZStd::next(mData.begin(), mIndexTable[rowA].mStartIndex), - AZStd::next(mData.begin(), mIndexTable[rowA].mStartIndex + numElementsA), - tempData.begin() - ); - - // remove the elements from rowA - while (GetNumElements(rowA)) - { - Remove(rowA, 0); - } - - // add all elements of row B - size_t i; - for (i = 0; i < numElementsB; ++i) - { - Add(rowA, GetElement(rowB, i)); - } - - // remove all elements from B - while (GetNumElements(rowB)) - { - Remove(rowB, 0); - } - - // add all elements from the original A - for (i = 0; i < numElementsA; ++i) - { - Add(rowB, tempData[i]); - } -} diff --git a/Gems/SceneProcessing/Code/Source/Generation/Components/MeshOptimizer/MeshBuilderSkinningInfo.cpp b/Gems/SceneProcessing/Code/Source/Generation/Components/MeshOptimizer/MeshBuilderSkinningInfo.cpp index 712e0c8ce0..1d62489fbe 100644 --- a/Gems/SceneProcessing/Code/Source/Generation/Components/MeshOptimizer/MeshBuilderSkinningInfo.cpp +++ b/Gems/SceneProcessing/Code/Source/Generation/Components/MeshOptimizer/MeshBuilderSkinningInfo.cpp @@ -25,8 +25,11 @@ namespace AZ::MeshBuilder // constructor MeshBuilderSkinningInfo::MeshBuilderSkinningInfo(size_t numOrgVertices) { - mInfluences.SetNumPreCachedElements(4); // TODO: verify if this is the fastest - mInfluences.Resize(numOrgVertices); + mInfluences.resize(numOrgVertices); + for (auto& subArray : mInfluences) + { + subArray.reserve(4); + } } @@ -100,13 +103,13 @@ namespace AZ::MeshBuilder // remove all influences for (size_t i = 0; i < numInfluences; ++i) { - mInfluences.Remove(v, 0); + RemoveInfluence(v, 0); } // re-add them for (const Influence& influence : influences) { - mInfluences.Add(v, influence); + AddInfluence(v, influence); } } } diff --git a/Gems/SceneProcessing/Code/Source/Generation/Components/MeshOptimizer/MeshBuilderSkinningInfo.h b/Gems/SceneProcessing/Code/Source/Generation/Components/MeshOptimizer/MeshBuilderSkinningInfo.h index 5f95265bb6..f5b716876e 100644 --- a/Gems/SceneProcessing/Code/Source/Generation/Components/MeshOptimizer/MeshBuilderSkinningInfo.h +++ b/Gems/SceneProcessing/Code/Source/Generation/Components/MeshOptimizer/MeshBuilderSkinningInfo.h @@ -10,8 +10,8 @@ #include #include +#include #include "MeshBuilderInvalidIndex.h" -#include "Array2D.h" namespace AZ::MeshBuilder { @@ -35,14 +35,19 @@ namespace AZ::MeshBuilder MeshBuilderSkinningInfo(size_t numOrgVertices); - void AddInfluence(size_t orgVtxNr, size_t nodeNr, float weight) { AddInfluence(orgVtxNr, {nodeNr, weight}); } - void AddInfluence(size_t orgVtxNr, const Influence& influence) { mInfluences.Add(orgVtxNr, influence); } - void RemoveInfluence(size_t orgVtxNr, size_t influenceNr) { mInfluences.Remove(orgVtxNr, influenceNr); } - const Influence& GetInfluence(size_t orgVtxNr, size_t influenceNr) const { return mInfluences.GetElement(orgVtxNr, influenceNr); } - size_t GetNumInfluences(size_t orgVtxNr) const { return mInfluences.GetNumElements(orgVtxNr); } - size_t GetNumOrgVertices() const { return mInfluences.GetNumRows(); } - void OptimizeMemoryUsage() { mInfluences.Shrink(); } - size_t CalcTotalNumInfluences() const { return mInfluences.CalcTotalNumElements(); } + void AddInfluence(size_t orgVtxNr, const Influence& influence) { mInfluences.resize(AZStd::max(mInfluences.size(), orgVtxNr)); mInfluences.at(orgVtxNr).emplace_back(influence); } + void RemoveInfluence(size_t orgVtxNr, size_t influenceNr) { mInfluences.at(orgVtxNr).erase(mInfluences.at(orgVtxNr).begin() + influenceNr); } + const Influence& GetInfluence(size_t orgVtxNr, size_t influenceNr) const { return mInfluences.at(orgVtxNr).at(influenceNr); } + size_t GetNumInfluences(size_t orgVtxNr) const { return mInfluences.at(orgVtxNr).size(); } + size_t GetNumOrgVertices() const { return mInfluences.size(); } + void OptimizeMemoryUsage() + { + for (auto& subArray : mInfluences) + { + subArray.shrink_to_fit(); + } + mInfluences.shrink_to_fit(); + } // optimize the weight data void Optimize(AZ::u32 maxNumWeightsPerVertex = 4, float weightThreshold = 0.0001f); @@ -53,7 +58,7 @@ namespace AZ::MeshBuilder // sort the influences, starting with the biggest weight static void SortInfluences(AZStd::vector& influences); - public: - MCore::Array2D mInfluences; + private: + AZStd::vector> mInfluences; }; } // namespace AZ::MeshBuilder diff --git a/Gems/SceneProcessing/Code/Source/Generation/Components/MeshOptimizer/MeshOptimizerComponent.cpp b/Gems/SceneProcessing/Code/Source/Generation/Components/MeshOptimizer/MeshOptimizerComponent.cpp index 084715b83a..35d6db6d03 100644 --- a/Gems/SceneProcessing/Code/Source/Generation/Components/MeshOptimizer/MeshOptimizerComponent.cpp +++ b/Gems/SceneProcessing/Code/Source/Generation/Components/MeshOptimizer/MeshOptimizerComponent.cpp @@ -117,6 +117,7 @@ namespace AZ::SceneGenerationComponents } using AZStd::unordered_map::reserve; + using AZStd::unordered_map::size; AZ::u32 operator[](const AZ::u32 vertexIndex) { @@ -203,7 +204,7 @@ namespace AZ::SceneGenerationComponents return {}; } - const size_t usedControlPointCount = meshData->GetUsedControlPointCount(); + const size_t usedControlPointCount = positionMap.size(); auto skinningInfo = AZStd::make_unique(aznumeric_cast(usedControlPointCount)); diff --git a/Gems/SceneProcessing/Code/Tests/MeshBuilder/SkinInfluencesTests.cpp b/Gems/SceneProcessing/Code/Tests/MeshBuilder/SkinInfluencesTests.cpp index 2917161c41..5749e7ee7b 100644 --- a/Gems/SceneProcessing/Code/Tests/MeshBuilder/SkinInfluencesTests.cpp +++ b/Gems/SceneProcessing/Code/Tests/MeshBuilder/SkinInfluencesTests.cpp @@ -66,7 +66,7 @@ namespace AZ::MeshBuilder for (size_t i = 0; i < numSkinInfluences; ++i) { const float influenceWeight = (i != numSkinInfluences - 1 ? fmod(random.GetRandomFloat(), totalWeight) : totalWeight); - skinningInfo->AddInfluence(v, i, influenceWeight); + skinningInfo->AddInfluence(v, {i, influenceWeight}); totalWeight -= influenceWeight; } const float totalSkinInfluenceWeight = CalcSkinInfluencesTotalWeight(skinningInfo.get(), v); diff --git a/Gems/SceneProcessing/Code/sceneprocessing_editor_static_files.cmake b/Gems/SceneProcessing/Code/sceneprocessing_editor_static_files.cmake index 5915dc81a9..1c27d70532 100644 --- a/Gems/SceneProcessing/Code/sceneprocessing_editor_static_files.cmake +++ b/Gems/SceneProcessing/Code/sceneprocessing_editor_static_files.cmake @@ -20,8 +20,6 @@ set(FILES Source/Generation/Components/TangentGenerator/TangentGenerators/MikkTGenerator.cpp Source/Generation/Components/TangentGenerator/TangentGenerators/BlendShapeMikkTGenerator.h Source/Generation/Components/TangentGenerator/TangentGenerators/BlendShapeMikkTGenerator.cpp - Source/Generation/Components/MeshOptimizer/Array2D.h - Source/Generation/Components/MeshOptimizer/Array2D.inl Source/Generation/Components/MeshOptimizer/MeshBuilder.cpp Source/Generation/Components/MeshOptimizer/MeshBuilder.h Source/Generation/Components/MeshOptimizer/MeshBuilderInvalidIndex.h