From 70bd3ea0ff0c877d866008a9f5d298aad8b1c6a3 Mon Sep 17 00:00:00 2001 From: Aaron Ruiz Mora Date: Tue, 4 May 2021 12:09:56 +0100 Subject: [PATCH] Performance pass to Cloth CPU Skinning - Added operator+(Matrix3x4), operator*(float), RetrieveScaleSq and GetReciprocalScaled to Matrix3x4. Used by Cloth CPU Linear Skinning. These operation will be performant as they use SIMD. - Modified so there are no virtual functions calls at vertex level. - Caching indices to simplify the loop when applying skinning. - Caching static variables Matrix3x4 zero and DualQuaternion zero to avoid creating it for every vertex. - Removing branching to skip joints when the weight is zero, these cases are rarely and this improves performance by removing branching from loops at vertex level. - Changing skinning influences so it's a continuous block of memory. - Caching the vector size() if a variable instead of directly using it in a for loop. --- Code/Framework/AzCore/AzCore/Math/Matrix3x4.h | 18 + .../AzCore/AzCore/Math/Matrix3x4.inl | 51 +++ .../AzCore/Tests/Math/Matrix3x4Tests.cpp | 60 +++ .../ClothComponentMesh/ActorClothSkinning.cpp | 375 ++++++++++-------- .../ClothComponentMesh/ActorClothSkinning.h | 55 ++- .../ClothComponentMesh/ClothComponentMesh.cpp | 15 +- .../ActorClothSkinningTest.cpp | 24 +- 7 files changed, 388 insertions(+), 210 deletions(-) diff --git a/Code/Framework/AzCore/AzCore/Math/Matrix3x4.h b/Code/Framework/AzCore/AzCore/Math/Matrix3x4.h index 8773d4ea6a..09f443c1cf 100644 --- a/Code/Framework/AzCore/AzCore/Math/Matrix3x4.h +++ b/Code/Framework/AzCore/AzCore/Math/Matrix3x4.h @@ -231,6 +231,18 @@ namespace AZ //! Compound assignment operator for matrix-matrix multiplication. Matrix3x4& operator*=(const Matrix3x4& rhs); + //! Operator for matrix-matrix addition. + [[nodiscard]] Matrix3x4 operator+(const Matrix3x4& rhs) const; + + //! Compound assignment operator for matrix-matrix addition. + Matrix3x4& operator+=(const Matrix3x4& rhs); + + //! Operator for multiplying all matrix's elements with a scalar + [[nodiscard]] Matrix3x4 operator*(float scalar) const; + + //! Compound assignment operator for multiplying all matrix's elements with a scalar + Matrix3x4& operator*=(float scalar); + //! Operator for transforming a Vector3. [[nodiscard]] Vector3 operator*(const Vector3& rhs) const; @@ -274,12 +286,18 @@ namespace AZ //! Gets the scale part of the transformation (the length of the basis vectors). [[nodiscard]] Vector3 RetrieveScale() const; + //! Gets the squared scale part of the transformation (the squared length of the basis vectors). + [[nodiscard]] Vector3 RetrieveScaleSq() const; + //! Gets the scale part of the transformation as in RetrieveScale, and also removes this scaling from the matrix. Vector3 ExtractScale(); //! Multiplies the basis vectors of the matrix by the elements of the scale specified. void MultiplyByScale(const Vector3& scale); + //! Returns a matrix with the reciprocal scale, keeping the same rotation and translation. + [[nodiscard]] Matrix3x4 GetReciprocalScaled() const; + //! Tests if the 3x3 part of the matrix is orthogonal. bool IsOrthogonal(float tolerance = Constants::Tolerance) const; diff --git a/Code/Framework/AzCore/AzCore/Math/Matrix3x4.inl b/Code/Framework/AzCore/AzCore/Math/Matrix3x4.inl index bd310d293e..232e8a5b30 100644 --- a/Code/Framework/AzCore/AzCore/Math/Matrix3x4.inl +++ b/Code/Framework/AzCore/AzCore/Math/Matrix3x4.inl @@ -487,6 +487,43 @@ namespace AZ } + AZ_MATH_INLINE Matrix3x4 Matrix3x4::operator+(const Matrix3x4& rhs) const + { + return Matrix3x4 + ( + Simd::Vec4::Add(m_rows[0].GetSimdValue(), rhs.m_rows[0].GetSimdValue()), + Simd::Vec4::Add(m_rows[1].GetSimdValue(), rhs.m_rows[1].GetSimdValue()), + Simd::Vec4::Add(m_rows[2].GetSimdValue(), rhs.m_rows[2].GetSimdValue()) + ); + } + + + AZ_MATH_INLINE Matrix3x4& Matrix3x4::operator+=(const Matrix3x4& rhs) + { + *this = *this + rhs; + return *this; + } + + + AZ_MATH_INLINE Matrix3x4 Matrix3x4::operator*(float scalar) const + { + const Vector4 vector4Scalar(scalar); + return Matrix3x4 + ( + Simd::Vec4::Mul(m_rows[0].GetSimdValue(), vector4Scalar.GetSimdValue()), + Simd::Vec4::Mul(m_rows[1].GetSimdValue(), vector4Scalar.GetSimdValue()), + Simd::Vec4::Mul(m_rows[2].GetSimdValue(), vector4Scalar.GetSimdValue()) + ); + } + + + AZ_MATH_INLINE Matrix3x4& Matrix3x4::operator*=(float scalar) + { + *this = *this * scalar; + return *this; + } + + AZ_MATH_INLINE Vector3 Matrix3x4::operator*(const Vector3& rhs) const { return Vector3 @@ -583,6 +620,12 @@ namespace AZ } + AZ_MATH_INLINE Vector3 Matrix3x4::RetrieveScaleSq() const + { + return Vector3(GetColumn(0).GetLengthSq(), GetColumn(1).GetLengthSq(), GetColumn(2).GetLengthSq()); + } + + AZ_MATH_INLINE Vector3 Matrix3x4::ExtractScale() { const Vector3 scale = RetrieveScale(); @@ -600,6 +643,14 @@ namespace AZ } + AZ_MATH_INLINE Matrix3x4 Matrix3x4::GetReciprocalScaled() const + { + Matrix3x4 result = *this; + result.MultiplyByScale(RetrieveScaleSq().GetReciprocal()); + return result; + } + + AZ_MATH_INLINE void Matrix3x4::Orthogonalize() { *this = GetOrthogonalized(); diff --git a/Code/Framework/AzCore/Tests/Math/Matrix3x4Tests.cpp b/Code/Framework/AzCore/Tests/Math/Matrix3x4Tests.cpp index f61b633fc2..353b46874b 100644 --- a/Code/Framework/AzCore/Tests/Math/Matrix3x4Tests.cpp +++ b/Code/Framework/AzCore/Tests/Math/Matrix3x4Tests.cpp @@ -484,6 +484,38 @@ namespace UnitTest EXPECT_TRUE(matrix5.IsClose(matrix1 * matrix4)); } + TEST(MATH_Matrix3x4, AddMatrix3x4) + { + const AZ::Matrix3x4 matrix1 = AZ::Matrix3x4::CreateFromValue(1.2f); + const AZ::Matrix3x4 matrix2 = AZ::Matrix3x4::CreateDiagonal(AZ::Vector3(1.3f, 1.5f, 0.4f)); + const AZ::Matrix3x4 matrix3 = AZ::Matrix3x4::CreateFromQuaternionAndTranslation( + AZ::Quaternion(0.42f, 0.46f, -0.66f, 0.42f), AZ::Vector3(2.8f, -3.7f, 1.6f)); + const AZ::Matrix3x4 matrix4 = AZ::Matrix3x4::CreateRotationX(-0.7f) * AZ::Matrix3x4::CreateScale(AZ::Vector3(0.6f, 1.3f, 0.7f)); + AZ::Matrix3x4 matrix5 = matrix1; + matrix5 += matrix4; + EXPECT_THAT(matrix1 + (matrix2 + matrix3), IsClose((matrix1 + matrix2) + matrix3)); + EXPECT_THAT(matrix2 + AZ::Matrix3x4::CreateZero(), IsClose(matrix2)); + EXPECT_THAT(matrix3 + AZ::Matrix3x4::CreateZero(), IsClose(AZ::Matrix3x4::CreateZero() + matrix3)); + EXPECT_THAT(matrix3 + matrix3, IsClose(matrix3 * 2.0f)); + EXPECT_THAT(matrix5, IsClose(matrix1 + matrix4)); + } + + TEST(MATH_Matrix3x4, MultiplyByScalar) + { + const AZ::Vector4 row0(1.488f, 2.56f, 0.096f, 2.3f); + const AZ::Vector4 row1(0.384f, -1.92f, 0.428f, -1.6f); + const AZ::Vector4 row2(1.28f, -2.4f, -0.24f, 3.7f); + const float scalar = 3.2f; + const AZ::Vector4 row0Result = row0 * scalar; + const AZ::Vector4 row1Result = row1 * scalar; + const AZ::Vector4 row2Result = row2 * scalar; + AZ::Matrix3x4 matrix = AZ::Matrix3x4::CreateFromRows(row0, row1, row2); + EXPECT_THAT(matrix * 0.0f, IsClose(AZ::Matrix3x4::CreateZero())); + EXPECT_THAT(matrix * 1.0f, IsClose(matrix)); + EXPECT_THAT(matrix * scalar, IsClose(AZ::Matrix3x4::CreateFromRows(row0Result, row1Result, row2Result))); + EXPECT_THAT(matrix * 2.0f, IsClose(matrix + matrix)); + } + TEST(MATH_Matrix3x4, MultiplyByVector3) { const AZ::Vector4 row0(1.488f, 2.56f, 0.096f, 2.3f); @@ -652,6 +684,34 @@ namespace UnitTest EXPECT_THAT(scaledMatrix.RetrieveScale(), IsClose(AZ::Vector3::CreateOne())); } + TEST_P(Matrix3x4ScaleFixture, ScaleSq) + { + const AZ::Matrix3x4 orthogonalMatrix = GetParam(); + EXPECT_THAT(orthogonalMatrix.RetrieveScaleSq(), IsClose(AZ::Vector3::CreateOne())); + AZ::Matrix3x4 unscaledMatrix = orthogonalMatrix; + unscaledMatrix.ExtractScale(); + EXPECT_THAT(unscaledMatrix.RetrieveScaleSq(), IsClose(AZ::Vector3::CreateOne())); + const AZ::Vector3 scale(2.8f, 0.7f, 1.3f); + AZ::Matrix3x4 scaledMatrix = orthogonalMatrix; + scaledMatrix.MultiplyByScale(scale); + EXPECT_THAT(scaledMatrix.RetrieveScaleSq(), IsClose(scale * scale)); + EXPECT_THAT(scaledMatrix.RetrieveScaleSq(), IsClose(scaledMatrix.RetrieveScale() * scaledMatrix.RetrieveScale())); + scaledMatrix.ExtractScale(); + EXPECT_THAT(scaledMatrix.RetrieveScaleSq(), IsClose(AZ::Vector3::CreateOne())); + } + + TEST_P(Matrix3x4ScaleFixture, GetReciprocalScaled) + { + const AZ::Matrix3x4 orthogonalMatrix = GetParam(); + EXPECT_THAT(orthogonalMatrix.GetReciprocalScaled(), IsClose(orthogonalMatrix)); + const AZ::Vector3 scale(2.8f, 0.7f, 1.3f); + AZ::Matrix3x4 scaledMatrix = orthogonalMatrix; + scaledMatrix.MultiplyByScale(scale); + AZ::Matrix3x4 reciprocalScaledMatrix = orthogonalMatrix; + reciprocalScaledMatrix.MultiplyByScale(scale.GetReciprocal()); + EXPECT_THAT(scaledMatrix.GetReciprocalScaled(), IsClose(reciprocalScaledMatrix)); + } + INSTANTIATE_TEST_CASE_P(MATH_Matrix3x4, Matrix3x4ScaleFixture, ::testing::ValuesIn(MathTestData::OrthogonalMatrix3x4s)); TEST(MATH_Matrix3x4, IsOrthogonal) diff --git a/Gems/NvCloth/Code/Source/Components/ClothComponentMesh/ActorClothSkinning.cpp b/Gems/NvCloth/Code/Source/Components/ClothComponentMesh/ActorClothSkinning.cpp index c295eca188..8f97b212a3 100644 --- a/Gems/NvCloth/Code/Source/Components/ClothComponentMesh/ActorClothSkinning.cpp +++ b/Gems/NvCloth/Code/Source/Components/ClothComponentMesh/ActorClothSkinning.cpp @@ -27,11 +27,11 @@ namespace NvCloth { namespace Internal { - bool ObtainSkinningData( + bool ObtainSkinningInfluences( AZ::EntityId entityId, const MeshNodeInfo& meshNodeInfo, - const size_t numSimParticles, - AZStd::vector& skinningData) + const size_t numVertices, + AZStd::vector& skinningInfluences) { AZ::Data::Asset modelAsset; AZ::Render::MeshComponentRequestBus::EventResult( @@ -67,7 +67,7 @@ namespace NvCloth const auto& skinToSkeletonIndexMap = actor->GetSkinToSkeletonIndexMap(); - skinningData.resize(numSimParticles); + size_t numberOfInfluencesPerVertex = 0; // For each submesh... for (const auto& subMeshInfo : meshNodeInfo.m_subMeshes) @@ -98,28 +98,48 @@ namespace NvCloth if (sourceSkinJointIndices.empty() || sourceSkinWeights.empty()) { - continue; + // Ignoring skinning when there is no skin data. + // All submeshes will either have or not have skin data, since they come from the same mesh. + return false; } AZ_Assert(sourceSkinJointIndices.size() == sourceSkinWeights.size(), "Size of skin joint indices buffer (%zu) different from skin weights buffer (%zu)", sourceSkinJointIndices.size(), sourceSkinWeights.size()); - const size_t influenceCount = sourceSkinWeights.size() / sourcePositions.size(); - if (influenceCount == 0) + const size_t subMeshInfluenceCount = sourceSkinWeights.size() / sourcePositions.size(); + AZ_Assert(subMeshInfluenceCount > 0, + "Submesh %d skinning data has zero joint influences per vertex.", + subMeshInfo.m_primitiveIndex); + + if (numberOfInfluencesPerVertex == 0) { - continue; + // Resize only in the first loop once we know the number of influences per vertex. + // The other submeshes should match the number of influences. + numberOfInfluencesPerVertex = subMeshInfluenceCount; + skinningInfluences.resize(numVertices * numberOfInfluencesPerVertex); + } + else if (subMeshInfluenceCount != numberOfInfluencesPerVertex) + { + AZ_Error("ActorClothSkinning", false, + "Submesh %d number of influences (%d) is different from a previous submesh (%d).", + subMeshInfo.m_primitiveIndex, + subMeshInfluenceCount, + numberOfInfluencesPerVertex); + return false; } for (int vertexIndex = 0; vertexIndex < subMeshInfo.m_numVertices; ++vertexIndex) { - SkinningInfo& skinningInfo = skinningData[subMeshInfo.m_verticesFirstIndex + vertexIndex]; - skinningInfo.m_jointIndices.resize(influenceCount); - skinningInfo.m_jointWeights.resize(influenceCount); + const size_t subMeshVertexIndex = vertexIndex * numberOfInfluencesPerVertex; + const size_t meshVertexIndex = (subMeshInfo.m_verticesFirstIndex + vertexIndex) * numberOfInfluencesPerVertex; - for (size_t influenceIndex = 0; influenceIndex < influenceCount; ++influenceIndex) + for (size_t influenceIndex = 0; influenceIndex < numberOfInfluencesPerVertex; ++influenceIndex) { - const AZ::u16 jointIndex = sourceSkinJointIndices[vertexIndex * influenceCount + influenceIndex]; - const float weight = sourceSkinWeights[vertexIndex * influenceCount + influenceIndex]; + const size_t subMeshVertexInfluenceIndex = subMeshVertexIndex + influenceIndex; + const size_t meshVertexInfluenceIndex = meshVertexIndex + influenceIndex; + + const AZ::u16 jointIndex = sourceSkinJointIndices[subMeshVertexInfluenceIndex]; + const float weight = sourceSkinWeights[subMeshVertexInfluenceIndex]; auto skeletonIndexIt = skinToSkeletonIndexMap.find(jointIndex); if (skeletonIndexIt == skinToSkeletonIndexMap.end()) @@ -130,8 +150,8 @@ namespace NvCloth return false; } - skinningInfo.m_jointIndices[influenceIndex] = skeletonIndexIt->second; - skinningInfo.m_jointWeights[influenceIndex] = weight; + skinningInfluences[meshVertexInfluenceIndex].m_jointIndex = skeletonIndexIt->second; + skinningInfluences[meshVertexInfluenceIndex].m_jointWeight = weight; } } } @@ -198,17 +218,21 @@ namespace NvCloth { } - protected: // ActorClothSkinning overrides ... void UpdateSkinning() override; - bool HasSkinningTransformData() override; - void ComputeVertexSkinnningTransform(const SkinningInfo& skinningInfo) override; - AZ::Vector3 ComputeSkinningPosition(const AZ::Vector3& originalPosition) override; - AZ::Vector3 ComputeSkinningVector(const AZ::Vector3& originalVector) override; + void ApplySkinning( + const AZStd::vector& originalPositions, + AZStd::vector& positions) override; + void ApplySkinningOnNonSimulatedVertices( + const MeshClothInfo& originalData, + ClothComponentMesh::RenderData& renderData) override; private: + AZ::Matrix3x4 ComputeVertexSkinnningTransform(AZ::u32 vertexIndex); + const AZ::Matrix3x4* m_skinningMatrices = nullptr; - AZ::Matrix3x4 m_vertexSkinningTransform = AZ::Matrix3x4::CreateIdentity(); + + inline static const AZ::Matrix3x4 s_zeroMatrix3x4 = AZ::Matrix3x4::CreateZero(); }; void ActorClothSkinningLinear::UpdateSkinning() @@ -218,41 +242,75 @@ namespace NvCloth m_skinningMatrices = Internal::ObtainSkinningMatrices(m_entityId); } - bool ActorClothSkinningLinear::HasSkinningTransformData() + void ActorClothSkinningLinear::ApplySkinning( + const AZStd::vector& originalPositions, + AZStd::vector& positions) { - return m_skinningMatrices != nullptr; - } - - void ActorClothSkinningLinear::ComputeVertexSkinnningTransform(const SkinningInfo& skinningInfo) - { - m_vertexSkinningTransform = AZ::Matrix3x4::CreateZero(); - for (size_t weightIndex = 0; weightIndex < skinningInfo.m_jointWeights.size(); ++weightIndex) + if (!m_skinningMatrices || + originalPositions.empty() || + originalPositions.size() != positions.size() || + originalPositions.size() != m_simulatedVertices.size()) { - const AZ::u16 jointIndex = skinningInfo.m_jointIndices[weightIndex]; - const float jointWeight = skinningInfo.m_jointWeights[weightIndex]; + return; + } - if (AZ::IsClose(jointWeight, 0.0f)) - { - continue; - } + AZ_PROFILE_FUNCTION(AZ::Debug::ProfileCategory::Cloth); - // Blending matrices the same way done in GPU shaders, by adding each weighted matrix element by element. - // This way the skinning results are much similar to the skinning performed in GPU. - for (int i = 0; i < 3; ++i) - { - m_vertexSkinningTransform.SetRow(i, m_vertexSkinningTransform.GetRow(i) + m_skinningMatrices[jointIndex].GetRow(i) * jointWeight); - } + const size_t vertexCount = m_simulatedVertices.size(); + for (size_t index = 0; index < vertexCount; ++index) + { + const AZ::Matrix3x4 vertexSkinningTransform = ComputeVertexSkinnningTransform(m_simulatedVertices[index]); + + const AZ::Vector3 skinnedPosition = vertexSkinningTransform * originalPositions[index].GetAsVector3(); + positions[index].Set(skinnedPosition, positions[index].GetW()); // Avoid overwriting the w component } } - AZ::Vector3 ActorClothSkinningLinear::ComputeSkinningPosition(const AZ::Vector3& originalPosition) + void ActorClothSkinningLinear::ApplySkinningOnNonSimulatedVertices( + const MeshClothInfo& originalData, + ClothComponentMesh::RenderData& renderData) { - return m_vertexSkinningTransform * originalPosition; + if (!m_skinningMatrices || + originalData.m_particles.empty() || + originalData.m_particles.size() != renderData.m_particles.size() || + originalData.m_particles.size() != m_skinningInfluences.size() / m_numberOfInfluencesPerVertex) + { + return; + } + + AZ_PROFILE_FUNCTION(AZ::Debug::ProfileCategory::Cloth); + + for (const AZ::u32 index : m_nonSimulatedVertices) + { + const AZ::Matrix3x4 vertexSkinningTransform = ComputeVertexSkinnningTransform(index); + + const AZ::Vector3 skinnedPosition = vertexSkinningTransform * originalData.m_particles[index].GetAsVector3(); + renderData.m_particles[index].Set(skinnedPosition, renderData.m_particles[index].GetW()); // Avoid overwriting the w component + + // Calculate the reciprocal scale version of the matrix to transform the vectors. + const AZ::Matrix3x4 vertexSkinningTransformReciprocalScale = vertexSkinningTransform.GetReciprocalScaled(); + + renderData.m_tangents[index] = vertexSkinningTransformReciprocalScale.TransformVector(originalData.m_tangents[index]).GetNormalized(); + renderData.m_bitangents[index] = vertexSkinningTransformReciprocalScale.TransformVector(originalData.m_bitangents[index]).GetNormalized(); + renderData.m_normals[index] = vertexSkinningTransformReciprocalScale.TransformVector(originalData.m_normals[index]).GetNormalized(); + } } - AZ::Vector3 ActorClothSkinningLinear::ComputeSkinningVector(const AZ::Vector3& originalVector) + AZ::Matrix3x4 ActorClothSkinningLinear::ComputeVertexSkinnningTransform(AZ::u32 vertexIndex) { - return (m_vertexSkinningTransform * AZ::Vector4::CreateFromVector3AndFloat(originalVector, 0.0f)).GetAsVector3().GetNormalized(); + AZ::Matrix3x4 vertexSkinningTransform = s_zeroMatrix3x4; + for (size_t influenceIndex = 0; influenceIndex < m_numberOfInfluencesPerVertex; ++influenceIndex) + { + const size_t vertexInfluenceIndex = vertexIndex * m_numberOfInfluencesPerVertex + influenceIndex; + + const AZ::u16 jointIndex = m_skinningInfluences[vertexInfluenceIndex].m_jointIndex; + const float jointWeight = m_skinningInfluences[vertexInfluenceIndex].m_jointWeight; + + // Blending matrices the same way done in GPU shaders, by adding each weighted matrix element by element. + // This way the skinning results are much similar to the skinning performed in GPU. + vertexSkinningTransform += m_skinningMatrices[jointIndex] * jointWeight; + } + return vertexSkinningTransform; } // Specialized class that applies dual quaternion blending skinning @@ -265,17 +323,21 @@ namespace NvCloth { } - protected: // ActorClothSkinning overrides ... void UpdateSkinning() override; - bool HasSkinningTransformData() override; - void ComputeVertexSkinnningTransform(const SkinningInfo& skinningInfo) override; - AZ::Vector3 ComputeSkinningPosition(const AZ::Vector3& originalPosition) override; - AZ::Vector3 ComputeSkinningVector(const AZ::Vector3& originalVector) override; + void ApplySkinning( + const AZStd::vector& originalPositions, + AZStd::vector& positions) override; + void ApplySkinningOnNonSimulatedVertices( + const MeshClothInfo& originalData, + ClothComponentMesh::RenderData& renderData) override; private: + MCore::DualQuaternion ComputeVertexSkinnningTransform(AZ::u32 vertexIndex); + AZStd::unordered_map m_skinningDualQuaternions; - MCore::DualQuaternion m_vertexSkinningTransform; + + inline static const MCore::DualQuaternion s_zeroDualQuaternion = MCore::DualQuaternion(AZ::Quaternion::CreateZero(), AZ::Quaternion::CreateZero()); }; void ActorClothSkinningDualQuaternion::UpdateSkinning() @@ -285,58 +347,93 @@ namespace NvCloth m_skinningDualQuaternions = Internal::ObtainSkinningDualQuaternions(m_entityId, m_jointIndices); } - bool ActorClothSkinningDualQuaternion::HasSkinningTransformData() + void ActorClothSkinningDualQuaternion::ApplySkinning( + const AZStd::vector& originalPositions, + AZStd::vector& positions) { - return !m_skinningDualQuaternions.empty(); - } - - void ActorClothSkinningDualQuaternion::ComputeVertexSkinnningTransform(const SkinningInfo& skinningInfo) - { - m_vertexSkinningTransform = MCore::DualQuaternion(AZ::Quaternion::CreateZero(), AZ::Quaternion::CreateZero()); - for (size_t weightIndex = 0; weightIndex < skinningInfo.m_jointWeights.size(); ++weightIndex) + if (m_skinningDualQuaternions.empty() || + originalPositions.empty() || + originalPositions.size() != positions.size() || + originalPositions.size() != m_simulatedVertices.size()) { - const AZ::u16 jointIndex = skinningInfo.m_jointIndices[weightIndex]; - const float jointWeight = skinningInfo.m_jointWeights[weightIndex]; - - if (AZ::IsClose(jointWeight, 0.0f)) - { - continue; - } - - m_vertexSkinningTransform += m_skinningDualQuaternions.at(jointIndex) * jointWeight; + return; + } + + AZ_PROFILE_FUNCTION(AZ::Debug::ProfileCategory::Cloth); + + const size_t vertexCount = m_simulatedVertices.size(); + for (size_t index = 0; index < vertexCount; ++index) + { + const MCore::DualQuaternion vertexSkinningTransform = ComputeVertexSkinnningTransform(m_simulatedVertices[index]); + + const AZ::Vector3 skinnedPosition = vertexSkinningTransform.TransformPoint(originalPositions[index].GetAsVector3()); + positions[index].Set(skinnedPosition, positions[index].GetW()); // Avoid overwriting the w component } - m_vertexSkinningTransform.Normalize(); } - AZ::Vector3 ActorClothSkinningDualQuaternion::ComputeSkinningPosition(const AZ::Vector3& originalPosition) + void ActorClothSkinningDualQuaternion::ApplySkinningOnNonSimulatedVertices( + const MeshClothInfo& originalData, + ClothComponentMesh::RenderData& renderData) { - return m_vertexSkinningTransform.TransformPoint(originalPosition); + if (m_skinningDualQuaternions.empty() || + originalData.m_particles.empty() || + originalData.m_particles.size() != renderData.m_particles.size() || + originalData.m_particles.size() != m_skinningInfluences.size() / m_numberOfInfluencesPerVertex) + { + return; + } + + AZ_PROFILE_FUNCTION(AZ::Debug::ProfileCategory::Cloth); + + for (const AZ::u32 index : m_nonSimulatedVertices) + { + const MCore::DualQuaternion vertexSkinningTransform = ComputeVertexSkinnningTransform(index); + + const AZ::Vector3 skinnedPosition = vertexSkinningTransform.TransformPoint(originalData.m_particles[index].GetAsVector3()); + renderData.m_particles[index].Set(skinnedPosition, renderData.m_particles[index].GetW()); // Avoid overwriting the w component + + // ComputeVertexSkinnningTransform is normalizing the dual quaternion, so it won't have scale + // and there is no need to compute the reciprocal scale version for transforming vectors. + + renderData.m_tangents[index] = vertexSkinningTransform.TransformVector(originalData.m_tangents[index]).GetNormalized(); + renderData.m_bitangents[index] = vertexSkinningTransform.TransformVector(originalData.m_bitangents[index]).GetNormalized(); + renderData.m_normals[index] = vertexSkinningTransform.TransformVector(originalData.m_normals[index]).GetNormalized(); + } } - AZ::Vector3 ActorClothSkinningDualQuaternion::ComputeSkinningVector(const AZ::Vector3& originalVector) + MCore::DualQuaternion ActorClothSkinningDualQuaternion::ComputeVertexSkinnningTransform(AZ::u32 vertexIndex) { - return m_vertexSkinningTransform.TransformVector(originalVector).GetNormalized(); + MCore::DualQuaternion vertexSkinningTransform = s_zeroDualQuaternion; + for (size_t influenceIndex = 0; influenceIndex < m_numberOfInfluencesPerVertex; ++influenceIndex) + { + const size_t vertexInfluenceIndex = vertexIndex * m_numberOfInfluencesPerVertex + influenceIndex; + + const AZ::u16 jointIndex = m_skinningInfluences[vertexInfluenceIndex].m_jointIndex; + const float jointWeight = m_skinningInfluences[vertexInfluenceIndex].m_jointWeight; + + const MCore::DualQuaternion& skinningDualQuaternion = m_skinningDualQuaternions.at(jointIndex); + + float flip = AZ::GetSign(vertexSkinningTransform.mReal.Dot(skinningDualQuaternion.mReal)); + vertexSkinningTransform += skinningDualQuaternion * jointWeight * flip; + } + // Normalizing the dual quaternion as the GPU shaders do. This will remove the scale from the transform. + vertexSkinningTransform.Normalize(); + return vertexSkinningTransform; } AZStd::unique_ptr ActorClothSkinning::Create( AZ::EntityId entityId, const MeshNodeInfo& meshNodeInfo, - const size_t numSimParticles) + const size_t numVertices, + const size_t numSimulatedVertices, + const AZStd::vector& meshRemappedVertices) { - AZStd::vector skinningData; - if (!Internal::ObtainSkinningData(entityId, meshNodeInfo, numSimParticles, skinningData)) + AZStd::vector skinningInfluences; + if (!Internal::ObtainSkinningInfluences(entityId, meshNodeInfo, numVertices, skinningInfluences)) { return nullptr; } - if (numSimParticles != skinningData.size()) - { - AZ_Error("ActorClothSkinning", false, - "Number of simulation particles (%zu) doesn't match with skinning data obtained (%zu)", - numSimParticles, skinningData.size()); - return nullptr; - } - AZStd::unique_ptr actorClothSkinning; const auto skinningMethod = Internal::ObtainSkinningMethod(entityId); switch (skinningMethod) @@ -355,27 +452,40 @@ namespace NvCloth return nullptr; } - // Insert the indices of the joints that influence the particle (weight is not 0) - AZStd::set jointIndices; - for (size_t particleIndex = 0; particleIndex < numSimParticles; ++particleIndex) + actorClothSkinning->m_numberOfInfluencesPerVertex = skinningInfluences.size() / numVertices; + if (actorClothSkinning->m_numberOfInfluencesPerVertex == 0) { - const SkinningInfo& skinningInfo = skinningData[particleIndex]; - for (size_t weightIndex = 0; weightIndex < skinningInfo.m_jointWeights.size(); ++weightIndex) - { - const AZ::u16 jointIndex = skinningInfo.m_jointIndices[weightIndex]; - const float jointWeight = skinningInfo.m_jointWeights[weightIndex]; + AZ_Error("ActorClothSkinning", false, + "Number of skinning joint influences per vertex is zero."); + return nullptr; + } - if (AZ::IsClose(jointWeight, 0.0f)) - { - continue; - } - - jointIndices.insert(jointIndex); - } + // Collect all indices of the joints that influence the vertices + AZStd::set jointIndices; + for (const auto& skinningInfluence : skinningInfluences) + { + jointIndices.insert(skinningInfluence.m_jointIndex); } actorClothSkinning->m_jointIndices.assign(jointIndices.begin(), jointIndices.end()); - actorClothSkinning->m_skinningData = AZStd::move(skinningData); + // Collect the indices for simulated and non-simulated vertices + actorClothSkinning->m_simulatedVertices.resize(numSimulatedVertices); + actorClothSkinning->m_nonSimulatedVertices.reserve(numVertices); + for (size_t vertexIndex = 0; vertexIndex < numVertices; ++vertexIndex) + { + const int remappedIndex = meshRemappedVertices[vertexIndex]; + if (remappedIndex >= 0) + { + actorClothSkinning->m_simulatedVertices[remappedIndex] = vertexIndex; + } + else + { + actorClothSkinning->m_nonSimulatedVertices.emplace_back(vertexIndex); + } + } + actorClothSkinning->m_nonSimulatedVertices.shrink_to_fit(); + + actorClothSkinning->m_skinningInfluences = AZStd::move(skinningInfluences); return actorClothSkinning; } @@ -385,69 +495,6 @@ namespace NvCloth { } - void ActorClothSkinning::ApplySkinning( - const AZStd::vector& originalPositions, - AZStd::vector& positions, - const AZStd::vector& meshRemappedVertices) - { - if (!HasSkinningTransformData() || - originalPositions.empty() || - originalPositions.size() != positions.size() || - m_skinningData.size() != meshRemappedVertices.size()) - { - return; - } - - AZ_PROFILE_FUNCTION(AZ::Debug::ProfileCategory::Cloth); - - AZStd::unordered_set skinnedIndices; - for (size_t index = 0; index < meshRemappedVertices.size(); ++index) - { - const int remappedIndex = meshRemappedVertices[index]; - if (remappedIndex >= 0 && !skinnedIndices.contains(remappedIndex)) - { - ComputeVertexSkinnningTransform(m_skinningData[index]); - - const AZ::Vector3 skinnedPosition = ComputeSkinningPosition(originalPositions[remappedIndex].GetAsVector3()); - positions[remappedIndex].Set(skinnedPosition, positions[remappedIndex].GetW()); // Avoid overwriting the w component - - skinnedIndices.emplace(remappedIndex); // Avoid computing this index again - } - } - } - - void ActorClothSkinning::ApplySkinninOnRemovedVertices( - const MeshClothInfo& originalData, - ClothComponentMesh::RenderData& renderData, - const AZStd::vector& meshRemappedVertices) - { - if (!HasSkinningTransformData() || - originalData.m_particles.empty() || - originalData.m_particles.size() != renderData.m_particles.size() || - originalData.m_particles.size() != m_skinningData.size() || - m_skinningData.size() != meshRemappedVertices.size()) - { - return; - } - - AZ_PROFILE_FUNCTION(AZ::Debug::ProfileCategory::Cloth); - - for (size_t index = 0; index < originalData.m_particles.size(); ++index) - { - if (meshRemappedVertices[index] < 0) - { - ComputeVertexSkinnningTransform(m_skinningData[index]); - - const AZ::Vector3 skinnedPosition = ComputeSkinningPosition(originalData.m_particles[index].GetAsVector3()); - renderData.m_particles[index].Set(skinnedPosition, renderData.m_particles[index].GetW()); // Avoid overwriting the w component - - renderData.m_tangents[index] = ComputeSkinningVector(originalData.m_tangents[index]); - renderData.m_bitangents[index] = ComputeSkinningVector(originalData.m_bitangents[index]); - renderData.m_normals[index] = ComputeSkinningVector(originalData.m_normals[index]); - } - } - } - void ActorClothSkinning::UpdateActorVisibility() { bool isVisible = true; diff --git a/Gems/NvCloth/Code/Source/Components/ClothComponentMesh/ActorClothSkinning.h b/Gems/NvCloth/Code/Source/Components/ClothComponentMesh/ActorClothSkinning.h index 8cf139c75a..01df1f87ef 100644 --- a/Gems/NvCloth/Code/Source/Components/ClothComponentMesh/ActorClothSkinning.h +++ b/Gems/NvCloth/Code/Source/Components/ClothComponentMesh/ActorClothSkinning.h @@ -12,6 +12,7 @@ #pragma once +#include #include #include @@ -22,14 +23,14 @@ namespace NvCloth { struct MeshNodeInfo; - //! Skinning information of a particle. - struct SkinningInfo + //! One skinning influence of a vertex. + struct SkinningInfluence { - //! Weights of each joint that influence the particle. - AZStd::vector m_jointWeights; + //! Weight of the joint that influences the vertex. + float m_jointWeight = 0.0f; - //! List of joints that influence the particle. - AZStd::vector m_jointIndices; + //! Index of the joint that influences the vertex. + AZ::u16 m_jointIndex = AZStd::numeric_limits::max(); }; //! Class to retrieve skinning information from an actor on the same entity @@ -44,7 +45,9 @@ namespace NvCloth static AZStd::unique_ptr Create( AZ::EntityId entityId, const MeshNodeInfo& meshNodeInfo, - const size_t numSimParticles); + const size_t numVertices, + const size_t numSimulatedVertices, + const AZStd::vector& meshRemappedVertices); explicit ActorClothSkinning(AZ::EntityId entityId); @@ -53,17 +56,15 @@ namespace NvCloth //! Applies skinning to a list of positions. //! @note w components are not affected. - void ApplySkinning( + virtual void ApplySkinning( const AZStd::vector& originalPositions, - AZStd::vector& positions, - const AZStd::vector& meshRemappedVertices); + AZStd::vector& positions) = 0; //! Applies skinning to a list of positions and vectors whose vertices - //! have not been used for simulation (remapped index is negative). - void ApplySkinninOnRemovedVertices( + //! have not been used for simulation. + virtual void ApplySkinningOnNonSimulatedVertices( const MeshClothInfo& originalData, - ClothComponentMesh::RenderData& renderData, - const AZStd::vector& meshRemappedVertices); + ClothComponentMesh::RenderData& renderData) = 0; //! Updates visibility variables. void UpdateActorVisibility(); @@ -75,24 +76,20 @@ namespace NvCloth bool WasActorVisible() const; protected: - //! Returns true if it has valid skinning trasform data. - virtual bool HasSkinningTransformData() = 0; - - //! Computes the skinnning transformation to apply to a vertex data. - virtual void ComputeVertexSkinnningTransform(const SkinningInfo& skinningInfo) = 0; - - //! Computes skinning on a position. - virtual AZ::Vector3 ComputeSkinningPosition(const AZ::Vector3& originalPosition) = 0; - - //! Computes skinning on a vector. - virtual AZ::Vector3 ComputeSkinningVector(const AZ::Vector3& originalVector) = 0; - AZ::EntityId m_entityId; - // Skinning information of all particles - AZStd::vector m_skinningData; + size_t m_numberOfInfluencesPerVertex = 0; - // Collection of skeleton joint indices that influence the particles + // Skinning influences of all vertices + AZStd::vector m_skinningInfluences; + + // Indices to skinning influences that are part of the simulation + AZStd::vector m_simulatedVertices; + + // Indices to skinning influences that are not part of the simulation + AZStd::vector m_nonSimulatedVertices; + + // Collection of skeleton joint indices that influence the vertices AZStd::vector m_jointIndices; // Visibility variables diff --git a/Gems/NvCloth/Code/Source/Components/ClothComponentMesh/ClothComponentMesh.cpp b/Gems/NvCloth/Code/Source/Components/ClothComponentMesh/ClothComponentMesh.cpp index 740518b61a..52c93ea672 100644 --- a/Gems/NvCloth/Code/Source/Components/ClothComponentMesh/ClothComponentMesh.cpp +++ b/Gems/NvCloth/Code/Source/Components/ClothComponentMesh/ClothComponentMesh.cpp @@ -184,7 +184,12 @@ namespace NvCloth m_actorClothColliders = ActorClothColliders::Create(m_entityId); // It will return a valid instance if it's an actor with skinning data. - m_actorClothSkinning = ActorClothSkinning::Create(m_entityId, m_meshNodeInfo, m_meshClothInfo.m_particles.size()); + m_actorClothSkinning = ActorClothSkinning::Create( + m_entityId, + m_meshNodeInfo, + m_meshClothInfo.m_particles.size(), + m_cloth->GetParticles().size(), + m_meshRemappedVertices); m_numberOfClothSkinningUpdates = 0; m_clothConstraints = ClothConstraints::Create( @@ -363,7 +368,7 @@ namespace NvCloth { // Update skinning for all particles and apply it to cloth AZStd::vector particles = m_cloth->GetParticles(); - m_actorClothSkinning->ApplySkinning(m_cloth->GetInitialParticles(), particles, m_meshRemappedVertices); + m_actorClothSkinning->ApplySkinning(m_cloth->GetInitialParticles(), particles); m_cloth->SetParticles(AZStd::move(particles)); m_cloth->DiscardParticleDelta(); } @@ -379,8 +384,8 @@ namespace NvCloth if (m_actorClothSkinning) { - m_actorClothSkinning->ApplySkinning(m_clothConstraints->GetMotionConstraints(), m_motionConstraints, m_meshRemappedVertices); - m_actorClothSkinning->ApplySkinning(m_clothConstraints->GetSeparationConstraints(), m_separationConstraints, m_meshRemappedVertices); + m_actorClothSkinning->ApplySkinning(m_clothConstraints->GetMotionConstraints(), m_motionConstraints); + m_actorClothSkinning->ApplySkinning(m_clothConstraints->GetSeparationConstraints(), m_separationConstraints); } m_cloth->GetClothConfigurator()->SetMotionConstraints(m_motionConstraints); @@ -404,7 +409,7 @@ namespace NvCloth if (m_config.m_removeStaticTriangles && m_actorClothSkinning) { // Apply skinning to the non-simulated part of the mesh. - m_actorClothSkinning->ApplySkinninOnRemovedVertices(m_meshClothInfo, renderData, m_meshRemappedVertices); + m_actorClothSkinning->ApplySkinningOnNonSimulatedVertices(m_meshClothInfo, renderData); } // Calculate normals of the cloth particles (simplified mesh). diff --git a/Gems/NvCloth/Code/Tests/Components/ClothComponentMesh/ActorClothSkinningTest.cpp b/Gems/NvCloth/Code/Tests/Components/ClothComponentMesh/ActorClothSkinningTest.cpp index 75cff2da04..3fba04f695 100644 --- a/Gems/NvCloth/Code/Tests/Components/ClothComponentMesh/ActorClothSkinningTest.cpp +++ b/Gems/NvCloth/Code/Tests/Components/ClothComponentMesh/ActorClothSkinningTest.cpp @@ -98,7 +98,7 @@ namespace UnitTest { AZ::EntityId entityId; AZStd::unique_ptr actorClothSkinning = - NvCloth::ActorClothSkinning::Create(entityId, {}, 0); + NvCloth::ActorClothSkinning::Create(entityId, {}, 0, 0, {}); EXPECT_TRUE(actorClothSkinning.get() == nullptr); } @@ -107,7 +107,7 @@ namespace UnitTest { AZ::EntityId entityId; AZStd::unique_ptr actorClothSkinning = - NvCloth::ActorClothSkinning::Create(entityId, MeshNodeInfo, MeshRemappedVertices.size()); + NvCloth::ActorClothSkinning::Create(entityId, MeshNodeInfo, MeshVertices.size(), MeshVertices.size(), MeshRemappedVertices); EXPECT_TRUE(actorClothSkinning.get() == nullptr); } @@ -122,7 +122,7 @@ namespace UnitTest } AZStd::unique_ptr actorClothSkinning = - NvCloth::ActorClothSkinning::Create(m_actorComponent->GetEntityId(), {}, 0); + NvCloth::ActorClothSkinning::Create(m_actorComponent->GetEntityId(), {}, 0, 0, {}); EXPECT_TRUE(actorClothSkinning.get() == nullptr); } @@ -139,7 +139,7 @@ namespace UnitTest } AZStd::unique_ptr actorClothSkinning = - NvCloth::ActorClothSkinning::Create(m_actorComponent->GetEntityId(), MeshNodeInfo, MeshVertices.size()); + NvCloth::ActorClothSkinning::Create(m_actorComponent->GetEntityId(), MeshNodeInfo, MeshVertices.size(), MeshVertices.size(), MeshRemappedVertices); EXPECT_TRUE(actorClothSkinning.get() == nullptr); } @@ -156,7 +156,7 @@ namespace UnitTest } AZStd::unique_ptr actorClothSkinning = - NvCloth::ActorClothSkinning::Create(m_actorComponent->GetEntityId(), MeshNodeInfo, MeshVertices.size()); + NvCloth::ActorClothSkinning::Create(m_actorComponent->GetEntityId(), MeshNodeInfo, MeshVertices.size(), MeshVertices.size(), MeshRemappedVertices); EXPECT_TRUE(actorClothSkinning.get() != nullptr); } @@ -184,7 +184,7 @@ namespace UnitTest } AZStd::unique_ptr actorClothSkinning = - NvCloth::ActorClothSkinning::Create(actorComponent->GetEntityId(), MeshNodeInfo, MeshVertices.size()); + NvCloth::ActorClothSkinning::Create(actorComponent->GetEntityId(), MeshNodeInfo, MeshVertices.size(), MeshVertices.size(), MeshRemappedVertices); ASSERT_TRUE(actorClothSkinning.get() != nullptr); const AZStd::vector clothParticles = {{ @@ -195,7 +195,7 @@ namespace UnitTest AZStd::vector skinnedClothParticles(clothParticles.size(), NvCloth::SimParticleFormat(0.0f, 0.0f, 0.0f, 1.0f)); actorClothSkinning->UpdateSkinning(); - actorClothSkinning->ApplySkinning(clothParticles, skinnedClothParticles, MeshRemappedVertices); + actorClothSkinning->ApplySkinning(clothParticles, skinnedClothParticles); EXPECT_THAT(skinnedClothParticles, ::testing::Pointwise(ContainerIsCloseTolerance(Tolerance), clothParticles)); @@ -208,7 +208,7 @@ namespace UnitTest AZStd::vector newSkinnedClothParticles(clothParticles.size(), NvCloth::SimParticleFormat(0.0f, 0.0f, 0.0f, 1.0f)); actorClothSkinning->UpdateSkinning(); - actorClothSkinning->ApplySkinning(clothParticles, newSkinnedClothParticles, MeshRemappedVertices); + actorClothSkinning->ApplySkinning(clothParticles, newSkinnedClothParticles); const AZ::Transform diffTransform = AZ::Transform::CreateRotationY(AZ::DegToRad(90.0f)); const AZStd::vector clothParticlesResult = {{ @@ -245,7 +245,7 @@ namespace UnitTest } AZStd::unique_ptr actorClothSkinning = - NvCloth::ActorClothSkinning::Create(m_actorComponent->GetEntityId(), MeshNodeInfo, MeshVertices.size()); + NvCloth::ActorClothSkinning::Create(m_actorComponent->GetEntityId(), MeshNodeInfo, MeshVertices.size(), MeshVertices.size(), MeshRemappedVertices); ASSERT_TRUE(actorClothSkinning.get() != nullptr); const AZStd::vector clothParticles = {{ @@ -256,7 +256,7 @@ namespace UnitTest AZStd::vector skinnedClothParticles(clothParticles.size(), NvCloth::SimParticleFormat(0.0f, 0.0f, 0.0f, 1.0f)); actorClothSkinning->UpdateSkinning(); - actorClothSkinning->ApplySkinning(clothParticles, skinnedClothParticles, MeshRemappedVertices); + actorClothSkinning->ApplySkinning(clothParticles, skinnedClothParticles); EXPECT_THAT(skinnedClothParticles, ::testing::Pointwise(ContainerIsCloseTolerance(Tolerance), clothParticles)); @@ -271,7 +271,7 @@ namespace UnitTest AZStd::vector newSkinnedClothParticles(clothParticles.size(), NvCloth::SimParticleFormat(0.0f, 0.0f, 0.0f, 1.0f)); actorClothSkinning->UpdateSkinning(); - actorClothSkinning->ApplySkinning(clothParticles, newSkinnedClothParticles, MeshRemappedVertices); + actorClothSkinning->ApplySkinning(clothParticles, newSkinnedClothParticles); const AZStd::vector clothParticlesResult = {{ NvCloth::SimParticleFormat(-48.4177f, -31.9446f, 45.2279f, 1.0f), @@ -294,7 +294,7 @@ namespace UnitTest } AZStd::unique_ptr actorClothSkinning = - NvCloth::ActorClothSkinning::Create(m_actorComponent->GetEntityId(), MeshNodeInfo, MeshVertices.size()); + NvCloth::ActorClothSkinning::Create(m_actorComponent->GetEntityId(), MeshNodeInfo, MeshVertices.size(), MeshVertices.size(), MeshRemappedVertices); ASSERT_TRUE(actorClothSkinning.get() != nullptr); EXPECT_FALSE(actorClothSkinning->IsActorVisible());