Skip to content

Commit 4f39839

Browse files
yutingyefacebook-github-bot
authored andcommitted
Add unified saveCharacter function for FBX/GLTF parity (#753)
Summary: After D85530152 added marker sequence support to fbx, we have functionality parity between fbx and gltf. This diff created a unified `saveCharacter()` function in `character_io` that automatically selects between FBX and GLTF export based on file extension. Added .fbx support to `loadMarkers()` as the unified function for loading marker data from any supported format. Added an additional unified `saveCharacter()` function in `character_io` that takes skeleton states as input instead of model parameters. Rename `saveCharacter()` in `gltf_io` to `saveGltfCharacter()`, but keeping the existing ones for backwards compatibility for now. It will be removed in a later diff when downstreams are updated. Differential Revision: D85517572
1 parent 570c6da commit 4f39839

File tree

13 files changed

+281
-7
lines changed

13 files changed

+281
-7
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+
saveFbxWithSkeletonState(
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 saveFbxWithSkeletonState(
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: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,28 @@ void saveFbxWithJointParams(
125125
const std::vector<std::vector<Marker>>& markerSequence = {},
126126
std::string_view fbxNamespace = "");
127127

128+
/// Save a character with animation using skeleton states directly.
129+
/// @param filename Path to the output FBX file
130+
/// @param character The character to save
131+
/// @param skeletonStates SkeletonState for each frame (empty for bind pose only)
132+
/// @param framerate Animation framerate in frames per second
133+
/// @param saveMesh Whether to include mesh geometry in the output
134+
/// @param coordSystemInfo Coordinate system configuration for the FBX file
135+
/// @param permissive Permissive mode allows saving mesh-only characters (without skin weights)
136+
/// @param markerSequence Optional marker sequence data to save with the character
137+
/// @param fbxNamespace Optional namespace to prepend to all node names (e.g., "ns" will become
138+
/// "ns:")
139+
void saveFbxWithSkeletonState(
140+
const filesystem::path& filename,
141+
const Character& character,
142+
std::span<const SkeletonState> skeletonStates,
143+
double framerate = 120.0,
144+
bool saveMesh = false,
145+
const FBXCoordSystemInfo& coordSystemInfo = FBXCoordSystemInfo(),
146+
bool permissive = false,
147+
const std::vector<std::vector<Marker>>& markerSequence = {},
148+
std::string_view fbxNamespace = "");
149+
128150
/// Save a character model (skeleton and mesh) without animation.
129151
/// @param filename Path to the output FBX file
130152
/// @param character The character to save

momentum/io/fbx/fbx_io_openfbx_only.cpp

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,20 @@ void saveFbxWithJointParams(
7474
"FBX saving is not supported in OpenFBX-only mode. FBX loading is available via OpenFBX, but saving requires the full Autodesk FBX SDK.");
7575
}
7676

77+
void saveFbxWithSkeletonState(
78+
const filesystem::path& /* filename */,
79+
const Character& /* character */,
80+
std::span<const SkeletonState> /* skeletonStates */,
81+
double /* framerate */,
82+
bool /* saveMesh */,
83+
const FBXCoordSystemInfo& /* coordSystemInfo */,
84+
bool /* permissive */,
85+
const std::vector<std::vector<Marker>>& /* markerSequence */,
86+
std::string_view /* fbxNamespace */) {
87+
MT_THROW(
88+
"FBX saving is not supported in OpenFBX-only mode. FBX loading is available via OpenFBX, but saving requires the full Autodesk FBX SDK.");
89+
}
90+
7791
void saveFbxModel(
7892
const filesystem::path& /* filename */,
7993
const Character& /* character */,

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: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -114,23 +114,52 @@ fx::gltf::Document makeCharacterDocument(
114114
/// numFrames)
115115
/// @param[in] offsets Offset values per joint capturing the skeleton bone lengths using translation
116116
/// and scale offset (7*numJoints, 1)
117-
void saveCharacter(
117+
[[deprecated("Use saveGltfCharacter() instead")]] void saveCharacter(
118118
const filesystem::path& filename,
119-
const Character& Character,
119+
const Character& character,
120120
float fps = 120.0f,
121121
const MotionParameters& motion = {},
122122
const IdentityParameters& offsets = {},
123123
const std::vector<std::vector<Marker>>& markerSequence = {},
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+
143+
/// Saves character skeleton states to a glb file.
144+
///
145+
/// @param[in] skeletonStates The skeleton states for each frame of the motion sequence (numFrames,
146+
/// numJoints, 8)
147+
[[deprecated("Use saveGltfCharacter() instead")]] void saveCharacter(
148+
const filesystem::path& filename,
149+
const Character& character,
150+
float fps,
151+
std::span<const SkeletonState> skeletonStates,
152+
const std::vector<std::vector<Marker>>& markerSequence = {},
153+
GltfFileFormat fileFormat = GltfFileFormat::Extension,
154+
const GltfOptions& options = GltfOptions());
155+
127156
/// Saves character skeleton states to a glb file.
128157
///
129158
/// @param[in] skeletonStates The skeleton states for each frame of the motion sequence (numFrames,
130159
/// numJoints, 8)
131-
void saveCharacter(
160+
void saveGltfCharacter(
132161
const filesystem::path& filename,
133-
const Character& Character,
162+
const Character& character,
134163
float fps,
135164
std::span<const SkeletonState> skeletonStates,
136165
const std::vector<std::vector<Marker>>& markerSequence = {},

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

0 commit comments

Comments
 (0)