Skip to content

Commit 8fd6455

Browse files
cdtwiggfacebook-github-bot
authored andcommitted
pybind GLTFBuilder. (#566)
Summary: Pull Request resolved: #566 We want to be able to save multiple characters to the same GLTF file, but the current Python code doesn't support this. However on the C++ side we have the GLTFBuilder class that allows us to add all kinds of things to GLTF files in including characters, motion, markers and meshes. Let's bind it out. Putting them in a separate CPP file because geometry_pybind.cpp is getting too big and this worked well for solver2_pybind. Reviewed By: jeongseok-meta Differential Revision: D82321865 fbshipit-source-id: f57fe9b0b952699e08289efc943847dde1ad0b0d
1 parent 3a5ae98 commit 8fd6455

File tree

6 files changed

+406
-41
lines changed

6 files changed

+406
-41
lines changed

pymomentum/cmake/build_variables.bzl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,12 +95,14 @@ tensor_ik_test_sources = [
9595
geometry_public_headers = [
9696
"geometry/momentum_geometry.h",
9797
"geometry/momentum_io.h",
98+
"geometry/gltf_builder_pybind.h",
9899
]
99100

100101
geometry_sources = [
101102
"geometry/geometry_pybind.cpp",
102103
"geometry/momentum_geometry.cpp",
103104
"geometry/momentum_io.cpp",
105+
"geometry/gltf_builder_pybind.cpp",
104106
]
105107

106108
solver_public_headers = [

pymomentum/geometry/geometry_pybind.cpp

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
* LICENSE file in the root directory of this source tree.
66
*/
77

8+
#include "pymomentum/geometry/gltf_builder_pybind.h"
89
#include "pymomentum/geometry/momentum_geometry.h"
910
#include "pymomentum/geometry/momentum_io.h"
1011
#include "pymomentum/tensor_momentum/tensor_blend_shape.h"
@@ -832,7 +833,7 @@ and doesn't require that the Character have a valid parameter transform. Unlike
832833
support the proprietary momentum motion format for storing model parameters in GLB.
833834
834835
:param gltf_filename: A .gltf file; e.g. character_s0.glb.
835-
:return: a tuple [Character, skel_states, fps], where skel_states is the tensor [nFrames x nJoints x 8].
836+
:return: a tuple [Character, skel_states, timestamps], where skel_states is the tensor [n_frames x n_joints x 8] and timestamps is [n_frames]
836837
)",
837838
py::arg("gltf_filename"))
838839

@@ -3132,4 +3133,7 @@ The character has only one parameter limit: min-max type [-0.1, 0.1] for root.
31323133
R"(Create a pose prior that acts on the simple 3-joint test character.
31333134
31343135
:return: A simple pose prior.)");
3136+
3137+
// Register GltfBuilder bindings
3138+
registerGltfBuilderBindings(m);
31353139
}
Lines changed: 314 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,314 @@
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+
#include "pymomentum/geometry/gltf_builder_pybind.h"
9+
#include "pymomentum/geometry/momentum_io.h"
10+
11+
#include <momentum/character/character.h>
12+
#include <momentum/character/fwd.h>
13+
#include <momentum/character/skeleton_state.h>
14+
#include <momentum/character/types.h>
15+
#include <momentum/io/gltf/gltf_builder.h>
16+
17+
#include <pybind11/eigen.h>
18+
#include <pybind11/pybind11.h>
19+
#include <pybind11/stl.h>
20+
21+
#include <fmt/format.h>
22+
#include <sstream>
23+
24+
namespace py = pybind11;
25+
namespace mm = momentum;
26+
27+
namespace pymomentum {
28+
29+
void registerGltfBuilderBindings(pybind11::module& m) {
30+
// =====================================================
31+
// momentum::GltfBuilder::MarkerMesh enum
32+
// =====================================================
33+
py::enum_<mm::GltfBuilder::MarkerMesh>(m, "MarkerMesh")
34+
.value("NoMesh", mm::GltfBuilder::MarkerMesh::None) // None is a reserved
35+
// work in Python.
36+
.value("UnitCube", mm::GltfBuilder::MarkerMesh::UnitCube);
37+
38+
// =====================================================
39+
// momentum::GltfFileFormat enum
40+
// =====================================================
41+
py::enum_<mm::GltfFileFormat>(m, "GltfFileFormat")
42+
.value("Extension", mm::GltfFileFormat::Extension)
43+
.value("GltfBinary", mm::GltfFileFormat::GltfBinary)
44+
.value("GltfAscii", mm::GltfFileFormat::GltfAscii);
45+
46+
// =====================================================
47+
// momentum::GltfBuilder
48+
// - constructor with fps
49+
// - getFps() / setFps()
50+
// - addCharacter()
51+
// - addMesh()
52+
// - addMotion()
53+
// - addSkeletonStates()
54+
// - addMarkerSequence()
55+
// - save()
56+
// - to_bytes()
57+
// =====================================================
58+
59+
py::class_<mm::GltfBuilder>(
60+
m,
61+
"GltfBuilder",
62+
R"(A builder class for creating GLTF files with multiple characters and animations.
63+
64+
The GltfBuilder allows you to incrementally construct a GLTF scene by adding characters,
65+
meshes, motions, and marker data. This is useful for creating complex scenes with multiple
66+
characters or combining different types of data into a single GLTF file.)")
67+
.def(
68+
py::init([](float fps) {
69+
auto builder = std::make_unique<mm::GltfBuilder>();
70+
builder->setFps(fps);
71+
return builder;
72+
}),
73+
R"(Create a new GltfBuilder with the specified frame rate.
74+
75+
:param fps: Frame rate in frames per second for animations.)",
76+
py::arg("fps") = 120.0f)
77+
.def_property(
78+
"fps",
79+
&mm::GltfBuilder::getFps,
80+
&mm::GltfBuilder::setFps,
81+
R"(The frame rate in frames per second used for animations.
82+
83+
This property controls the timing of all animations added to the GLTF file.
84+
Setting this value will affect subsequently added motions and animations.
85+
86+
:type: float)")
87+
.def(
88+
"add_character",
89+
[](mm::GltfBuilder& builder,
90+
const mm::Character& character,
91+
const std::optional<Eigen::Vector3f>& positionOffset,
92+
const std::optional<Eigen::Vector4f>& rotationOffset,
93+
bool addExtensions,
94+
bool addCollisions,
95+
bool addLocators,
96+
bool addMesh) {
97+
// Use defaults if not provided
98+
Eigen::Vector3f actualPositionOffset =
99+
positionOffset.value_or(Eigen::Vector3f::Zero());
100+
Eigen::Vector4f actualRotationOffset = rotationOffset.value_or(
101+
Eigen::Vector4f(0.0f, 0.0f, 0.0f, 1.0f));
102+
103+
// Convert Vector4f (x,y,z,w) to Quaternionf (w,x,y,z)
104+
mm::Quaternionf quaternionOffset(
105+
actualRotationOffset[3], // w
106+
actualRotationOffset[0], // x
107+
actualRotationOffset[1], // y
108+
actualRotationOffset[2]); // z
109+
110+
builder.addCharacter(
111+
character,
112+
actualPositionOffset,
113+
quaternionOffset,
114+
addExtensions,
115+
addCollisions,
116+
addLocators,
117+
addMesh);
118+
},
119+
R"(Add a character to the GLTF scene.
120+
121+
Each character will have a root node with the character's name as the parent
122+
of the skeleton root and the character mesh. Position and rotation offsets
123+
can be provided as an initial transform for the character.
124+
125+
:param character: The character to add to the scene.
126+
:param position_offset: Translation offset for the character's root node. Defaults to zero vector if None.
127+
:param rotation_offset: Rotation offset as a quaternion in (x,y,z,w) format. Defaults to identity quaternion if None.
128+
:param add_extensions: Whether to add momentum extensions to GLTF nodes.
129+
:param add_collisions: Whether to add collision geometry to the scene.
130+
:param add_locators: Whether to add locator data to the scene.
131+
:param add_mesh: Whether to add the character's mesh to the scene.)",
132+
py::arg("character"),
133+
py::arg("position_offset") = std::nullopt,
134+
py::arg("rotation_offset") = std::nullopt,
135+
py::arg("add_extensions") = true,
136+
py::arg("add_collisions") = true,
137+
py::arg("add_locators") = true,
138+
py::arg("add_mesh") = true)
139+
.def(
140+
"add_mesh",
141+
&mm::GltfBuilder::addMesh,
142+
R"(Add a static mesh to the GLTF scene.
143+
144+
This can be used to add environment meshes, target scans, or other static
145+
geometry that doesn't require animation. The mesh will be added as a separate
146+
node in the scene with the specified name.
147+
148+
:param mesh: The mesh to add to the scene.
149+
:param name: Name for the mesh node in the GLTF scene.
150+
:param add_color: Whether to include vertex colors if present in the mesh.)",
151+
py::arg("mesh"),
152+
py::arg("name"),
153+
py::arg("add_color") = false)
154+
.def(
155+
"add_motion",
156+
[](mm::GltfBuilder& builder,
157+
const mm::Character& character,
158+
float fps,
159+
const std::optional<mm::MotionParameters>& motion,
160+
const std::optional<mm::IdentityParameters>& offsets,
161+
bool addExtensions,
162+
const std::string& customName) {
163+
// Apply same validation and transposition as
164+
// saveGLTFCharacterToFile
165+
mm::MotionParameters transposedMotion;
166+
if (motion.has_value()) {
167+
const auto& [parameters, poses] = motion.value();
168+
MT_THROW_IF(
169+
poses.cols() != parameters.size(),
170+
"Expected motion parameters to be n_frames x {}, but got {} x {}",
171+
parameters.size(),
172+
poses.rows(),
173+
poses.cols());
174+
}
175+
176+
builder.addMotion(
177+
character,
178+
fps,
179+
pymomentum::transpose(motion.value_or(mm::MotionParameters{})),
180+
offsets.value_or(mm::IdentityParameters{}),
181+
addExtensions,
182+
customName);
183+
},
184+
R"(Add a motion sequence to the specified character.
185+
186+
If addCharacter has not been called before adding the motion, the character
187+
will be automatically added with default settings. The motion data contains
188+
model parameters that animate the character over time.
189+
190+
:param character: The character to add motion for.
191+
:param fps: Frame rate in frames per second for the motion data.
192+
:param motion: Optional motion parameters as a tuple of (parameter_names, motion_data).
193+
Motion data should be a matrix with shape [n_frames x n_parameters].
194+
:param offsets: Optional identity parameters as a tuple of (joint_names, offset_data).
195+
Offset data should be a vector with shape [n_joints * 7].
196+
:param add_extensions: Whether to add momentum extensions to GLTF nodes.
197+
:param custom_name: Custom name for the animation in the GLTF file.)",
198+
py::arg("character"),
199+
py::arg("fps") = 120.0f,
200+
py::arg("motion") = std::optional<mm::MotionParameters>{},
201+
py::arg("offsets") = std::optional<mm::IdentityParameters>{},
202+
py::arg("add_extensions") = true,
203+
py::arg("custom_name") = "default")
204+
.def(
205+
"add_skeleton_states",
206+
[](mm::GltfBuilder& builder,
207+
const mm::Character& character,
208+
float fps,
209+
const py::array_t<float>& skeletonStates,
210+
const std::string& customName) {
211+
// Use the shared utility function for conversion
212+
std::vector<mm::SkeletonState> skelStates =
213+
pymomentum::arrayToSkeletonStates(skeletonStates, character);
214+
215+
// Call the addSkeletonStates method
216+
builder.addSkeletonStates(
217+
character, fps, gsl::make_span(skelStates), customName);
218+
},
219+
R"(Add skeleton states animation to the specified character.
220+
221+
If addCharacter has not been called before adding the skeleton states, the character
222+
will be automatically added with default settings. The skeleton states contain
223+
per-joint transforms that define the character's pose over time.
224+
225+
:param character: The character to add skeleton states for.
226+
:param fps: Frame rate in frames per second for the skeleton state data.
227+
:param skeleton_states: Skeleton states as a 3D array with shape [nFrames, nJoints, 8].
228+
Each joint state contains [tx, ty, tz, rx, ry, rz, rw, s] where
229+
translation is (tx,ty,tz), rotation is quaternion (rx,ry,rz,rw)
230+
in (x,y,z,w) format, and s is scale.
231+
:param custom_name: Custom name for the animation in the GLTF file.)",
232+
py::arg("character"),
233+
py::arg("fps"),
234+
py::arg("skeleton_states"),
235+
py::arg("custom_name") = "default")
236+
.def(
237+
"add_marker_sequence",
238+
[](mm::GltfBuilder& builder,
239+
float fps,
240+
const std::vector<std::vector<mm::Marker>>& markerSequence,
241+
mm::GltfBuilder::MarkerMesh markerMesh,
242+
const std::string& animName) {
243+
builder.addMarkerSequence(
244+
fps, gsl::make_span(markerSequence), markerMesh, animName);
245+
},
246+
R"(Add marker sequence animation data to the GLTF scene.
247+
248+
This method adds motion capture marker data to the GLTF file. The marker data
249+
represents 3D positions of markers over time, which can be used for motion capture
250+
analysis or visualization. Optional marker mesh visualization can be added as unit cubes.
251+
252+
:param fps: Frame rate in frames per second for the marker sequence data.
253+
:param marker_sequence: A 2D list/array with shape [numFrames][numMarkers] containing
254+
Marker objects for each frame. Each Marker contains name,
255+
position, and occlusion status.
256+
:param marker_mesh: Type of mesh to represent markers visually using :class:`MarkerMesh` enum.
257+
Default is MarkerMesh.None for no visual representation.
258+
MarkerMesh.UnitCube displays markers as unit cubes.
259+
:param anim_name: Custom name for the marker animation in the GLTF file.)",
260+
py::arg("fps"),
261+
py::arg("marker_sequence"),
262+
py::arg("marker_mesh") = mm::GltfBuilder::MarkerMesh::None,
263+
py::arg("anim_name") = "default")
264+
.def(
265+
"save",
266+
[](mm::GltfBuilder& builder,
267+
const std::string& filename,
268+
const std::optional<mm::GltfFileFormat>& fileFormat) {
269+
mm::GltfFileFormat actualFileFormat =
270+
fileFormat.value_or(mm::GltfFileFormat::Extension);
271+
builder.save(filename, actualFileFormat);
272+
},
273+
R"(Save the GLTF scene to a file.
274+
275+
This method writes the constructed GLTF scene to the specified file. The file format
276+
can be explicitly specified or automatically deduced from the file extension.
277+
278+
:param filename: Path where to save the GLTF file.
279+
:param file_format: Optional file format specification using GltfFileFormat enum.
280+
If not provided, format will be deduced from filename extension.)",
281+
py::arg("filename"),
282+
py::arg("file_format") = std::optional<mm::GltfFileFormat>{})
283+
.def(
284+
"to_bytes",
285+
[](mm::GltfBuilder& builder,
286+
const std::optional<mm::GltfFileFormat>& fileFormat) -> py::bytes {
287+
// Get a copy of the document
288+
fx::gltf::Document doc = builder.getDocument();
289+
290+
// Use ostringstream to serialize the document to bytes
291+
std::ostringstream output(std::ios::binary | std::ios::out);
292+
fx::gltf::Save(
293+
doc,
294+
output,
295+
{},
296+
fileFormat.value_or(mm::GltfFileFormat::GltfBinary) !=
297+
mm::GltfFileFormat::GltfAscii);
298+
299+
// Convert to Python bytes
300+
const std::string& str = output.str();
301+
return py::bytes(str);
302+
},
303+
R"(Convert the GLTF scene to bytes in memory.
304+
305+
This method serializes the constructed GLTF scene to a byte array without
306+
writing to disk. This is useful for programmatic processing, network transmission,
307+
or when you need the GLTF data as bytes for other purposes.
308+
309+
:return: The GLTF scene as bytes. For GltfBinary format, this will be GLB binary data.
310+
For GltfAscii format, this will be JSON text encoded as UTF-8 bytes.)",
311+
py::arg("file_format") = mm::GltfFileFormat::GltfBinary);
312+
}
313+
314+
} // namespace pymomentum
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
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+
#pragma once
9+
10+
#include <pybind11/pybind11.h>
11+
12+
namespace pymomentum {
13+
14+
/// Register the GltfBuilder class bindings with the given pybind11 module
15+
void registerGltfBuilderBindings(pybind11::module& m);
16+
17+
} // namespace pymomentum

0 commit comments

Comments
 (0)