Skip to content

Commit 0319910

Browse files
committed
Add unified saveCharacter function for FBX/GLTF parity (#753)
Summary: Pull Request resolved: #753 After D85530152 added marker sequence support to fbx, we have functionality parity between fbx and gltf. This diff created a unified `saveCharacterToFile()` function in `character_io` that automatically selects between FBX and GLTF export based on file extension. Added .fbx support loadMarkers as the unified function for loading marker data from any supported format. Differential Revision: D85517572
1 parent 865f093 commit 0319910

File tree

11 files changed

+241
-3
lines changed

11 files changed

+241
-3
lines changed

CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -452,6 +452,7 @@ mt_library(
452452
PRIVATE_LINK_LIBRARIES
453453
io_common
454454
io_gltf
455+
io_fbx
455456
ezc3d
456457
)
457458

momentum/io/character_io.cpp

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
#include "momentum/io/character_io.h"
99

1010
#include "momentum/character/character.h"
11+
#include "momentum/character/skeleton_state.h"
1112
#include "momentum/io/fbx/fbx_io.h"
1213
#include "momentum/io/gltf/gltf_io.h"
1314
#include "momentum/io/shape/pose_shape_io.h"
@@ -139,4 +140,76 @@ Character loadFullCharacterFromBuffer(
139140
return character.value();
140141
}
141142

143+
void saveCharacter(
144+
const filesystem::path& filename,
145+
const Character& character,
146+
const MatrixXf& motion,
147+
const VectorXf& offsets,
148+
const std::vector<std::vector<Marker>>& markerSequence,
149+
float fps) {
150+
// Parse format from file extension
151+
const auto format = parseCharacterFormat(filename);
152+
MT_THROW_IF(
153+
format == CharacterFormat::Unknown,
154+
"Unknown character format for path: {}. Supported formats: .fbx, .glb, .gltf",
155+
filename.string());
156+
157+
if (format == CharacterFormat::Gltf) {
158+
saveGltfCharacter(
159+
filename,
160+
character,
161+
fps,
162+
{character.parameterTransform.name, motion},
163+
{character.skeleton.getJointNames(), offsets},
164+
markerSequence);
165+
} else if (format == CharacterFormat::Fbx) {
166+
// Save as FBX
167+
saveFbx(
168+
filename,
169+
character,
170+
motion,
171+
offsets,
172+
static_cast<double>(fps),
173+
true, // saveMesh
174+
FBXCoordSystemInfo(),
175+
false, // permissive
176+
markerSequence);
177+
} else {
178+
MT_THROW(
179+
"{} is not a supported format. Supported formats: .fbx, .glb, .gltf", filename.string());
180+
}
181+
}
182+
183+
void saveCharacter(
184+
const filesystem::path& filename,
185+
const Character& character,
186+
std::span<const SkeletonState> skeletonStates,
187+
const std::vector<std::vector<Marker>>& markerSequence,
188+
float fps) {
189+
// Parse format from file extension
190+
const auto format = parseCharacterFormat(filename);
191+
MT_THROW_IF(
192+
format == CharacterFormat::Unknown,
193+
"Unknown character format for path: {}. Supported formats: .fbx, .glb, .gltf",
194+
filename.string());
195+
196+
if (format == CharacterFormat::Gltf) {
197+
saveGltfCharacter(filename, character, fps, skeletonStates, markerSequence);
198+
} else if (format == CharacterFormat::Fbx) {
199+
// Save as FBX
200+
saveFbxWithSkelState(
201+
filename,
202+
character,
203+
skeletonStates,
204+
static_cast<double>(fps),
205+
true, // saveMesh
206+
FBXCoordSystemInfo(),
207+
false, // permissive
208+
markerSequence);
209+
} else {
210+
MT_THROW(
211+
"{} is not a supported format. Supported formats: .fbx, .glb, .gltf", filename.string());
212+
}
213+
}
214+
142215
} // namespace momentum

momentum/io/character_io.h

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,13 @@
88
#pragma once
99

