diff --git a/CMakeLists.txt b/CMakeLists.txt index 1b78a52c..0c22371b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -479,6 +479,15 @@ if(MOMENTUM_BUILD_TESTING) NAME character_test_helpers HEADERS_VARS character_test_helpers_public_headers SOURCES_VARS character_test_helpers_sources + PUBLIC_LINK_LIBRARIES + character + EXCLUDE_FROM_INSTALL + ) + + mt_library( + NAME character_test_helpers_gtest + HEADERS_VARS character_test_helpers_gtest_public_headers + SOURCES_VARS character_test_helpers_gtest_sources PUBLIC_LINK_LIBRARIES character PRIVATE_LINK_LIBRARIES diff --git a/cmake/build_variables.bzl b/cmake/build_variables.bzl index 2c5908ea..8723ecdb 100644 --- a/cmake/build_variables.bzl +++ b/cmake/build_variables.bzl @@ -464,6 +464,14 @@ character_test_helpers_sources = [ "test/character/character_helpers.cpp", ] +character_test_helpers_gtest_public_headers = [ + "test/character/character_helpers_gtest.h", +] + +character_test_helpers_gtest_sources = [ + "test/character/character_helpers_gtest.cpp", +] + solver_test_helper_public_headers = [ "test/solver/solver_test_helpers.h", ] diff --git a/momentum/test/character/character_helpers.cpp b/momentum/test/character/character_helpers.cpp index c49b2be5..f444c66d 100644 --- a/momentum/test/character/character_helpers.cpp +++ b/momentum/test/character/character_helpers.cpp @@ -20,16 +20,17 @@ #include "momentum/math/mppca.h" #include "momentum/math/random.h" -#include -#include -#include - #include namespace momentum { namespace { +template +Eigen::MatrixX randomMatrix(Eigen::Index nRows, Eigen::Index nCols) { + return normal>(nRows, nCols, 0, 1); +} + [[nodiscard]] Skeleton createDefaultSkeleton(size_t numJoints) { Skeleton result; Joint joint; @@ -176,39 +177,6 @@ ParameterLimits createDefaultParameterLimits() { return lm; } -template -void sortSkinWeightsRows( - Eigen::MatrixBase& indexMap, - Eigen::MatrixBase& weightMap) { - using Index = typename DerivedIndexMatrix::Scalar; - using Scalar = typename DerivedWeightMatrix::Scalar; - - for (int i = 0; i < indexMap.rows(); ++i) { - std::vector> pairs; - - for (int j = 0; j < indexMap.cols(); ++j) { - if (indexMap(i, j) == 0) { // Assuming 0 is the marker for unused elements - break; - } - pairs.emplace_back(indexMap(i, j), weightMap(i, j)); - } - - // Sort pairs based on the first element of the pair (index value) - std::sort( - pairs.begin(), - pairs.end(), - [](const std::pair& a, const std::pair& b) { - return a.first < b.first; - }); - - // Place sorted values back into the matrices - for (Index k = 0; k < pairs.size(); ++k) { - indexMap(i, k) = pairs[k].first; - weightMap(i, k) = pairs[k].second; - } - } -} - } // namespace template @@ -229,203 +197,6 @@ CharacterT createTestCharacter(size_t numJoints) { template CharacterT createTestCharacter(size_t numJoints); template CharacterT createTestCharacter(size_t numJoints); -namespace { - -template -Eigen::MatrixX randomMatrix(Eigen::Index nRows, Eigen::Index nCols) { - Eigen::MatrixX result(nRows, nCols); - for (Eigen::Index i = 0; i < nRows; ++i) { - for (Eigen::Index j = 0; j < nCols; ++j) { - result(i, j) = normal(0, 1); - } - } - return result; -} - -MATCHER_P(FloatNearPointwise, tol, "Value mismatch") { - for (int i = 0; i < std::get<0>(arg).size(); i++) { - if (std::abs(std::get<0>(arg)[i] - std::get<1>(arg)[i]) > tol) { - return false; - } - } - return true; -} - -MATCHER(IntExactPointwise, "Value mismatch") { - for (int i = 0; i < std::get<0>(arg).size(); i++) { - if (std::get<0>(arg)[i] != std::get<1>(arg)[i]) { - return false; - } - } - return true; -} - -MATCHER(ElementsEq, "Elements mismatch") { - return ::testing::get<0>(arg) == ::testing::get<1>(arg); -} - -MATCHER(ElementsIsApprox, "Elements mismatch") { - return ::testing::get<0>(arg).isApprox(::testing::get<1>(arg)); -} - -} // namespace - -void compareMeshes(const Mesh_u& refMesh, const Mesh_u& mesh) { - ASSERT_TRUE((refMesh && mesh)); - EXPECT_THAT(refMesh->vertices, testing::Pointwise(FloatNearPointwise(0.0001), mesh->vertices)); - EXPECT_THAT(refMesh->normals, testing::Pointwise(FloatNearPointwise(0.01), mesh->normals)); - EXPECT_THAT(refMesh->faces, testing::Pointwise(IntExactPointwise(), mesh->faces)); - EXPECT_THAT(refMesh->colors, testing::Pointwise(IntExactPointwise(), mesh->colors)); - EXPECT_THAT(refMesh->texcoords, testing::Pointwise(FloatNearPointwise(0.0001), mesh->texcoords)); - EXPECT_THAT( - refMesh->confidence, testing::Pointwise(testing::DoubleNear(0.0001), mesh->confidence)); - EXPECT_THAT( - refMesh->texcoord_faces, testing::Pointwise(IntExactPointwise(), mesh->texcoord_faces)); -} - -void compareLocators(const LocatorList& refLocators, const LocatorList& locators) { - EXPECT_EQ(refLocators.size(), locators.size()); - auto sortedRefLocators = refLocators; - auto sortedLocators = locators; - auto compareLocators = [](const Locator& l1, const Locator& l2) { - return l1.parent != l2.parent ? l1.parent < l2.parent : l1.name < l2.name; - }; - std::sort(sortedRefLocators.begin(), sortedRefLocators.end(), compareLocators); - std::sort(sortedLocators.begin(), sortedLocators.end(), compareLocators); - EXPECT_THAT(sortedRefLocators, testing::Pointwise(ElementsEq(), sortedLocators)); -} - -void compareCollisionGeometry( - const CollisionGeometry_u& refCollision, - const CollisionGeometry_u& collision) { - if (refCollision == nullptr) - ASSERT_EQ(collision, nullptr); - else { - ASSERT_NE(collision, nullptr); - EXPECT_EQ(refCollision->size(), collision->size()); - auto sortedRefCollision = *refCollision; - auto sortedCollision = *collision; - auto compareCollisions = [](const TaperedCapsule& l1, const TaperedCapsule& l2) { - if (l1.parent != l2.parent) - return l1.parent < l2.parent; - if (l1.length != l2.length) - return l1.length < l2.length; - if (!l1.radius.isApprox(l2.radius)) - return l1.radius.x() != l2.radius.x() ? l1.radius.x() < l2.radius.x() - : l1.radius.y() < l2.radius.y(); - const auto t1 = l1.transformation.matrix(); - const auto t2 = l2.transformation.matrix(); - EXPECT_EQ(t1.size(), t2.size()); - for (auto i = 0; i < t1.size(); i++) { - if (std::abs(t1.data()[i] - t2.data()[i]) > std::numeric_limits::epsilon()) { - return t1.data()[i] < t2.data()[i]; - } - } - return false; - }; - std::sort(sortedRefCollision.begin(), sortedRefCollision.end(), compareCollisions); - std::sort(sortedCollision.begin(), sortedCollision.end(), compareCollisions); - for (size_t i = 0; i < sortedCollision.size(); ++i) { - const auto& collA = sortedRefCollision[i]; - const auto& collB = sortedCollision[i]; - - EXPECT_TRUE(collA.isApprox(collB)) << "Collision geometry mismatch at index " << i << ":\n" - << "- refCollision:\n" - << " - radius_0 : " << collA.radius.x() << "\n" - << " - radius_1 : " << collA.radius.y() << "\n" - << " - length : " << collA.length << "\n" - << " - parent : " << collA.parent << "\n" - << " - transform:\n" - << collA.transformation.matrix() << "\n" - << "- collision:\n" - << " - radius_0 : " << collB.radius.x() << "\n" - << " - radius_1 : " << collB.radius.y() << "\n" - << " - length : " << collB.length << "\n" - << " - parent : " << collB.parent << "\n" - << " - transform:\n" - << collB.transformation.matrix() << std::endl; - } - } -} - -void compareChars(const Character& refChar, const Character& character, const bool withMesh) { - const auto& refJoints = refChar.skeleton.joints; - const auto& joints = character.skeleton.joints; - ASSERT_EQ(refJoints.size(), joints.size()); - for (size_t i = 0; i < refJoints.size(); ++i) { - EXPECT_TRUE(refJoints[i].isApprox(joints[i])) - << "Joint " << i << " is not equal:\n" - << "- refJoint:\n" - << " - name: " << refJoints[i].name << "\n" - << " - parent: " << refJoints[i].parent << "\n" - << " - preRotation: " << refJoints[i].preRotation.coeffs().transpose() << "\n" - << " - translationOffset: " << refJoints[i].translationOffset.transpose() << "\n" - << "- joint:\n" - << " - name: " << joints[i].name << "\n" - << " - parent: " << joints[i].parent << "\n" - << " - preRotation: " << joints[i].preRotation.coeffs().transpose() << "\n" - << " - translationOffset: " << joints[i].translationOffset.transpose() << "\n"; - } - ASSERT_TRUE(refChar.parameterTransform.isApprox(character.parameterTransform)); - EXPECT_THAT(refChar.parameterLimits, testing::Pointwise(ElementsEq(), character.parameterLimits)); - compareLocators(refChar.locators, character.locators); - compareCollisionGeometry(refChar.collision, character.collision); - ASSERT_EQ(refChar.inverseBindPose.size(), character.inverseBindPose.size()); - for (size_t i = 0; i < refChar.inverseBindPose.size(); ++i) { - EXPECT_TRUE(refChar.inverseBindPose[i].isApprox(character.inverseBindPose[i], 1e-4f)) - << "InverseBindPose " << i << " is not equal:\n" - << "- Expected:\n" - << refChar.inverseBindPose[i].matrix() << "\n" - << "- Actual :\n" - << character.inverseBindPose[i].matrix() << std::endl; - } - EXPECT_EQ(refChar.jointMap, character.jointMap); - - if (withMesh) { - auto ptrValEq = [](auto& l, auto& r) { - if (l == nullptr && r == nullptr) { - return true; - } - if (l == nullptr) { - return (r == nullptr); - } - if (r == nullptr) { - return false; - } - return true; - }; - - compareMeshes(refChar.mesh, character.mesh); - - auto refCharIndices = refChar.skinWeights->index; - auto refCharWeights = refChar.skinWeights->weight; - sortSkinWeightsRows(refCharIndices, refCharWeights); - - auto charIndices = refChar.skinWeights->index; - auto charWeights = refChar.skinWeights->weight; - sortSkinWeightsRows(charIndices, charWeights); - - ASSERT_EQ(refCharIndices.rows(), charIndices.rows()); - ASSERT_EQ(refCharIndices.cols(), charIndices.cols()); - for (auto i = 0u; i < refCharIndices.rows(); ++i) { - EXPECT_EQ(refCharIndices.row(i), charIndices.row(i)) - << "SkinWeights index row " << i << " mismatch\n"; - } - ASSERT_LT((refCharWeights - charWeights).lpNorm(), 1e-4f); - ASSERT_TRUE(ptrValEq(refChar.skinWeights, character.skinWeights)); - - auto ptrValIsApprox = [](auto& l, auto& r) { - if (l == nullptr) - return (r == nullptr); - if (r == nullptr) - return false; - return l->isApprox(*r); - }; - ASSERT_TRUE(ptrValIsApprox(refChar.poseShapes, character.poseShapes)); - ASSERT_TRUE(ptrValIsApprox(refChar.blendShape, character.blendShape)); - } -} - template CharacterT withTestBlendShapes(const CharacterT& character) { MT_CHECK(character.mesh); diff --git a/momentum/test/character/character_helpers.h b/momentum/test/character/character_helpers.h index 91178560..fd77192d 100644 --- a/momentum/test/character/character_helpers.h +++ b/momentum/test/character/character_helpers.h @@ -12,10 +12,6 @@ namespace momentum { -// Matching methods -void compareMeshes(const Mesh_u& refMesh, const Mesh_u& mesh); -void compareChars(const Character& refChar, const Character& character, bool withMesh = true); - /// Creates a character with a customizable number of joints. /// /// @param numJoints The number of joints in the resulting character. diff --git a/momentum/test/character/character_helpers_gtest.cpp b/momentum/test/character/character_helpers_gtest.cpp new file mode 100644 index 00000000..15e066c4 --- /dev/null +++ b/momentum/test/character/character_helpers_gtest.cpp @@ -0,0 +1,254 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "momentum/test/character/character_helpers_gtest.h" + +#include "momentum/character/blend_shape.h" +#include "momentum/character/character.h" +#include "momentum/character/collision_geometry.h" +#include "momentum/character/linear_skinning.h" +#include "momentum/character/locator.h" +#include "momentum/character/parameter_transform.h" +#include "momentum/character/skeleton.h" +#include "momentum/character/skin_weights.h" +#include "momentum/common/checks.h" +#include "momentum/math/mesh.h" +#include "momentum/math/mppca.h" + +#include +#include +#include + +#include + +namespace momentum { + +namespace { + +MATCHER_P(FloatNearPointwise, tol, "Value mismatch") { + for (int i = 0; i < std::get<0>(arg).size(); i++) { + if (std::abs(std::get<0>(arg)[i] - std::get<1>(arg)[i]) > tol) { + return false; + } + } + return true; +} + +MATCHER(IntExactPointwise, "Value mismatch") { + for (int i = 0; i < std::get<0>(arg).size(); i++) { + if (std::get<0>(arg)[i] != std::get<1>(arg)[i]) { + return false; + } + } + return true; +} + +MATCHER(ElementsEq, "Elements mismatch") { + return ::testing::get<0>(arg) == ::testing::get<1>(arg); +} + +MATCHER(ElementsIsApprox, "Elements mismatch") { + return ::testing::get<0>(arg).isApprox(::testing::get<1>(arg)); +} + +template +void sortSkinWeightsRows( + Eigen::MatrixBase& indexMap, + Eigen::MatrixBase& weightMap) { + using Index = typename DerivedIndexMatrix::Scalar; + using Scalar = typename DerivedWeightMatrix::Scalar; + + for (int i = 0; i < indexMap.rows(); ++i) { + std::vector> pairs; + + for (int j = 0; j < indexMap.cols(); ++j) { + if (indexMap(i, j) == 0) { // Assuming 0 is the marker for unused elements + break; + } + pairs.emplace_back(indexMap(i, j), weightMap(i, j)); + } + + // Sort pairs based on the first element of the pair (index value) + std::sort( + pairs.begin(), + pairs.end(), + [](const std::pair& a, const std::pair& b) { + return a.first < b.first; + }); + + // Place sorted values back into the matrices + for (Index k = 0; k < pairs.size(); ++k) { + indexMap(i, k) = pairs[k].first; + weightMap(i, k) = pairs[k].second; + } + } +} + +} // namespace + +void compareMeshes(const Mesh_u& refMesh, const Mesh_u& mesh) { + ASSERT_TRUE((refMesh && mesh)); + EXPECT_THAT(refMesh->vertices, testing::Pointwise(FloatNearPointwise(0.0001), mesh->vertices)); + EXPECT_THAT(refMesh->normals, testing::Pointwise(FloatNearPointwise(0.01), mesh->normals)); + EXPECT_THAT(refMesh->faces, testing::Pointwise(IntExactPointwise(), mesh->faces)); + EXPECT_THAT(refMesh->colors, testing::Pointwise(IntExactPointwise(), mesh->colors)); + EXPECT_THAT(refMesh->texcoords, testing::Pointwise(FloatNearPointwise(0.0001), mesh->texcoords)); + EXPECT_THAT( + refMesh->confidence, testing::Pointwise(testing::DoubleNear(0.0001), mesh->confidence)); + EXPECT_THAT( + refMesh->texcoord_faces, testing::Pointwise(IntExactPointwise(), mesh->texcoord_faces)); +} + +void compareLocators(const LocatorList& refLocators, const LocatorList& locators) { + EXPECT_EQ(refLocators.size(), locators.size()); + auto sortedRefLocators = refLocators; + auto sortedLocators = locators; + auto compareLocators = [](const Locator& l1, const Locator& l2) { + return l1.parent != l2.parent ? l1.parent < l2.parent : l1.name < l2.name; + }; + std::sort(sortedRefLocators.begin(), sortedRefLocators.end(), compareLocators); + std::sort(sortedLocators.begin(), sortedLocators.end(), compareLocators); + EXPECT_THAT(sortedRefLocators, testing::Pointwise(ElementsEq(), sortedLocators)); +} + +void compareCollisionGeometry( + const CollisionGeometry_u& refCollision, + const CollisionGeometry_u& collision) { + if (refCollision == nullptr) { + ASSERT_EQ(collision, nullptr); + } else { + ASSERT_NE(collision, nullptr); + EXPECT_EQ(refCollision->size(), collision->size()); + auto sortedRefCollision = *refCollision; + auto sortedCollision = *collision; + auto compareCollisions = [](const TaperedCapsule& l1, const TaperedCapsule& l2) { + if (l1.parent != l2.parent) { + return l1.parent < l2.parent; + } + if (l1.length != l2.length) { + return l1.length < l2.length; + } + if (!l1.radius.isApprox(l2.radius)) { + return l1.radius.x() != l2.radius.x() ? l1.radius.x() < l2.radius.x() + : l1.radius.y() < l2.radius.y(); + } + const auto t1 = l1.transformation.matrix(); + const auto t2 = l2.transformation.matrix(); + EXPECT_EQ(t1.size(), t2.size()); + for (auto i = 0; i < t1.size(); i++) { + if (std::abs(t1.data()[i] - t2.data()[i]) > std::numeric_limits::epsilon()) { + return t1.data()[i] < t2.data()[i]; + } + } + return false; + }; + std::sort(sortedRefCollision.begin(), sortedRefCollision.end(), compareCollisions); + std::sort(sortedCollision.begin(), sortedCollision.end(), compareCollisions); + for (size_t i = 0; i < sortedCollision.size(); ++i) { + const auto& collA = sortedRefCollision[i]; + const auto& collB = sortedCollision[i]; + + EXPECT_TRUE(collA.isApprox(collB)) << "Collision geometry mismatch at index " << i << ":\n" + << "- refCollision:\n" + << " - radius_0 : " << collA.radius.x() << "\n" + << " - radius_1 : " << collA.radius.y() << "\n" + << " - length : " << collA.length << "\n" + << " - parent : " << collA.parent << "\n" + << " - transform:\n" + << collA.transformation.matrix() << "\n" + << "- collision:\n" + << " - radius_0 : " << collB.radius.x() << "\n" + << " - radius_1 : " << collB.radius.y() << "\n" + << " - length : " << collB.length << "\n" + << " - parent : " << collB.parent << "\n" + << " - transform:\n" + << collB.transformation.matrix() << std::endl; + } + } +} + +void compareChars(const Character& refChar, const Character& character, const bool withMesh) { + const auto& refJoints = refChar.skeleton.joints; + const auto& joints = character.skeleton.joints; + ASSERT_EQ(refJoints.size(), joints.size()); + for (size_t i = 0; i < refJoints.size(); ++i) { + EXPECT_TRUE(refJoints[i].isApprox(joints[i])) + << "Joint " << i << " is not equal:\n" + << "- refJoint:\n" + << " - name: " << refJoints[i].name << "\n" + << " - parent: " << refJoints[i].parent << "\n" + << " - preRotation: " << refJoints[i].preRotation.coeffs().transpose() << "\n" + << " - translationOffset: " << refJoints[i].translationOffset.transpose() << "\n" + << "- joint:\n" + << " - name: " << joints[i].name << "\n" + << " - parent: " << joints[i].parent << "\n" + << " - preRotation: " << joints[i].preRotation.coeffs().transpose() << "\n" + << " - translationOffset: " << joints[i].translationOffset.transpose() << "\n"; + } + ASSERT_TRUE(refChar.parameterTransform.isApprox(character.parameterTransform)); + EXPECT_THAT(refChar.parameterLimits, testing::Pointwise(ElementsEq(), character.parameterLimits)); + compareLocators(refChar.locators, character.locators); + compareCollisionGeometry(refChar.collision, character.collision); + ASSERT_EQ(refChar.inverseBindPose.size(), character.inverseBindPose.size()); + for (size_t i = 0; i < refChar.inverseBindPose.size(); ++i) { + EXPECT_TRUE(refChar.inverseBindPose[i].isApprox(character.inverseBindPose[i], 1e-4f)) + << "InverseBindPose " << i << " is not equal:\n" + << "- Expected:\n" + << refChar.inverseBindPose[i].matrix() << "\n" + << "- Actual :\n" + << character.inverseBindPose[i].matrix() << std::endl; + } + EXPECT_EQ(refChar.jointMap, character.jointMap); + + if (withMesh) { + auto ptrValEq = [](auto& l, auto& r) { + if (l == nullptr && r == nullptr) { + return true; + } + if (l == nullptr) { + return (r == nullptr); + } + if (r == nullptr) { + return false; + } + return true; + }; + + compareMeshes(refChar.mesh, character.mesh); + + auto refCharIndices = refChar.skinWeights->index; + auto refCharWeights = refChar.skinWeights->weight; + sortSkinWeightsRows(refCharIndices, refCharWeights); + + auto charIndices = refChar.skinWeights->index; + auto charWeights = refChar.skinWeights->weight; + sortSkinWeightsRows(charIndices, charWeights); + + ASSERT_EQ(refCharIndices.rows(), charIndices.rows()); + ASSERT_EQ(refCharIndices.cols(), charIndices.cols()); + for (auto i = 0u; i < refCharIndices.rows(); ++i) { + EXPECT_EQ(refCharIndices.row(i), charIndices.row(i)) + << "SkinWeights index row " << i << " mismatch\n"; + } + ASSERT_LT((refCharWeights - charWeights).lpNorm(), 1e-4f); + ASSERT_TRUE(ptrValEq(refChar.skinWeights, character.skinWeights)); + + auto ptrValIsApprox = [](auto& l, auto& r) { + if (l == nullptr) { + return (r == nullptr); + } + if (r == nullptr) { + return false; + } + return l->isApprox(*r); + }; + ASSERT_TRUE(ptrValIsApprox(refChar.poseShapes, character.poseShapes)); + ASSERT_TRUE(ptrValIsApprox(refChar.blendShape, character.blendShape)); + } +} + +} // namespace momentum diff --git a/momentum/test/character/character_helpers_gtest.h b/momentum/test/character/character_helpers_gtest.h new file mode 100644 index 00000000..ffa4d125 --- /dev/null +++ b/momentum/test/character/character_helpers_gtest.h @@ -0,0 +1,19 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include + +namespace momentum { + +// Matching methods +void compareMeshes(const Mesh_u& refMesh, const Mesh_u& mesh); +void compareChars(const Character& refChar, const Character& character, bool withMesh = true); + +} // namespace momentum