Merge pull request #2277 from aws-lumberyard-dev/burelc/LYN-4490
[MeshOptimizer] Weld nearby vertices when optimizing meshes
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 <AzCore/std/containers/vector.h>
|
||||
|
||||
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:
|
||||
*
|
||||
* <pre>
|
||||
*
|
||||
* [ROW0]: [E0][E1][E2]
|
||||
* [ROW1]: [E0][E1]
|
||||
* [ROW2]: [E0][E1][E2][E3]
|
||||
* [ROW3]: [E0]
|
||||
*
|
||||
* </pre>
|
||||
*
|
||||
* 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 T>
|
||||
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<TableEntry>& 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<T>& GetData() { return mData; }
|
||||
|
||||
private:
|
||||
AZStd::vector<T> mData; /**< The element data. */
|
||||
AZStd::vector<TableEntry> 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
|
||||
@@ -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 <class T>
|
||||
void Array2D<T>::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 <class T>
|
||||
void Array2D<T>::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 <class T>
|
||||
void Array2D<T>::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 <class T>
|
||||
void Array2D<T>::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 <class T>
|
||||
void Array2D<T>::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 <class T>
|
||||
void Array2D<T>::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 <class T>
|
||||
size_t Array2D<T>::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 <class T>
|
||||
void Array2D<T>::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<T> 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]);
|
||||
}
|
||||
}
|
||||
+7
-4
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+16
-11
@@ -10,8 +10,8 @@
|
||||
|
||||
#include <AzCore/Memory/Memory.h>
|
||||
#include <AzCore/base.h>
|
||||
#include <AzCore/std/containers/vector.h>
|
||||
#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<Influence>& influences);
|
||||
|
||||
public:
|
||||
MCore::Array2D<Influence> mInfluences;
|
||||
private:
|
||||
AZStd::vector<AZStd::vector<Influence>> mInfluences;
|
||||
};
|
||||
} // namespace AZ::MeshBuilder
|
||||
|
||||
+107
-23
@@ -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 MeshDataType>
|
||||
class Vector3Map
|
||||
: private AZStd::unordered_map<AZ::Vector3, AZ::u32>
|
||||
{
|
||||
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<AZ::Vector3, AZ::u32>::reserve;
|
||||
using AZStd::unordered_map<AZ::Vector3, AZ::u32>::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<class MeshDataType>
|
||||
Vector3Map(const MeshDataType*) -> Vector3Map<const MeshDataType>;
|
||||
|
||||
MeshOptimizerComponent::MeshOptimizerComponent()
|
||||
{
|
||||
BindToCall(&MeshOptimizerComponent::OptimizeMeshes);
|
||||
@@ -107,7 +187,7 @@ namespace AZ::SceneGenerationComponents
|
||||
auto* serializeContext = azrtti_cast<AZ::SerializeContext*>(context);
|
||||
if (serializeContext)
|
||||
{
|
||||
serializeContext->Class<MeshOptimizerComponent, GenerationComponent>()->Version(2);
|
||||
serializeContext->Class<MeshOptimizerComponent, GenerationComponent>()->Version(4);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,14 +196,15 @@ namespace AZ::SceneGenerationComponents
|
||||
const MeshDataType* meshData,
|
||||
const SkinWeightDataView& skinWeights,
|
||||
AZ::u32 maxWeightsPerVertex,
|
||||
float weightThreshold)
|
||||
float weightThreshold,
|
||||
const Vector3Map<MeshDataType>& positionMap)
|
||||
{
|
||||
if (skinWeights.empty())
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
const size_t usedControlPointCount = meshData->GetUsedControlPointCount();
|
||||
const size_t usedControlPointCount = positionMap.size();
|
||||
|
||||
auto skinningInfo = AZStd::make_unique<AZ::MeshBuilder::MeshBuilderSkinningInfo>(aznumeric_cast<AZ::u32>(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<AZStd::pair<const IMeshData*, NodeIndex>> meshes = [](const SceneGraph& graph)
|
||||
{
|
||||
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.
|
||||
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<MeshBuilder::MeshBuilderVertexAttributeLayerVector3*> bitangentLayers = makeLayersForData(bitangents);
|
||||
const AZStd::vector<MeshBuilder::MeshBuilderVertexAttributeLayerColor*> vertexColorLayers = makeLayersForData(vertexColors);
|
||||
|
||||
const auto* skinRule = meshGroup.GetRuleContainerConst().FindFirstByType<SceneAPI::DataTypes::ISkinRule>().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<SceneAPI::DataTypes::ISkinRule>().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
|
||||
|
||||
@@ -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 <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/DataTypes/GraphData/IMeshData.h>
|
||||
#include <SceneAPI/SceneCore/DataTypes/GraphData/ISkinWeightData.h>
|
||||
#include <SceneAPI/SceneCore/Events/GenerateEventContext.h>
|
||||
#include <SceneAPI/SceneCore/Utilities/SceneGraphSelector.h>
|
||||
#include <SceneAPI/SceneData/GraphData/MeshData.h>
|
||||
#include <SceneAPI/SceneData/GraphData/SkinWeightData.h>
|
||||
#include <SceneAPI/SceneData/Groups/MeshGroup.h>
|
||||
#include <Generation/Components/MeshOptimizer/MeshOptimizerComponent.h>
|
||||
|
||||
#include <InitSceneAPIFixture.h>
|
||||
|
||||
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<AZ::SceneAPI::DataTypes::IMeshData> 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<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);
|
||||
|
||||
return mesh;
|
||||
}
|
||||
|
||||
static AZStd::unique_ptr<AZ::SceneData::GraphData::SkinWeightData> MakeSkinData()
|
||||
{
|
||||
auto skinWeights = AZStd::make_unique<AZ::SceneData::GraphData::SkinWeightData>();
|
||||
|
||||
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<AZ::SceneAPI::DataTypes::IMeshData>(graph.GetNodeContent(meshNodeIndex))->GetVertexCount(), 6);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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<AZ::SceneAPI::DataTypes::IMeshData>(graph.GetNodeContent(meshNodeIndex))->GetVertexCount(), 6);
|
||||
|
||||
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);
|
||||
|
||||
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<AZ::SceneAPI::DataTypes::ISkinWeightData>(graph.GetNodeContent(optimizedSkinDataNodeIndex));
|
||||
ASSERT_TRUE(optimizedSkinWeights);
|
||||
|
||||
const AZStd::vector<AZStd::vector<AZ::SceneData::GraphData::SkinWeightData::Link>> 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<AZStd::vector<AZ::SceneData::GraphData::SkinWeightData::Link>> 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
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user