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/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 58be460a44..35d6db6d03 100644 --- a/Gems/SceneProcessing/Code/Source/Generation/Components/MeshOptimizer/MeshOptimizerComponent.cpp +++ b/Gems/SceneProcessing/Code/Source/Generation/Components/MeshOptimizer/MeshOptimizerComponent.cpp @@ -97,6 +97,86 @@ 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; + using AZStd::unordered_map::size; + + 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 +187,7 @@ namespace AZ::SceneGenerationComponents auto* serializeContext = azrtti_cast(context); if (serializeContext) { - serializeContext->Class()->Version(2); + serializeContext->Class()->Version(4); } } @@ -116,14 +196,15 @@ namespace AZ::SceneGenerationComponents const MeshDataType* meshData, const SkinWeightDataView& skinWeights, AZ::u32 maxWeightsPerVertex, - float weightThreshold) + float weightThreshold, + const Vector3Map& positionMap) { if (skinWeights.empty()) { return {}; } - const size_t usedControlPointCount = meshData->GetUsedControlPointCount(); + const size_t usedControlPointCount = positionMap.size(); auto skinningInfo = AZStd::make_unique(aznumeric_cast(usedControlPointCount)); @@ -142,15 +223,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; } @@ -193,17 +271,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); @@ -288,6 +361,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) @@ -429,10 +508,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; + Vector3Map positionMap(meshData, hasBlendShapes, positionTolerance); + positionMap.reserve(vertexCount); // Add the vertex data to all the layers const AZ::u32 faceCount = meshData->GetFaceCount(); @@ -441,8 +519,8 @@ 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"); + const AZ::u32 orgVertexNumber = positionMap[vertexIndex]; + orgVtxLayer->SetCurrentVertexValue(orgVertexNumber); posLayer->SetCurrentVertexValue(meshData->GetPosition(vertexIndex)); @@ -472,6 +550,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 new file mode 100644 index 0000000000..81528fc3e4 --- /dev/null +++ b/Gems/SceneProcessing/Code/Tests/MeshBuilder/MeshOptimizerComponentTests.cpp @@ -0,0 +1,221 @@ +/* + * 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 +#include +#include + +#include + +namespace AZ::SceneAPI::DataTypes +{ + void PrintTo(const ISkinWeightData::Link& link, ::std::ostream* os) + { + *os << '{' << link.boneId << ", " << link.weight << '}'; + } +} + +namespace SceneProcessing +{ + class VertexDeduplicationFixture + : public SceneProcessing::InitSceneAPIFixture + { + public: + void SetUp() override + { + SceneProcessing::InitSceneAPIFixture::SetUp(); + + 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); + + 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)); + + 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); + } + + 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 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 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