|
| 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 |
0 commit comments