1010
#include <momentum/character/fwd.h>
11+
#include <momentum/character/marker.h>
12+
#include <momentum/common/filesystem.h>
1113
#include <momentum/math/fwd.h>
1214
#include <momentum/math/types.h>
1315

1416
#include <string>
17+
#include <vector>
1518

1619
namespace momentum {
1720

@@ -53,4 +56,28 @@ enum class CharacterFormat : uint8_t {
5356
std::span<const std::byte> paramBuffer = std::span<const std::byte>(),
5457
std::span<const std::byte> locBuffer = std::span<const std::byte>());
5558

59+
/// High level function to save a character with motion and markers to any supported format.
60+
///
61+
/// The format is determined by the file extension (.fbx, .glb, .gltf).
62+
/// This is a unified interface that automatically selects between FBX and GLTF based on extension.
63+
/// @param[in] filename The path where the character file will be saved.
64+
/// @param[in] character The Character object to save.
65+
/// @param[in] motion The motion represented in model parameters (numModelParams, numFrames).
66+
/// @param[in] offsets Offset values per joint capturing skeleton bone lengths (7*numJoints, 1).
67+
/// @param[in] markerSequence Optional marker sequence data to save with the character.
68+
/// @param[in] fps Frame rate for the animation (default: 120.0).
69+
void saveCharacter(
70+
const filesystem::path& filename,
71+
const Character& character,
72+
const MatrixXf& motion /* = MatrixXf()*/,
73+
const VectorXf& offsets = VectorXf(),
74+
const std::vector<std::vector<Marker>>& markerSequence = {},
75+
float fps = 120.0f);
76+
77+
void saveCharacter(
78+
const filesystem::path& filename,
79+
const Character& Character,
80+
std::span<const SkeletonState> skeletonStates,
81+
const std::vector<std::vector<Marker>>& markerSequence,
82+
float fps = 120.0f);
5683
} // namespace momentum

momentum/io/fbx/fbx_io.cpp

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -849,6 +849,39 @@ void saveFbxWithJointParams(
849849
fbxNamespace);
850850
}
851851

