Jelajahi Sumber

[MeshOptimizer] Determine the original vertex index based on the position (#1562)

* 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 <[email protected]>

* Use a filter view instead of reimplementing a filter view

Signed-off-by: Chris Burel <[email protected]>

* Don't attempt to weld similar vertices if there's blendshapes

Signed-off-by: Chris Burel <[email protected]>

* Add test for the mesh optimizer's ability to weld nearby vertices

Signed-off-by: Chris Burel <[email protected]>

* Add logging call to show mesh optimizer effect on vertex count

Signed-off-by: Chris Burel <[email protected]>

* Use a bunch of temporaries in order to make `position` `const`

Signed-off-by: Chris Burel <[email protected]>
Chris Burel 4 tahun lalu
induk
melakukan
ef1f95f1d0

+ 12 - 12
Code/Tools/SceneAPI/SceneData/Groups/MeshGroup.h

@@ -28,27 +28,27 @@ namespace AZ
         }
         }
         namespace SceneData
         namespace SceneData
         {
         {
-            class MeshGroup
+            class SCENE_DATA_CLASS MeshGroup
                 : public DataTypes::IMeshGroup
                 : public DataTypes::IMeshGroup
             {
             {
             public:
             public:
                 AZ_RTTI(MeshGroup, "{07B356B7-3635-40B5-878A-FAC4EFD5AD86}", DataTypes::IMeshGroup);
                 AZ_RTTI(MeshGroup, "{07B356B7-3635-40B5-878A-FAC4EFD5AD86}", DataTypes::IMeshGroup);
                 AZ_CLASS_ALLOCATOR(MeshGroup, SystemAllocator, 0)
                 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 void Reflect(AZ::ReflectContext* context);
                 static bool VersionConverter(SerializeContext& context, SerializeContext::DataElementNode& classElement);
                 static bool VersionConverter(SerializeContext& context, SerializeContext::DataElementNode& classElement);

+ 1 - 0
Gems/SceneProcessing/Code/CMakeLists.txt

@@ -111,6 +111,7 @@ if(PAL_TRAIT_BUILD_TESTS_SUPPORTED)
                 PRIVATE
                 PRIVATE
                     Gem::SceneProcessing.Editor.Static
                     Gem::SceneProcessing.Editor.Static
                     AZ::AzTest
                     AZ::AzTest
+                    AZ::SceneData
         )
         )
         ly_add_googletest(
         ly_add_googletest(
             NAME Gem::SceneProcessing.Editor.Tests
             NAME Gem::SceneProcessing.Editor.Tests

+ 44 - 12
Gems/SceneProcessing/Code/Source/Generation/Components/MeshOptimizer/MeshOptimizerComponent.cpp

@@ -106,7 +106,7 @@ namespace AZ::SceneGenerationComponents
         auto* serializeContext = azrtti_cast<AZ::SerializeContext*>(context);
         auto* serializeContext = azrtti_cast<AZ::SerializeContext*>(context);
         if (serializeContext)
         if (serializeContext)
         {
         {
-            serializeContext->Class<MeshOptimizerComponent, GenerationComponent>()->Version(2);
+            serializeContext->Class<MeshOptimizerComponent, GenerationComponent>()->Version(3);
         }
         }
     }
     }
 
 
@@ -192,17 +192,12 @@ namespace AZ::SceneGenerationComponents
         const AZStd::vector<AZStd::pair<const IMeshData*, NodeIndex>> meshes = [](const SceneGraph& graph)
         const AZStd::vector<AZStd::pair<const IMeshData*, NodeIndex>> meshes = [](const SceneGraph& graph)
         {
         {
             AZStd::vector<AZStd::pair<const IMeshData*, NodeIndex>> meshes;
             AZStd::vector<AZStd::pair<const IMeshData*, NodeIndex>> meshes;
-            for (auto it = graph.GetContentStorage().cbegin(); it != graph.GetContentStorage().cend(); ++it)
+            const auto meshNodes = Containers::MakeDerivedFilterView<IMeshData>(graph.GetContentStorage());
+            for (auto it = meshNodes.cbegin(); it != meshNodes.cend(); ++it)
             {
             {
-                // Skip anything that isn't a mesh.
-                const auto* mesh = azdynamic_cast<const AZ::SceneAPI::DataTypes::IMeshData*>(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.
                 // 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;
             return meshes;
         }(graph);
         }(graph);
@@ -287,6 +282,12 @@ namespace AZ::SceneGenerationComponents
 
 
                 auto [optimizedMesh, optimizedUVs, optimizedTangents, optimizedBitangents, optimizedVertexColors, optimizedSkinWeights] = OptimizeMesh(mesh, mesh, uvDatas, tangentDatas, bitangentDatas, colorDatas, skinWeightDatas, meshGroup, hasBlendShapes);
                 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));
                 const NodeIndex optimizedMeshNodeIndex = graph.AddChild(graph.GetNodeParent(nodeIndex), name.c_str(), AZStd::move(optimizedMesh));
 
 
                 auto addOptimizedNodes = [&graph, &optimizedMeshNodeIndex](const auto& originalNodeIndexes, auto& optimizedNodes)
                 auto addOptimizedNodes = [&graph, &optimizedMeshNodeIndex](const auto& originalNodeIndexes, auto& optimizedNodes)
@@ -433,6 +434,12 @@ namespace AZ::SceneGenerationComponents
         const float weightThreshold = skinRule ? skinRule->GetWeightThreshold() : 0.001f;
         const float weightThreshold = skinRule ? skinRule->GetWeightThreshold() : 0.001f;
         meshBuilder.SetSkinningInfo(ExtractSkinningInfo(meshData, skinWeights, maxWeightsPerVertex, weightThreshold));
         meshBuilder.SetSkinningInfo(ExtractSkinningInfo(meshData, skinWeights, maxWeightsPerVertex, weightThreshold));
 
 
+        constexpr float positionTolerance = 0.0001f;
+        constexpr float positionToleranceReciprocal = 1.0f / positionTolerance;
+        AZStd::unordered_map<AZ::Vector3, AZ::u32> positionMap{};
+
+        AZ::u32 currentOriginalVertexIndex = 0;
+
         // Add the vertex data to all the layers
         // Add the vertex data to all the layers
         const AZ::u32 faceCount = meshData->GetFaceCount();
         const AZ::u32 faceCount = meshData->GetFaceCount();
         for (AZ::u32 faceIndex = 0; faceIndex < faceCount; ++faceIndex)
         for (AZ::u32 faceIndex = 0; faceIndex < faceCount; ++faceIndex)
@@ -440,8 +447,33 @@ namespace AZ::SceneGenerationComponents
             meshBuilder.BeginPolygon(baseMesh->GetFaceMaterialId(faceIndex));
             meshBuilder.BeginPolygon(baseMesh->GetFaceMaterialId(faceIndex));
             for (const AZ::u32 vertexIndex : meshData->GetFaceInfo(faceIndex).vertexIndex)
             for (const AZ::u32 vertexIndex : meshData->GetFaceInfo(faceIndex).vertexIndex)
             {
             {
-                const int orgVertexNumber = meshData->GetUsedPointIndexForControlPoint(meshData->GetControlPointIndex(vertexIndex));
-                AZ_Assert(orgVertexNumber >= 0, "Invalid vertex number");
+                const AZ::u32 orgVertexNumber = [&meshData, &hasBlendShapes, &vertexIndex, &positionMap, &currentOriginalVertexIndex, 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;
+                }();
+
                 orgVtxLayer->SetCurrentVertexValue(orgVertexNumber);
                 orgVtxLayer->SetCurrentVertexValue(orgVertexNumber);
 
 
                 posLayer->SetCurrentVertexValue(meshData->GetPosition(vertexIndex));
                 posLayer->SetCurrentVertexValue(meshData->GetPosition(vertexIndex));

+ 100 - 0
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 <gtest/gtest.h>
+
+#include <AzCore/Component/ComponentApplication.h>
+#include <AzCore/Component/Entity.h>
+#include <AzCore/Jobs/JobManagerComponent.h>
+#include <AzCore/Memory/MemoryComponent.h>
+#include <AzCore/UnitTest/TestTypes.h>
+#include <AzCore/std/smart_ptr/shared_ptr.h>
+#include <AzCore/std/smart_ptr/unique_ptr.h>
+#include <SceneAPI/SceneCore/Containers/Scene.h>
+#include <SceneAPI/SceneCore/Containers/SceneGraph.h>
+#include <SceneAPI/SceneCore/Events/GenerateEventContext.h>
+#include <SceneAPI/SceneCore/Utilities/SceneGraphSelector.h>
+#include <SceneAPI/SceneData/GraphData/MeshData.h>
+#include <SceneAPI/SceneData/Groups/MeshGroup.h>
+#include <Generation/Components/MeshOptimizer/MeshOptimizerComponent.h>
+
+#include <InitSceneAPIFixture.h>
+
+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<AZ::SceneData::GraphData::MeshData>();
+            {
+                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<AZ::SceneAPI::SceneData::MeshGroup>();
+        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<AZ::SceneAPI::DataTypes::IMeshData>(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

+ 1 - 0
Gems/SceneProcessing/Code/sceneprocessing_editor_tests_files.cmake

@@ -7,6 +7,7 @@
 
 
 set(FILES
 set(FILES
     Tests/InitSceneAPIFixture.h
     Tests/InitSceneAPIFixture.h
+    Tests/MeshBuilder/MeshOptimizerComponentTests.cpp
     Tests/MeshBuilder/MeshBuilderTests.cpp
     Tests/MeshBuilder/MeshBuilderTests.cpp
     Tests/MeshBuilder/MeshVerticesTests.cpp
     Tests/MeshBuilder/MeshVerticesTests.cpp
     Tests/MeshBuilder/SkinInfluencesTests.cpp
     Tests/MeshBuilder/SkinInfluencesTests.cpp