Skip to content

Commit ce15a9d

Browse files
cstollmetameta-codesync[bot]
authored andcommitted
Add metadata field to character (#920)
Summary: Pull Request resolved: #920 Added a metadata field to the character. Both name and metadata are now read as a user property from the skeleton root node and properly copied along with other values in the character. The metadata field can be used to uniquely identify an asset or store other information about processing. It's limited to 65536 characters at the moment and on loading tested to be in json format. Reviewed By: maxouellet Differential Revision: D87269358 fbshipit-source-id: a47ceba5e86ff656a04cb6eb121de578def92c18
1 parent 96a3086 commit ce15a9d

File tree

9 files changed

+116
-12
lines changed

9 files changed

+116
-12
lines changed

momentum/character/character.cpp

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@ CharacterT<T>::CharacterT(
3838
BlendShapeBase_const_p faceExpressionBlendShapes,
3939
const std::string& nameIn,
4040
const momentum::TransformationList& inverseBindPose_in,
41-
const SkinnedLocatorList& skinnedLocators)
41+
const SkinnedLocatorList& skinnedLocators,
42+
std::string_view metadataIn)
4243
: skeleton(s),
4344
parameterTransform(pt),
4445
parameterLimits(pl),
@@ -47,7 +48,8 @@ CharacterT<T>::CharacterT(
4748
blendShape(std::move(blendShapes)),
4849
faceExpressionBlendShape(std::move(faceExpressionBlendShapes)),
4950
inverseBindPose(inverseBindPose_in),
50-
name(nameIn) {
51+
name(nameIn),
52+
metadata(metadataIn) {
5153
if (m) {
5254
mesh = std::make_unique<Mesh>(*m);
5355
// create skinweights copy only if both mesh and skinweights exist
@@ -87,7 +89,8 @@ CharacterT<T>::CharacterT(const CharacterT& c)
8789
faceExpressionBlendShape(c.faceExpressionBlendShape),
8890
inverseBindPose(c.inverseBindPose),
8991
jointMap(c.jointMap),
90-
name(c.name) {
92+
name(c.name),
93+
metadata(c.metadata) {
9194
if (c.mesh) {
9295
mesh = std::make_unique<Mesh>(*c.mesh);
9396
// create skinweights copy only if both mesh and skinweights exist
@@ -136,6 +139,7 @@ CharacterT<T>& CharacterT<T>::operator=(const CharacterT& rhs) {
136139
std::swap(blendShape, tmp.blendShape);
137140
std::swap(faceExpressionBlendShape, tmp.faceExpressionBlendShape);
138141
std::swap(name, tmp.name);
142+
std::swap(metadata, tmp.metadata);
139143

140144
return *this;
141145
}
@@ -320,6 +324,8 @@ CharacterT<T> CharacterT<T>::simplifySkeleton(const std::vector<bool>& activeJoi
320324

321325
// create character result
322326
CharacterT<T> result(simplifiedSkeleton, simplifiedTransform);
327+
result.name = name;
328+
result.metadata = metadata;
323329

324330
result.jointMap = simplifiedJointMap;
325331

@@ -387,7 +393,8 @@ CharacterT<T> CharacterT<T>::simplifyParameterTransform(const ParameterSet& para
387393
faceExpressionBlendShape,
388394
name,
389395
inverseBindPose,
390-
skinnedLocators);
396+
skinnedLocators,
397+
metadata);
391398
}
392399

393400
template <typename T>

momentum/character/character.h

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,9 @@ struct CharacterT {
7070
/// Character identifier
7171
std::string name;
7272

73+
/// Metadata (as a JSON-serialized string)
74+
std::string metadata;
75+
7376
/// @}
7477

7578
/// Default constructor
@@ -92,6 +95,8 @@ struct CharacterT {
9295
/// @param faceExpressionBlendShapes Optional facial expression blend shapes
9396
/// @param nameIn Optional character identifier
9497
/// @param inverseBindPose Optional inverse bind pose transformations
98+
/// @param skinnedLocators Optional points of interest attached to joints, with skinning weights
99+
/// @param metadataIn Optional metadata
95100
CharacterT(
96101
const Skeleton& s,
97102
const ParameterTransform& pt,
@@ -105,7 +110,8 @@ struct CharacterT {
105110
BlendShapeBase_const_p faceExpressionBlendShapes = {},
106111
const std::string& nameIn = "",
107112
const momentum::TransformationList& inverseBindPose = {},
108-
const SkinnedLocatorList& skinnedLocators = {});
113+
const SkinnedLocatorList& skinnedLocators = {},
114+
std::string_view metadataIn = "");
109115

110116
/// Copy constructor
111117
CharacterT(const CharacterT& c);

momentum/io/fbx/fbx_io.cpp

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -551,6 +551,16 @@ void saveBlendShapesToFbx(
551551
mesh->AddDeformer(blendShape);
552552
}
553553

554+
void addMetaData(::fbxsdk::FbxNode* skeletonRootNode, const Character& character) {
555+
// add metadata
556+
if (skeletonRootNode != nullptr) {
557+
::fbxsdk::FbxProperty::Create(skeletonRootNode, ::fbxsdk::FbxStringDT, "metadata")
558+
.Set(FbxString(character.metadata.c_str()));
559+
::fbxsdk::FbxProperty::Create(skeletonRootNode, ::fbxsdk::FbxStringDT, "name")
560+
.Set(FbxString(character.name.c_str()));
561+
}
562+
}
563+
554564
void saveFbxCommon(
555565
const filesystem::path& filename,
556566
const Character& character,
@@ -658,6 +668,9 @@ void saveFbxCommon(
658668
}
659669
}
660670

671+
// add metadata
672+
addMetaData(skeletonRootNode, character);
673+
661674
// ---------------------------------------------
662675
// create the locator nodes
663676
// ---------------------------------------------

momentum/io/fbx/openfbx_loader.cpp

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
#include "momentum/math/mesh.h"
2525
#include "momentum/math/utility.h"
2626

27+
#include <nlohmann/json.hpp>
2728
#include <ofbx.h>
2829
#include <gsl/span_ext>
2930

@@ -221,6 +222,20 @@ double resolveDoubleProperty(const ofbx::Object& object, const char* name) {
221222
}
222223
}
223224

225+
std::string resolveStringProperty(const ofbx::Object& object, const char* name) {
226+
const ofbx::IElement* element = resolveProperty(object, name);
227+
MT_THROW_IF(element == nullptr, "Unable to find property element in {}", object.name);
228+
const ofbx::IElementProperty* x = getElementProperty(element, 4);
229+
MT_THROW_IF(x == nullptr, "Unable to find property {} in {}", name, object.name);
230+
if (x->getType() == ofbx::IElementProperty::STRING) {
231+
char result[65536];
232+
x->getValue().toString(result);
233+
return {result};
234+
} else {
235+
MT_THROW("For property {}, expected string but got {}.", name, propertyTypeStr(x->getType()));
236+
}
237+
}
238+
224239
template <typename VecArray, typename EltType>
225240
VecArray extractPropertyArrayImp(const ofbx::IElementProperty* prop, const char* what) {
226241
using VecType = typename VecArray::value_type;
@@ -1238,6 +1253,25 @@ std::tuple<Character, std::vector<MatrixXf>, float> loadOpenFbx(
12381253
const auto [skeleton, jointFbxNodes, locators, collision] =
12391254
parseSkeleton(scene->getRoot(), {}, permissive);
12401255

1256+
std::string name;
1257+
std::string metadata = "{}"; // empty json string
1258+
if (!jointFbxNodes.empty()) {
1259+
auto* resName = resolveProperty(*jointFbxNodes[0], "name");
1260+
if (resName != nullptr) {
1261+
name = resolveStringProperty(*jointFbxNodes[0], "name");
1262+
}
1263+
auto* resMeta = resolveProperty(*jointFbxNodes[0], "metadata");
1264+
if (resMeta != nullptr) {
1265+
metadata = resolveStringProperty(*jointFbxNodes[0], "metadata");
1266+
}
1267+
}
1268+
// ensure metadata is valid JSON
1269+
try {
1270+
auto json = nlohmann::json::parse(metadata);
1271+
} catch (const nlohmann::json::parse_error& e) {
1272+
MT_LOGW("Failed to parse metadata: {}", e.what());
1273+
}
1274+
12411275
TransformationList inverseBindPoseTransforms;
12421276
for (const auto& j : jointFbxNodes) {
12431277
Eigen::Affine3d mat = Eigen::Affine3d::Identity();
@@ -1284,7 +1318,12 @@ std::tuple<Character, std::vector<MatrixXf>, float> loadOpenFbx(
12841318
skinWeights.get(),
12851319
collision.empty() ? nullptr : &collision,
12861320
nullptr,
1287-
blendShape);
1321+
blendShape,
1322+
{}, // faceExpressionBlendShapes
1323+
name, // nameIn
1324+
{}, // inverseBindPose
1325+
{}, // skinnedLocators
1326+
metadata);
12881327
result.resetJointMap();
12891328
result.inverseBindPose = inverseBindPoseTransforms;
12901329

momentum/io/gltf/gltf_builder.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -926,6 +926,8 @@ void GltfBuilder::addCharacter(
926926
parameterLimitsToJson(character, def["parameterLimits"]);
927927
parameterSetsToJson(character, def["parameterSet"]);
928928
poseConstraintsToJson(character, def["poseConstraints"]);
929+
// add metadata
930+
def["metadata"] = character.metadata;
929931
}
930932
}
931933

momentum/io/gltf/gltf_io.cpp

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -798,6 +798,15 @@ void loadGlobalExtensions(const fx::gltf::Document& model, Character& character)
798798
if (def.count("parameterLimits") > 0) {
799799
character.parameterLimits = parameterLimitsFromJson(character, def["parameterLimits"]);
800800
}
801+
if (def.count("metadata") > 0) {
802+
character.metadata = def.value<std::string>("metadata", {});
803+
// ensure metadata is valid JSON
804+
try {
805+
auto json = nlohmann::json::parse(character.metadata);
806+
} catch (const nlohmann::json::parse_error& e) {
807+
MT_LOGW("Failed to parse metadata: {}", e.what());
808+
}
809+
}
801810
} catch (std::runtime_error& err) {
802811
MT_THROW("Unable to load gltf : {}", err.what());
803812
}

momentum/test/character/character_helpers.cpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,8 @@ CharacterT<T> createTestCharacter(size_t numJoints) {
237237
BlendShapeBase_const_p{},
238238
std::string("test character"),
239239
momentum::TransformationList{},
240-
createDefaultSkinnedLocatorList(skeleton));
240+
createDefaultSkinnedLocatorList(skeleton),
241+
std::string(R"({"name":"testCharacterV1","version":1})"));
241242
}
242243

243244
template CharacterT<float> createTestCharacter(size_t numJoints);

momentum/test/character/character_test.cpp

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,9 @@ TYPED_TEST(CharacterTest, CopyConstructor) {
182182
copiedCharacter.mesh->vertices[0] = Vector3f(99, 99, 99);
183183
EXPECT_NE(copiedCharacter.mesh->vertices[0], this->character.mesh->vertices[0]);
184184
}
185+
186+
// Check that we preserve metadata
187+
EXPECT_EQ(copiedCharacter.metadata, this->character.metadata);
185188
}
186189

187190
// Test assignment operator
@@ -227,6 +230,9 @@ TYPED_TEST(CharacterTest, AssignmentOperator) {
227230
assignedCharacter = temp;
228231
}
229232
EXPECT_EQ(assignedCharacter.name, "assigned");
233+
234+
// Check that we preserve metadata
235+
EXPECT_EQ(assignedCharacter.metadata, this->character.metadata);
230236
}
231237

232238
// Test move constructor
@@ -258,6 +264,9 @@ TYPED_TEST(CharacterTest, MoveConstructor) {
258264
const auto bindPose = movedCharacter.bindPose();
259265
EXPECT_EQ(bindPose.pose.size(), static_cast<Eigen::Index>(originalParamCount));
260266
}
267+
268+
// Check that we preserve metadata
269+
EXPECT_EQ(movedCharacter.metadata, this->character.metadata);
261270
}
262271

263272
// Test move assignment operator
@@ -543,6 +552,9 @@ TYPED_TEST(CharacterTest, SimplifySkeleton) {
543552

544553
// Check that the locators were remapped
545554
EXPECT_EQ(simplifiedCharacter.locators.size(), this->character.locators.size());
555+
556+
// Check that we preserve metadata
557+
EXPECT_EQ(simplifiedCharacter.metadata, this->character.metadata);
546558
}
547559

548560
// Test simplifyParameterTransform method
@@ -573,6 +585,9 @@ TYPED_TEST(CharacterTest, SimplifyParameterTransform) {
573585
EXPECT_EQ(simplifiedCharacter.mesh->vertices.size(), this->character.mesh->vertices.size());
574586
EXPECT_EQ(
575587
simplifiedCharacter.skinWeights->index.rows(), this->character.skinWeights->index.rows());
588+
589+
// Check that we preserve metadata
590+
EXPECT_EQ(simplifiedCharacter.metadata, this->character.metadata);
576591
}
577592

578593
// Test simplify method
@@ -599,6 +614,9 @@ TYPED_TEST(CharacterTest, Simplify) {
599614
// EXPECT_EQ(simplifiedCharacter.parameterTransform.name[0], "root_tx");
600615
// EXPECT_EQ(simplifiedCharacter.parameterTransform.name[1], "root_rx");
601616
// EXPECT_EQ(simplifiedCharacter.parameterTransform.name[2], "joint1_rx");
617+
618+
// Check that we preserve metadata
619+
EXPECT_EQ(simplifiedCharacter.metadata, this->character.metadata);
602620
}
603621

604622
// Test remapSkinWeights method

pymomentum/geometry/character_pybind.cpp

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ void registerCharacterBindings(py::class_<mm::Character>& characterClass) {
6969
// =====================================================
7070
// momentum::Character
7171
// - name
72+
// - metadata
7273
// - skeleton
7374
// - parameter_transform
7475
// - locators
@@ -176,7 +177,8 @@ void registerCharacterBindings(py::class_<mm::Character>& characterClass) {
176177
character.faceExpressionBlendShape,
177178
character.name,
178179
character.inverseBindPose,
179-
character.skinnedLocators);
180+
character.skinnedLocators,
181+
character.metadata);
180182
},
181183
"Adds mesh and skin weight to the character and return a new character instance",
182184
py::arg("mesh"),
@@ -197,7 +199,9 @@ void registerCharacterBindings(py::class_<mm::Character>& characterClass) {
197199
character.blendShape,
198200
character.faceExpressionBlendShape,
199201
character.name,
200-
character.inverseBindPose);
202+
character.inverseBindPose,
203+
character.skinnedLocators,
204+
character.metadata);
201205
},
202206
"Returns a new character with the parameter limits set to the passed-in limits.",
203207
py::arg("parameter_limits"))
@@ -231,7 +235,8 @@ void registerCharacterBindings(py::class_<mm::Character>& characterClass) {
231235
character.faceExpressionBlendShape,
232236
character.name,
233237
character.inverseBindPose,
234-
character.skinnedLocators);
238+
character.skinnedLocators,
239+
character.metadata);
235240
},
236241
R"(Returns a new character with the passed-in locators. If 'replace' is true, the existing locators are replaced, otherwise (the default) the new locators are appended to the existing ones.
237242
@@ -282,7 +287,8 @@ void registerCharacterBindings(py::class_<mm::Character>& characterClass) {
282287
character.faceExpressionBlendShape,
283288
character.name,
284289
character.inverseBindPose,
285-
combinedSkinnedLocators);
290+
combinedSkinnedLocators,
291+
character.metadata);
286292
},
287293
R"(Returns a new character with the passed-in skinned locators. If 'replace' is true, the existing skinned locators are replaced, otherwise (the default) the new skinned locators are appended to the existing ones.
288294
@@ -292,6 +298,7 @@ void registerCharacterBindings(py::class_<mm::Character>& characterClass) {
292298
py::arg("skinned_locators"),
293299
py::arg("replace") = false)
294300
.def_readonly("name", &mm::Character::name, "The character's name.")
301+
.def_readonly("metadata", &mm::Character::metadata, "The character's metadata.")
295302
.def_readonly(
296303
"skeleton", &mm::Character::skeleton, "The character's skeleton. See :class:`Skeleton`.")
297304
.def_readonly(
@@ -401,7 +408,9 @@ It can be used to solve for facial expressions.
401408
c.blendShape,
402409
c.faceExpressionBlendShape,
403410
c.name,
404-
c.inverseBindPose);
411+
c.inverseBindPose,
412+
c.skinnedLocators,
413+
c.metadata);
405414
},
406415
"Returns a new :class:`Character` with the collision geometry replaced.")
407416
.def(

0 commit comments

Comments
 (0)