852+
void saveFbxWithSkelState(
853+
const filesystem::path& filename,
854+
const Character& character,
855+
std::span<const SkeletonState> skeletonStates,
856+
const double framerate,
857+
const bool saveMesh,
858+
const FBXCoordSystemInfo& coordSystemInfo,
859+
bool permissive,
860+
const std::vector<std::vector<Marker>>& markerSequence,
861+
std::string_view fbxNamespace) {
862+
const size_t nFrames = skeletonStates.size();
863+
MatrixXf jointParams(character.parameterTransform.zero().v.size(), nFrames);
864+
for (size_t iFrame = 0; iFrame < nFrames; ++iFrame) {
865+
jointParams.col(iFrame) =
866+
skeletonStateToJointParameters(skeletonStates[iFrame], character.skeleton).v;
867+
}
868+
869+
// Call the helper function to save FBX file with joint values.
870+
// Set skipActiveJointParamCheck=true to skip the active joint param check as the joint params are
871+
// passed in directly from user.
872+
saveFbxCommon(
873+
filename,
874+
character,
875+
jointParams,
876+
framerate,
877+
saveMesh,
878+
true,
879+
coordSystemInfo,
880+
permissive,
881+
markerSequence,
882+
fbxNamespace);
883+
}
884+
852885
void saveFbxModel(
853886
const filesystem::path& filename,
854887
const Character& character,

momentum/io/fbx/fbx_io.h

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,17 @@ void saveFbxWithJointParams(
125125
const std::vector<std::vector<Marker>>& markerSequence = {},
126126
std::string_view fbxNamespace = "");
127127

128+
void saveFbxWithSkelState(
129+
const filesystem::path& filename,
130+
const Character& character,
131+
std::span<const SkeletonState> skeletonStates,
132+
double framerate = 120.0,
133+
bool saveMesh = false,
134+
const FBXCoordSystemInfo& coordSystemInfo = FBXCoordSystemInfo(),
135+
bool permissive = false,
136+
const std::vector<std::vector<Marker>>& markerSequence = {},
137+
std::string_view fbxNamespace = "");
138+
128139
/// Save a character model (skeleton and mesh) without animation.
129140
/// @param filename Path to the output FBX file
130141
/// @param character The character to save

momentum/io/gltf/gltf_io.cpp

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1308,7 +1308,7 @@ MarkerSequence loadMarkerSequence(const filesystem::path& filename) {
13081308
return result;
13091309
}
13101310

1311-
void saveCharacter(
1311+
void saveGltfCharacter(
13121312
const filesystem::path& filename,
13131313
const Character& character,
13141314
const float fps,
@@ -1326,6 +1326,19 @@ void saveCharacter(
13261326
}
13271327

13281328
void saveCharacter(
1329+
const filesystem::path& filename,
1330+
const Character& character,
1331+
const float fps,
1332+
const MotionParameters& motion,
1333+
const IdentityParameters& offsets,
1334+
const std::vector<std::vector<Marker>>& markerSequence,
1335+
const GltfFileFormat fileFormat,
1336+
const GltfOptions& options) {
1337+
return saveGltfCharacter(
1338+
filename, character, fps, motion, offsets, markerSequence, fileFormat, options);
1339+
}
1340+
1341+
void saveGltfCharacter(
13291342
const filesystem::path& filename,
13301343
const Character& character,
13311344
const float fps,
@@ -1341,6 +1354,18 @@ void saveCharacter(
13411354
GltfBuilder::save(model, filename, fileFormat, kEmbedResources);
13421355
}
13431356

1357+
void saveCharacter(
1358+
const filesystem::path& filename,
1359+
const Character& character,
1360+
const float fps,
1361+
std::span<const SkeletonState> skeletonStates,
1362+
const std::vector<std::vector<Marker>>& markerSequence,
1363+
const GltfFileFormat fileFormat,
1364+
const GltfOptions& options) {
1365+
return saveGltfCharacter(
1366+
filename, character, fps, skeletonStates, markerSequence, fileFormat, options);
1367+
}
1368+
13441369
std::vector<std::byte> saveCharacterToBytes(
13451370
const Character& character,
13461371
float fps,

momentum/io/gltf/gltf_io.h

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,22 @@ void saveCharacter(
124124
GltfFileFormat fileFormat = GltfFileFormat::Extension,
125125
const GltfOptions& options = GltfOptions());
126126

127+
/// Saves character motion to a glb file.
128+
///
129+
/// @param[in] motion The model parameters representing the motion of the character (numModelParams,
130+
/// numFrames)
131+
/// @param[in] offsets Offset values per joint capturing the skeleton bone lengths using translation
132+
/// and scale offset (7*numJoints, 1)
133+
void saveGltfCharacter(
134+
const filesystem::path& filename,
135+
const Character& Character,
136+
float fps = 120.0f,
137+
const MotionParameters& motion = {},
138+
const IdentityParameters& offsets = {},
139+
const std::vector<std::vector<Marker>>& markerSequence = {},
140+
GltfFileFormat fileFormat = GltfFileFormat::Extension,
141+
const GltfOptions& options = GltfOptions());
142+
127143
/// Saves character skeleton states to a glb file.
128144
///
129145
/// @param[in] skeletonStates The skeleton states for each frame of the motion sequence (numFrames,
@@ -137,6 +153,19 @@ void saveCharacter(
137153
GltfFileFormat fileFormat = GltfFileFormat::Extension,
138154
const GltfOptions& options = GltfOptions());
139155

156+
/// Saves character skeleton states to a glb file.
157+
///
158+
/// @param[in] skeletonStates The skeleton states for each frame of the motion sequence (numFrames,
159+
/// numJoints, 8)
160+
void saveGltfCharacter(
161+
const filesystem::path& filename,
162+
const Character& Character,
163+
float fps,
164+
std::span<const SkeletonState> skeletonStates,
165+
const std::vector<std::vector<Marker>>& markerSequence = {},
166+
GltfFileFormat fileFormat = GltfFileFormat::Extension,
167+
const GltfOptions& options = GltfOptions());
168+
140169
std::vector<std::byte> saveCharacterToBytes(
141170
const Character& character,
142171
float fps = 120.0f,

momentum/io/marker/marker_io.cpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
#include "momentum/character/marker.h"
1111
#include "momentum/common/filesystem.h"
1212
#include "momentum/common/log.h"
13+
#include "momentum/io/fbx/fbx_io.h"
1314
#include "momentum/io/gltf/gltf_io.h"
1415
#include "momentum/io/marker/c3d_io.h"
1516
#include "momentum/io/marker/trc_io.h"
@@ -40,6 +41,8 @@ std::vector<MarkerSequence> loadMarkers(
4041
return {loadTrc(filename, up)};
4142
} else if (ext == ".glb") {
4243
return {loadMarkerSequence(filename)};
44+
} else if (ext == ".fbx") {
45+
return {loadFbxMarkerSequence(filename)};
4346
} else {
4447
MT_LOGE("{} Unknown marker file type {}", __func__, filename);
4548
return {};

pymomentum/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ mt_python_binding(
158158
LINK_LIBRARIES
159159
character
160160
character_test_helpers
161+
io
161162
io_fbx
162163
io_gltf
163164
io_legacy_json

pymomentum/geometry/momentum_io.cpp

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,17 @@
1717
#include <momentum/io/gltf/gltf_io.h>
1818
#include <momentum/io/marker/marker_io.h>
1919

20+
// Forward declaration for unified save function
21+
namespace momentum {
22+
void saveCharacter(
23+
const filesystem::path& filename,
24+
const Character& character,
25+
const MatrixXf& motion,
26+
const VectorXf& offsets,
27+
const std::vector<std::vector<Marker>>& markerSequence,
28+
float fps);
29+
} // namespace momentum
30+
2031
namespace pymomentum {
2132

2233
momentum::Character loadGLTFCharacterFromFile(const std::string& path) {
@@ -117,7 +128,7 @@ void saveGLTFCharacterToFile(
117128
poses.rows(),
118129
poses.cols());
119130
}
120-
momentum::saveCharacter(
131+
momentum::saveGltfCharacter(
121132
path,
122133
character,
123134
fps,
@@ -147,7 +158,7 @@ void saveGLTFCharacterToFileFromSkelStates(
147158
std::vector<momentum::SkeletonState> skeletonStates =
148159
arrayToSkeletonStates(skel_states, character);
149160

150-
momentum::saveCharacter(
161+
momentum::saveGltfCharacter(
151162
path,
152163
character,
153164
fps,
@@ -217,6 +228,22 @@ void saveFBXCharacterToFileWithJointParams(
217228
}
218229
}
219230

231+
void saveCharacter(
232+
const std::string& path,
233+
const momentum::Character& character,
234+
const float fps,
235+
const std::optional<const Eigen::MatrixXf>& motion,
236+
const std::optional<const Eigen::VectorXf>& offsets,
237+
const std::optional<const std::vector<std::vector<momentum::Marker>>>& markers) {
238+
momentum::saveCharacter(
239+
path,
240+
character,
241+
motion.has_value() ? motion.value().transpose() : Eigen::MatrixXf{},
242+
offsets.value_or(Eigen::VectorXf{}),
243+
markers.value_or(std::vector<std::vector<momentum::Marker>>{}),
244+
fps);
245+
}
246+
220247
std::tuple<momentum::Character, RowMatrixf, Eigen::VectorXf, float> loadGLTFCharacterWithMotion(
221248
const std::string& gltfFilename) {
222249
const auto [character, motion, identity, fps] = momentum::loadCharacterWithMotion(gltfFilename);

0 commit comments

Comments
 (0)