Skip to content

Commit 2ab5106

Browse files
yutingyemeta-codesync[bot]
authored andcommitted
Add a simple example to export an animated sequence as objs (#742)
Summary: Pull Request resolved: #742 An example to export deformed character mesh as objs. It shows how to manipulate character states from either glb or fbx files. Reviewed By: jeongseok-meta Differential Revision: D62511544 fbshipit-source-id: 7a00caef3d962774b4a8dd9225f06a1413f7171d
1 parent e536797 commit 2ab5106

File tree

5 files changed

+375
-0
lines changed

5 files changed

+375
-0
lines changed

CMakeLists.txt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -864,6 +864,17 @@ if(MOMENTUM_BUILD_EXAMPLES)
864864
io
865865
marker_tracker
866866
)
867+
868+
mt_executable(
869+
NAME export_objs
870+
SOURCES_VARS export_objs_sources
871+
LINK_LIBRARIES
872+
character
873+
io_fbx
874+
io_gltf
875+
CLI11::CLI11
876+
)
877+
867878
endif()
868879

869880
#===============================================================================

cmake/build_variables.bzl

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -677,6 +677,10 @@ animate_shapes_sources = [
677677
"examples/animate_shapes/animate_shapes.cpp",
678678
]
679679

680+
export_objs_sources = [
681+
"examples/export_objs/export_objs.cpp",
682+
]
683+
680684
process_markers_app_sources = [
681685
"examples/process_markers_app/process_markers_app.cpp",
682686
]
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
# Export OBJs Example
2+
3+
This example demonstrates how to export animation data from GLB/GLTF or FBX files as per-frame OBJ mesh files.
4+
5+
## Overview
6+
7+
The `export_objs` utility loads a character with animation from either:
8+
- **GLB/GLTF files**: Using the Momentum GLTF loader
9+
- **FBX files**: Using the Momentum FBX loader
10+
11+
It then exports each frame of the animation as a separate OBJ file containing the deformed mesh geometry. The output folder will be created if it doesn't exist yet.
12+
13+
## Building with buck
14+
15+
```bash
16+
buck2 build path/to/momentum/examples:export_objs
17+
```
18+
19+
## Usage
20+
21+
Using buck:
22+
```bash
23+
buck2 run path/to/momentum/examples:export_objs -- \
24+
-i <input_file> \
25+
-o <output_folder> \
26+
[--first <frame_number>] \
27+
[--last <frame_number>] \
28+
[--stride <frame_stride>]
29+
```
30+
31+
Using pixi:
32+
```
33+
pixi run export_objs -i <input_file> -o <output_folder> [--first <frame_number>] [--last <frame_number>] [--stride <frame_stride>]
34+
```
35+
36+
### Options
37+
38+
- `-i, --input`: Path to the input animation file (`.fbx`, `.glb`, or `.gltf`) [required]
39+
- `-o, --output`: Path to the output folder where OBJ files will be saved [required]
40+
- `--first`: First frame to export (default: 0)
41+
- `--last`: Last frame to export, inclusive (default: -1 for all frames)
42+
- `--stride`: Frame stride when exporting (default: 1)
43+
44+
### Examples
45+
46+
**Export all frames from a GLB file:**
47+
48+
Using buck:
49+
```bash
50+
buck2 run path/to/momentum/examples:export_objs -- \
51+
-i test.glb \
52+
-o /tmp/exported_objs
53+
```
54+
55+
Using pixi:
56+
```
57+
pixi run export_objs -i test.glb -o /tmp/exported_objs
58+
```
59+
60+
**Export frames 10-50 from an FBX file with stride 2:**
61+
62+
Using buck:
63+
```bash
64+
buck2 run path/to/momentum/examples:export_objs -- \
65+
-i path/to/animation.fbx \
66+
-o /tmp/exported_objs \
67+
--first 10 \
68+
--last 50 \
69+
--stride 2
70+
```
71+
72+
Using pixi:
73+
```
74+
pixi run export_objs -i path/to/animation.fbx -o /tmp/exported_objs --first 10 --last 50 --stride 2
75+
```
76+
77+
**Export static mesh (no animation):**
78+
If the input file has no animation data, it will export a single OBJ file with the template mesh.
79+
80+
## Output Format
81+
82+
### Animation Sequences
83+
The exported OBJ files are named sequentially:
84+
- `00000.obj`, `00001.obj`, `00002.obj`, etc.
85+
86+
### Static Mesh (No Animation)
87+
If the input file has no animation data, a single OBJ file is exported with the name:
88+
- `<input_filename>.obj` (e.g., `character.obj` for `character.glb`)
89+
90+
### OBJ File Contents
91+
Each OBJ file contains:
92+
- Vertex positions (`v` lines)
93+
- Triangle faces (`f` lines, 1-indexed)
94+
95+
Note: The simple OBJ exporter in this example does not export:
96+
- Texture coordinates
97+
- Normals
98+
- Materials
99+
100+
## Implementation Notes
101+
102+
### GLB/GLTF Files
103+
- Loads character with motion using `loadCharacterWithMotion()`
104+
- Returns motion as a single `MatrixXf` containing **model parameters**
105+
- Also returns a separate identity parameter (stored in `JointParameters`)
106+
- The identity parameter is constant for the character and not time-varying
107+
- Each column represents one frame's model parameters
108+
109+
### FBX Files
110+
- Loads character with motion using `loadFbxCharacterWithMotion()`
111+
- Returns motion as a `std::vector<MatrixXf>` containing **joint parameters**
112+
- If multiple motions are present, only the first one is exported
113+
- FBX files do not store custom parameter information, so only joint parameters are available
114+
115+
### Parameter Handling Difference
116+
The key difference between GLB and FBX files:
117+
- **GLB**: Motion matrix contains model parameters (stored in custom plugin); identity parameters are separate
118+
- **FBX**: Motion matrix contains joint parameters (no custom storage); model parameters are not available
119+
120+
This affects how parameters are set in `CharacterState`:
121+
- For GLB: `params.offsets = id; params.pose = motion.col(iFrame)` (model parameters)
122+
- For FBX: `params.pose.v.setZero(0); params.offsets = motion.col(iFrame)` (joint parameters)
123+
124+
### Per-Frame Export Process
125+
1. Load the character and animation
126+
2. For each frame:
127+
- Set the character state with the appropriate parameters (model or joint)
128+
- Compute the deformed mesh geometry
129+
- Export the mesh to an OBJ file
130+
131+
## See Also
132+
133+
- `examples/glb_viewer/`: Interactive viewer for GLB files with Rerun
134+
- `examples/fbx_viewer/`: Interactive viewer for FBX files with Rerun
135+
- `examples/convert_model/`: Convert between different model formats
Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
/**
9+
* Export OBJs Example
10+
*
11+
* This example demonstrates how to export animation data from GLB/GLTF or FBX files
12+
* as per-frame OBJ mesh files. It loads a character with animation and exports each
13+
* frame as a separate OBJ file containing the deformed mesh geometry.
14+
*
15+
* Supported formats:
16+
* - GLB/GLTF: Loaded using loadCharacterWithMotion()
17+
* - FBX: Loaded using loadFbxCharacterWithMotion()
18+
*
19+
* Usage:
20+
* export_objs -i <input_file> -o <output_folder> [options]
21+
*
22+
* See README.md for detailed documentation.
23+
*/
24+
25+
#include <momentum/character/character.h>
26+
#include <momentum/character/character_state.h>
27+
#include <momentum/common/log.h>
28+
#include <momentum/io/fbx/fbx_io.h>
29+
#include <momentum/io/gltf/gltf_io.h>
30+
#include <momentum/math/mesh.h>
31+
32+
#include <CLI/CLI.hpp>
33+
#include <fmt/format.h>
34+
35+
#include <filesystem>
36+
37+
using namespace momentum;
38+
39+
namespace {
40+
41+
struct Options {
42+
std::string inputFile;
43+
std::string outputFolder;
44+
size_t firstFrame = 0;
45+
int lastFrame = -1;
46+
size_t stride = 1;
47+
};
48+
49+
std::shared_ptr<Options> setupOptions(CLI::App& app) {
50+
auto opt = std::make_shared<Options>();
51+
app.add_option("-i,--input", opt->inputFile, "Path to the input animation file (.fbx/.glb).")
52+
->required()
53+
->check(CLI::ExistingFile);
54+
app.add_option("-o,--output", opt->outputFolder, "Path to the output folder.")->required();
55+
app.add_option("--first", opt->firstFrame, "First frame in the motion to start obj export.")
56+
->default_val(opt->firstFrame)
57+
->check(CLI::NonNegativeNumber);
58+
app.add_option(
59+
"--last",
60+
opt->lastFrame,
61+
"Last frame in the motion to export (inclusive). -1 to indicate the last frame in the motion.")
62+
->default_val(opt->lastFrame);
63+
app.add_option("--stride", opt->stride, "Frame stride when exporting data.")
64+
->default_val(opt->stride)
65+
->check(CLI::PositiveNumber);
66+
67+
return opt;
68+
}
69+
70+
// A simple obj export function as an example to avoid external deps.
71+
int saveObj(const std::string& filename, const Mesh* mesh) {
72+
std::ofstream file(filename);
73+
if (!file.is_open()) {
74+
MT_LOGE("Failed to open {} for writing", filename);
75+
return -1;
76+
}
77+
78+
// Simple info
79+
file << "# " << mesh->vertices.size() << " vertices; " << mesh->faces.size() << " faces"
80+
<< std::endl;
81+
82+
// Vertex positions
83+
for (const auto& v : mesh->vertices) {
84+
file << "v " << v(0) << " " << v(1) << " " << v(2) << std::endl;
85+
}
86+
file << std::endl;
87+
88+
// Faces -- our mesh is triangle only.
89+
// NOTE: obj vertex indices is 1-based not 0-based.
90+
for (const auto& f : mesh->faces) {
91+
file << "f " << 1 + f(0) << " " << 1 + f(1) << " " << 1 + f(2) << std::endl;
92+
}
93+
file.close();
94+
return 0;
95+
}
96+
97+
} // namespace
98+
99+
int main(int argc, char* argv[]) try {
100+
CLI::App app("Export objs app");
101+
auto opts = setupOptions(app);
102+
CLI11_PARSE(app, argc, argv);
103+
104+
// Determine file type by extension
105+
const std::string extension = std::filesystem::path(opts->inputFile).extension().string();
106+
const bool isGlb = (extension == ".glb" || extension == ".gltf");
107+
const bool isFbx = (extension == ".fbx");
108+
109+
if (!isGlb && !isFbx) {
110+
MT_LOGE("Unsupported file format: {}. Only .glb, .gltf, and .fbx are supported.", extension);
111+
return EXIT_FAILURE;
112+
}
113+
114+
Character character;
115+
MatrixXf motion; // For GLB: single motion matrix
116+
std::vector<MatrixXf> motions; // For FBX: vector of motion matrices
117+
VectorXf id; // Identity parameters (GLB only)
118+
float fps = 0.0f;
119+
120+
// Load character and motion based on file type
121+
if (isGlb) {
122+
MT_LOGI("Loading GLB/GLTF file: {}", opts->inputFile);
123+
std::tie(character, motion, id, fps) = loadCharacterWithMotion(opts->inputFile);
124+
} else {
125+
MT_LOGI("Loading FBX file: {}", opts->inputFile);
126+
std::tie(character, motions, fps) = loadFbxCharacterWithMotion(opts->inputFile);
127+
// Convert vector of motions to single motion matrix if needed
128+
if (!motions.empty() && motions[0].cols() > 0) {
129+
motion = motions[0]; // Use first motion
130+
if (motions.size() > 1) {
131+
MT_LOGW("FBX file contains {} motions. Using only the first one.", motions.size());
132+
}
133+
}
134+
}
135+
136+
if (character.mesh == nullptr) {
137+
MT_LOGW("No mesh found in the input; exit without saving.");
138+
return EXIT_SUCCESS;
139+
}
140+
141+
// Check and create the output folder if needed
142+
if (!std::filesystem::is_directory(opts->outputFolder)) {
143+
MT_LOGI("Create output folder {}", opts->outputFolder);
144+
std::filesystem::create_directories(opts->outputFolder);
145+
}
146+
147+
const size_t numFrames = motion.cols();
148+
149+
if (numFrames == 0) {
150+
// Export the template mesh (no animation)
151+
MT_LOGI("No animation data found. Exporting template mesh.");
152+
const std::string outFile = fmt::format(
153+
"{}/{}.obj",
154+
opts->outputFolder,
155+
std::filesystem::path(opts->inputFile).filename().stem().string());
156+
if (saveObj(outFile, character.mesh.get()) == 0) {
157+
return EXIT_SUCCESS;
158+
} else {
159+
return EXIT_FAILURE;
160+
}
161+
}
162+
163+
// Export animation sequence
164+
MT_LOGI("Exporting animation with {} frames at {} fps", numFrames, fps);
165+
166+
// Apply frame range options
167+
const size_t startFrame = opts->firstFrame;
168+
const size_t endFrame = (opts->lastFrame < 0)
169+
? numFrames
170+
: std::min(static_cast<size_t>(opts->lastFrame + 1), numFrames);
171+
172+
if (startFrame >= numFrames) {
173+
MT_LOGE("First frame ({}) is out of range (total frames: {})", startFrame, numFrames);
174+
return EXIT_FAILURE;
175+
}
176+
177+
CharacterState state(character);
178+
CharacterParameters params;
179+
if (isGlb && id.size() > 0) {
180+
// id is in JointParameters. It is constant for the character and not time-varying.
181+
params.offsets = id;
182+
} else if (isFbx) {
183+
params.pose.v.setZero(character.parameterTransform.numAllModelParameters()); // should be zero
184+
}
185+
186+
size_t exportCount = 0;
187+
for (size_t iFrame = startFrame; iFrame < endFrame; iFrame += opts->stride) {
188+
// Here is the tricky part: for glb files, we store model parameters in a custom plugin and
189+
// read it back in. So the motion matrix we read from glb is of ModelParameters type. For fbx
190+
// files, we do not store custom information, so we can only read back joint parameters. The
191+
// motion matrix we read from fbx is of JointParameters type. We will need to handle them
192+
// differently.
193+
if (isGlb) {
194+
params.pose = motion.col(iFrame);
195+
} else {
196+
params.offsets = motion.col(iFrame);
197+
}
198+
state.set(params, character, true, false, false);
199+
const std::string outFile = fmt::format("{}/{:05}.obj", opts->outputFolder, exportCount);
200+
if (saveObj(outFile, state.meshState.get()) == 0) {
201+
exportCount++;
202+
}
203+
}
204+
MT_LOGI("Exported {} frames to {}", exportCount, opts->outputFolder);
205+
return EXIT_SUCCESS;
206+
} catch (std::exception& e) {
207+
MT_LOGE("Exception thrown {}", e.what());
208+
return EXIT_FAILURE;
209+
} catch (...) {
210+
MT_LOGE("Unknown exception.");
211+
return EXIT_FAILURE;
212+
}

0 commit comments

Comments
 (0)