Skip to content

Commit 2ce2149

Browse files
committed
Add unified saveCharacter function for FBX/GLTF parity
Summary: Created a high-level saveCharacter() function in character_io that automatically selects between FBX and GLTF export based on file extension. This achieves feature parity between the two formats for saving characters with motion and marker sequences. Key changes: - Added saveCharacter() overload that takes a filesystem path - Function automatically detects format from extension (.fbx, .glb, .gltf) - Routes to appropriate format-specific save function (saveFbx or GLTF saveCharacter) - Supports motion, offsets, and marker sequences for both formats - Provides clear error messages for unknown/unsupported formats Differential Revision: D85517572
1 parent 865f093 commit 2ce2149

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)