draco_io/fbx_scene.rs
1//! Shared, lossy FBX scene values.
2
3use std::fmt;
4use std::io;
5
6use draco_core::mesh::Mesh;
7
8/// What kind of deviation or loss a [`FbxWarning`] describes.
9///
10/// Codes are stable identifiers; the human-readable text lives on the warning
11/// itself and may be reworded.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
13#[non_exhaustive]
14pub enum FbxWarningCode {
15 /// A terminator record carried non-zero property fields.
16 MalformedNullRecord,
17 /// A named node declared no end offset, so it was read without children.
18 MissingNodeEndOffset,
19 /// A node's property list ended somewhere other than its header declared.
20 PropertyListLengthMismatch,
21 /// A node record claimed to end past the end of the file.
22 NodeEndPastEndOfFile,
23 /// A Model used an FBX inheritance rule the imported transform cannot
24 /// express, so its local TRS is missing that rule.
25 UnsupportedTransformInherit,
26 /// A layer element used a mapping or reference mode that could not be
27 /// resolved, so default values were substituted.
28 UnsupportedLayerMapping,
29 /// A geometry carried a `LayerElement*` this crate does not import, so
30 /// that layer's data is absent from the decoded scene.
31 DroppedLayerElement,
32 /// A node carried a `NodeAttribute` class this crate does not represent,
33 /// so the attribute's own properties are absent from the scene.
34 DroppedNodeAttribute,
35 /// A Model reached neither the document root nor a parent Model by
36 /// object connection, so it is not part of the decoded scene graph --
37 /// the same objects the source file kept out of its scene.
38 UnconnectedModelDropped,
39 /// A Model hierarchy ran deeper than the reader descends, so the subtree
40 /// below that point -- and any skin cluster bound into it -- is absent.
41 ModelDepthLimitReached,
42}
43
44impl FbxWarningCode {
45 /// Whether data present in the file is absent from the decoded scene.
46 ///
47 /// Container-layout notices describe a tolerated deviation but no loss;
48 /// the semantic codes describe something the caller will not find in the
49 /// result. A converter can use this to decide what to surface downstream.
50 pub fn is_data_loss(self) -> bool {
51 match self {
52 FbxWarningCode::MalformedNullRecord
53 | FbxWarningCode::PropertyListLengthMismatch
54 | FbxWarningCode::NodeEndPastEndOfFile => false,
55 FbxWarningCode::MissingNodeEndOffset
56 | FbxWarningCode::UnsupportedTransformInherit
57 | FbxWarningCode::UnsupportedLayerMapping
58 | FbxWarningCode::DroppedLayerElement
59 | FbxWarningCode::DroppedNodeAttribute
60 | FbxWarningCode::UnconnectedModelDropped
61 | FbxWarningCode::ModelDepthLimitReached => true,
62 }
63 }
64
65 /// Stable machine-readable slug, for logs and downstream reports.
66 pub fn as_str(self) -> &'static str {
67 match self {
68 FbxWarningCode::MalformedNullRecord => "malformed-null-record",
69 FbxWarningCode::MissingNodeEndOffset => "missing-node-end-offset",
70 FbxWarningCode::PropertyListLengthMismatch => "property-list-length-mismatch",
71 FbxWarningCode::NodeEndPastEndOfFile => "node-end-past-end-of-file",
72 FbxWarningCode::UnsupportedTransformInherit => "unsupported-transform-inherit",
73 FbxWarningCode::UnsupportedLayerMapping => "unsupported-layer-mapping",
74 FbxWarningCode::DroppedLayerElement => "dropped-layer-element",
75 FbxWarningCode::DroppedNodeAttribute => "dropped-node-attribute",
76 FbxWarningCode::UnconnectedModelDropped => "unconnected-model-dropped",
77 FbxWarningCode::ModelDepthLimitReached => "model-depth-limit-reached",
78 }
79 }
80}
81
82/// One non-fatal notice raised while reading an FBX document.
83///
84/// Occurrences are collapsed by `(code, subject)`: a malformed pattern
85/// repeated across thousands of nodes yields one warning with a count, not
86/// thousands of identical strings.
87#[derive(Debug, Clone, PartialEq, Eq)]
88pub struct FbxWarning {
89 /// Stable classification of the notice.
90 pub code: FbxWarningCode,
91 /// Human-readable description.
92 pub message: String,
93 /// Owning FBX object name, when one is known.
94 pub subject: Option<String>,
95 /// How many times this `(code, subject)` pair fired. Never zero.
96 pub count: u32,
97}
98
99impl FbxWarning {
100 /// Creates a warning that has fired once.
101 pub fn new(code: FbxWarningCode, message: impl Into<String>) -> Self {
102 Self {
103 code,
104 message: message.into(),
105 subject: None,
106 count: 1,
107 }
108 }
109
110 /// Attaches the FBX object this notice is about.
111 #[must_use]
112 pub fn with_subject(mut self, subject: impl Into<String>) -> Self {
113 self.subject = Some(subject.into());
114 self
115 }
116}
117
118/// Appends a warning, collapsing repeats of the same `(code, subject)` pair
119/// into a single entry with a count.
120///
121/// Without this, a malformed pattern repeated across every node in a large
122/// file produces thousands of identical strings and buries anything else.
123///
124/// Lives beside [`FbxWarning`] rather than in either reader half because both
125/// the container decoder and the scene layer raise notices. Both of those are
126/// read-side, so a writer-only build has no caller for it.
127#[cfg(feature = "fbx-reader")]
128pub(crate) fn push_warning(
129 warnings: &mut Vec<FbxWarning>,
130 code: FbxWarningCode,
131 message: String,
132 subject: Option<&str>,
133) {
134 if let Some(existing) = warnings
135 .iter_mut()
136 .find(|warning| warning.code == code && warning.subject.as_deref() == subject)
137 {
138 existing.count = existing.count.saturating_add(1);
139 return;
140 }
141 let mut warning = FbxWarning::new(code, message);
142 warning.subject = subject.map(str::to_owned);
143 warnings.push(warning);
144}
145
146impl fmt::Display for FbxWarning {
147 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
148 write!(formatter, "{}", self.message)?;
149 if self.count > 1 {
150 write!(formatter, " (x{})", self.count)?;
151 }
152 Ok(())
153 }
154}
155
156/// Stable, document-local identifier for an FBX model node.
157///
158/// FBX names are not unique. Scene relationships and animation targets use
159/// this 32-bit identifier, which is also safe to transfer through JavaScript.
160#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
161pub struct FbxNodeId(pub u32);
162
163/// Local transform extracted from or written to an FBX model node.
164///
165/// This matrix is synthesized from local translation, rotation, and scaling.
166/// It does not represent FBX pivot or inheritance rules.
167#[derive(Debug, Clone, Copy, PartialEq)]
168pub struct FbxTransform {
169 /// Packed transform matrix compatible with the FBX 16-value layout.
170 ///
171 /// The outer arrays are emitted in order and can be passed directly to a
172 /// column-major WebGL matrix. Translation therefore occupies
173 /// `matrix[3][0..3]`.
174 pub matrix: [[f32; 4]; 4],
175}
176
177/// Source Model transform-stack properties required to reproduce FBX local
178/// animation semantics. Values remain in authored FBX units and degrees.
179#[derive(Debug, Clone, Default, PartialEq)]
180pub struct FbxTransformStack {
181 /// `Lcl Translation` in source units.
182 pub translation: Option<[f32; 3]>,
183 /// `Lcl Rotation` in degrees.
184 pub rotation: Option<[f32; 3]>,
185 /// `Lcl Scaling`.
186 pub scaling: Option<[f32; 3]>,
187 /// FBX `RotationOrder` enum value.
188 pub rotation_order: Option<i32>,
189 /// FBX `RotationActive` flag. Its absence is distinct from `false` in a
190 /// source-provenance export because the Model template supplies defaults.
191 pub rotation_active: Option<bool>,
192 /// `PreRotation` in degrees.
193 pub pre_rotation: Option<[f32; 3]>,
194 /// `PostRotation` in degrees.
195 pub post_rotation: Option<[f32; 3]>,
196 /// `RotationOffset` in source units.
197 pub rotation_offset: Option<[f32; 3]>,
198 /// `RotationPivot` in source units.
199 pub rotation_pivot: Option<[f32; 3]>,
200 /// `ScalingOffset` in source units.
201 pub scaling_offset: Option<[f32; 3]>,
202 /// `ScalingPivot` in source units.
203 pub scaling_pivot: Option<[f32; 3]>,
204 /// FBX `InheritType` enum value.
205 pub inherit_type: Option<i32>,
206}
207
208/// A Model's `Geometric*` properties, in authored FBX units and degrees.
209///
210/// FBX applies these to the geometry attached to a node and to nothing else:
211/// unlike the rest of the transform stack they are not inherited by child
212/// nodes, so they belong to the mesh instance rather than to the node.
213/// Exporters write them for every object whose pivot is not at its mesh
214/// origin.
215#[derive(Debug, Clone, Default, PartialEq)]
216pub struct FbxGeometricTransform {
217 /// `GeometricTranslation` in source units.
218 pub translation: Option<[f32; 3]>,
219 /// `GeometricRotation` in degrees.
220 pub rotation: Option<[f32; 3]>,
221 /// `GeometricScaling`.
222 pub scaling: Option<[f32; 3]>,
223}
224
225impl FbxGeometricTransform {
226 /// Whether every authored component is its FBX default.
227 pub fn is_identity(&self) -> bool {
228 let zero = |values: Option<[f32; 3]>| {
229 values.is_none_or(|values| values.iter().all(|value| *value == 0.0))
230 };
231 let one = |values: Option<[f32; 3]>| {
232 values.is_none_or(|values| values.iter().all(|value| *value == 1.0))
233 };
234 zero(self.translation) && zero(self.rotation) && one(self.scaling)
235 }
236
237 /// The composed `Gt * Gr * Gs` matrix, in the same packed column-major
238 /// layout as [`FbxTransform`].
239 pub fn matrix(&self) -> FbxTransform {
240 crate::fbx_transform::geometric_matrix(self)
241 }
242}
243
244/// Source FBX global coordinate, unit, and display-time settings retained for
245/// FBX-to-FBX provenance exports. This is intentionally not part of the
246/// portable SceneDocument contract.
247#[derive(Debug, Clone, Default, PartialEq)]
248pub struct FbxGlobalSettings {
249 /// FBX `UpAxis` enum value.
250 pub up_axis: Option<i32>,
251 /// FBX `UpAxisSign` value.
252 pub up_axis_sign: Option<i32>,
253 /// FBX `FrontAxis` enum value.
254 pub front_axis: Option<i32>,
255 /// FBX `FrontAxisSign` value.
256 pub front_axis_sign: Option<i32>,
257 /// FBX `CoordAxis` enum value.
258 pub coord_axis: Option<i32>,
259 /// FBX `CoordAxisSign` value.
260 pub coord_axis_sign: Option<i32>,
261 /// FBX `UnitScaleFactor` value.
262 pub unit_scale_factor: Option<f64>,
263 /// FBX `OriginalUnitScaleFactor` value.
264 pub original_unit_scale_factor: Option<f64>,
265 /// FBX `TimeMode` enum value.
266 pub time_mode: Option<i32>,
267}
268
269/// The layer elements preserved from one FBX geometry node.
270///
271/// Grouped rather than spread across [`FbxMeshInstance`] because they are one
272/// kind of thing that grows together: every format capability added so far
273/// arrived as another layer family, and each one that lands as a bare field
274/// has to be threaded through every construction site and every signature
275/// that carries geometry. [`crate::FbxGeometryLayers`] is the borrowed view of
276/// this plus the positions and indices it indexes into.
277#[derive(Debug, Clone, Default)]
278pub struct FbxMeshLayers {
279 /// Original UV layer elements, including mapping/reference information.
280 pub uv_sets: Vec<FbxUvSet>,
281 /// Original normal layer elements, including mapping/reference information.
282 pub normal_sets: Vec<FbxNormalSet>,
283 /// Original colour layer elements, including mapping/reference information.
284 pub color_sets: Vec<FbxColorSet>,
285 /// Original tangent layer elements, with handedness merged into `w`.
286 ///
287 /// Draco has no tangent attribute, so these never reach
288 /// [`FbxMeshInstance::mesh`]; they travel on the instance and through
289 /// [`crate::FbxRenderMesh`] instead, the same way extra UV sets do.
290 pub tangent_sets: Vec<FbxTangentSet>,
291 /// Original binormal layer elements.
292 ///
293 /// Derivable from the normal and tangent, and absent from glTF, so these
294 /// exist only so an FBX document survives a rewrite unchanged.
295 pub binormal_sets: Vec<FbxBinormalSet>,
296 /// Original `LayerElementSmoothing` layers.
297 ///
298 /// Hard and soft edges. glTF has no equivalent, so these survive an
299 /// FBX-to-FBX rewrite but do not travel further. A layer whose length does
300 /// not match the domain its mapping names is dropped with a warning rather
301 /// than kept as misaligned data.
302 pub smoothing_layers: Vec<FbxSmoothingLayer>,
303 /// Original `LayerElementEdgeCrease` and `LayerElementVertexCrease` layers.
304 pub crease_layers: Vec<FbxCreaseLayer>,
305}
306
307/// Geometry attached to one [`FbxSceneNode`].
308///
309/// This is materialized Draco geometry, not a lossless FBX geometry object.
310#[derive(Debug, Clone, Default)]
311pub struct FbxMeshInstance {
312 /// Name supplied by the FBX geometry node, when available.
313 pub name: Option<String>,
314 /// Decoded mesh geometry.
315 pub mesh: Mesh,
316 /// Original FBX control-point positions, retained independently from the
317 /// resolved render mesh used by Draco/WebGL.
318 pub control_points: Vec<[f32; 3]>,
319 /// Original FBX polygon-corner indices. Negative values terminate a face.
320 pub polygon_vertex_indices: Vec<i32>,
321 /// Layer elements preserved from the source geometry node.
322 pub layers: FbxMeshLayers,
323 /// Original FBX `Edges` array, verbatim.
324 ///
325 /// Each entry indexes [`Self::polygon_vertex_indices`], naming the polygon
326 /// corner an edge starts at. FBX does not require this to list every
327 /// topological edge -- importers reconstruct the missing ones from faces --
328 /// so it is kept raw rather than normalized. It is also the domain
329 /// `ByEdge` layer elements address.
330 pub edges: Vec<i32>,
331 /// Per-polygon material index from `LayerElementMaterial`, when present.
332 ///
333 /// Each entry corresponds to one triangle in fan-triangulation order
334 /// produced by [`crate::FbxReader`]. Entries are absolute indices into
335 /// [`FbxScene::materials`]. The list is empty when the geometry does not
336 /// carry a material layer (callers fall back to the first material).
337 pub material_indices: Vec<i32>,
338 /// Skin binding for this geometry, when it is armature-deformed.
339 pub skin: Option<FbxSkin>,
340 /// Blend-shape targets defined for this geometry.
341 pub morph_targets: Vec<FbxMorphTarget>,
342 /// The attaching Model's `Geometric*` offset, when it sets one.
343 ///
344 /// It sits here rather than on the node because FBX does not pass it to
345 /// child nodes: a consumer places this geometry with
346 /// `node_transform * geometric_transform` and places the children with the
347 /// node transform alone.
348 pub geometric_transform: Option<FbxGeometricTransform>,
349}
350
351/// A preserved FBX layer element carrying `N` float components per value.
352///
353/// Every float-valued FBX layer element has this shape -- a name, a mapping
354/// and reference mode, a value array, and an optional index array -- and
355/// differs only in its component count and the node names it is read from.
356/// The per-family aliases below name the ones this crate understands.
357#[derive(Debug, Clone, Default, PartialEq)]
358pub struct FbxLayerSet<const N: usize> {
359 /// FBX layer set name.
360 pub name: Option<String>,
361 /// FBX mapping information type, e.g. `ByPolygonVertex` or `ByVertice`.
362 pub mapping: Option<String>,
363 /// FBX reference information type, e.g. `Direct` or `IndexToDirect`.
364 pub reference: Option<String>,
365 /// Direct values.
366 pub values: Vec<[f32; N]>,
367 /// Optional direct-value indices, used when `reference` is `IndexToDirect`.
368 pub indices: Vec<i32>,
369}
370
371/// A preserved FBX `LayerElementUV`.
372pub type FbxUvSet = FbxLayerSet<2>;
373
374/// A preserved FBX `LayerElementNormal`.
375pub type FbxNormalSet = FbxLayerSet<3>;
376
377/// A preserved FBX `LayerElementColor`.
378///
379/// FBX normally stores four components; a three-component source is padded
380/// with an opaque alpha when read.
381pub type FbxColorSet = FbxLayerSet<4>;
382
383/// A preserved FBX `LayerElementTangent` or `LayerElementBinormal`.
384///
385/// FBX splits these across two sibling arrays: `Tangents` holds three
386/// components and the handedness sign lives in a separate `TangentsW`, which
387/// only FBX 7500 and later write. They are merged into one four-component
388/// value here because that is the form glTF's `TANGENT` needs, and split again
389/// on write.
390#[derive(Debug, Clone, Default, PartialEq)]
391pub struct FbxTangentSet {
392 /// Tangent vectors, with the handedness sign in `w`.
393 pub layer: FbxLayerSet<4>,
394 /// Whether the source carried an explicit handedness array.
395 ///
396 /// When it did not, `w` was defaulted to `+1.0`. The writer emits the
397 /// sibling array only when this is set, so a document that had no
398 /// handedness does not gain one by being rewritten.
399 pub has_handedness: bool,
400}
401
402/// A preserved FBX `LayerElementBinormal`.
403///
404/// Structurally identical to a tangent set, and always written alongside one:
405/// no corpus file carries either alone.
406pub type FbxBinormalSet = FbxTangentSet;
407
408/// What a `NodeAttribute` attached to a scene node describes.
409///
410/// Only the two classes this crate represents appear here. Others are reported
411/// through [`FbxWarningCode::DroppedNodeAttribute`] rather than given a variant
412/// that would carry nothing.
413#[derive(Debug, Clone, PartialEq)]
414#[non_exhaustive]
415pub enum FbxNodeAttribute {
416 /// A `Camera` attribute.
417 Camera(FbxCamera),
418 /// A `Light` attribute.
419 Light(FbxLight),
420}
421
422/// An FBX `Camera` node attribute.
423///
424/// Every field is optional because FBX omits any property left at its class
425/// default. Fields are limited to those that actually occur across the `ufbx`
426/// corpus; angles are in degrees and distances in the document's own units.
427#[derive(Debug, Clone, Default, PartialEq)]
428#[non_exhaustive]
429pub struct FbxCamera {
430 /// Eye position, in world space rather than relative to the node.
431 pub position: Option<[f32; 3]>,
432 /// Point the camera looks at, in the same space as [`Self::position`].
433 pub interest_position: Option<[f32; 3]>,
434 /// Up vector.
435 pub up_vector: Option<[f32; 3]>,
436 /// `CameraProjectionType`: 0 perspective, 1 orthographic.
437 pub projection_type: Option<i32>,
438 /// Diagonal field of view, in degrees.
439 pub field_of_view: Option<f32>,
440 /// Horizontal field of view, in degrees.
441 pub field_of_view_x: Option<f32>,
442 /// Vertical field of view, in degrees.
443 pub field_of_view_y: Option<f32>,
444 /// Focal length in millimetres.
445 pub focal_length: Option<f32>,
446 /// Near clip distance.
447 pub near_plane: Option<f32>,
448 /// Far clip distance.
449 pub far_plane: Option<f32>,
450 /// Render aperture width in pixels.
451 pub aspect_width: Option<f32>,
452 /// Render aperture height in pixels.
453 pub aspect_height: Option<f32>,
454 /// Film-back width in **inches**, not millimetres.
455 ///
456 /// This is the sensor size, and a consumer needs it with
457 /// [`Self::focal_length`] to reach a field of view: Blender computes
458 /// `sensor_width = film_width * 25.4` and falls back to its own 32 mm
459 /// default when the property is absent, which silently changes the framing
460 /// of every camera in the document.
461 pub film_width: Option<f32>,
462 /// Film-back height in inches.
463 pub film_height: Option<f32>,
464 /// Film-back aspect ratio, `film_width / film_height`.
465 pub film_aspect_ratio: Option<f32>,
466 /// `ApertureMode`: which of the aperture and field-of-view properties the
467 /// authoring tool treats as authoritative when they disagree.
468 pub aperture_mode: Option<i32>,
469 /// Orthographic zoom, meaningful when [`Self::projection_type`] is 1.
470 pub ortho_zoom: Option<f32>,
471}
472
473/// An FBX `Light` node attribute.
474///
475/// As with [`FbxCamera`], every field is optional and the set is limited to
476/// what the corpus contains. Notably no file carries `InnerAngle` or
477/// `OuterAngle`, so spot cone angles are not represented.
478#[derive(Debug, Clone, Default, PartialEq)]
479#[non_exhaustive]
480pub struct FbxLight {
481 /// `LightType`: 0 point, 1 directional, 2 spot, 3 area, 4 volume.
482 pub light_type: Option<i32>,
483 /// Linear RGB colour.
484 pub color: Option<[f32; 3]>,
485 /// Intensity, where 100 is FBX's unit brightness.
486 pub intensity: Option<f32>,
487 /// Whether the light contributes at all.
488 pub cast_light: Option<bool>,
489 /// Whether the light casts shadows.
490 pub cast_shadows: Option<bool>,
491 /// `DecayType`: 0 none, 1 linear, 2 quadratic, 3 cubic.
492 pub decay_type: Option<i32>,
493 /// Distance at which decay begins.
494 pub decay_start: Option<f32>,
495}
496
497/// A preserved FBX `LayerElementSmoothing`.
498///
499/// Smoothing is an integer flag per edge or per polygon -- whether the edge is
500/// soft, or the polygon smooth-shaded -- and is kept separate from
501/// [`FbxCreaseLayer`] because that one is a floating-point weight. Rounding one
502/// through the other's type would quietly change authored crease values.
503#[derive(Debug, Clone, Default, PartialEq, Eq)]
504pub struct FbxSmoothingLayer {
505 /// FBX mapping information type: `ByEdge` or `ByPolygon`.
506 pub mapping: Option<String>,
507 /// One flag per edge or per polygon, matching `mapping`.
508 pub values: Vec<i32>,
509}
510
511/// Which domain a [`FbxCreaseLayer`] sharpens.
512#[derive(Debug, Clone, Copy, PartialEq, Eq)]
513pub enum FbxCreaseKind {
514 /// `LayerElementEdgeCrease`, one weight per entry in
515 /// [`FbxMeshInstance::edges`].
516 Edge,
517 /// `LayerElementVertexCrease`, one weight per control point.
518 Vertex,
519}
520
521/// A preserved FBX `LayerElementEdgeCrease` or `LayerElementVertexCrease`.
522#[derive(Debug, Clone, PartialEq)]
523pub struct FbxCreaseLayer {
524 /// Whether this sharpens edges or control points.
525 pub kind: FbxCreaseKind,
526 /// FBX mapping information type: `ByEdge` or `ByVertice`.
527 pub mapping: Option<String>,
528 /// Crease weights, normally in `0..=1`.
529 pub values: Vec<f64>,
530}
531
532/// All influences from one joint onto a mesh's control points.
533#[derive(Debug, Clone)]
534pub struct FbxSkinCluster {
535 /// Joint Model that owns this cluster.
536 pub joint_node_id: FbxNodeId,
537 /// Affected mesh control-point indices.
538 pub control_point_indices: Vec<u32>,
539 /// Weight for each entry in [`Self::control_point_indices`].
540 pub weights: Vec<f32>,
541 /// Mesh global transform captured in the bind pose (`Transform`).
542 pub mesh_bind_transform: FbxTransform,
543 /// Joint global transform captured in the bind pose (`TransformLink`).
544 pub joint_bind_transform: FbxTransform,
545 /// Armature global transform captured in the bind pose
546 /// (`TransformAssociateModel`), when supplied by the source.
547 pub armature_bind_transform: Option<FbxTransform>,
548}
549
550/// Skinning data attached to a mesh instance.
551#[derive(Debug, Clone)]
552pub struct FbxSkin {
553 /// All clusters, one for every influencing joint.
554 pub clusters: Vec<FbxSkinCluster>,
555 /// Explicit FBX BindPose matrices keyed by model id, when present.
556 pub bind_pose: Vec<(FbxNodeId, FbxTransform)>,
557}
558
559/// One sparse FBX blend-shape target.
560#[derive(Debug, Clone)]
561pub struct FbxMorphTarget {
562 /// Display name of the shape geometry.
563 pub name: Option<String>,
564 /// Control-point indices affected by this target.
565 pub control_point_indices: Vec<u32>,
566 /// Position deltas for those indices.
567 pub position_deltas: Vec<[f32; 3]>,
568 /// Normal deltas for those indices, when present.
569 pub normal_deltas: Option<Vec<[f32; 3]>>,
570 /// Default weight in percent.
571 pub default_weight: f32,
572 /// Full-deformation weight in percent.
573 pub full_weight: f32,
574}
575
576/// What an FBX `Model` record declared its node to be, beyond what the
577/// geometry and attributes attached to it say.
578///
579/// Every exporter writes a joint's class on the Model itself, so that is the
580/// signal this carries; the `Skeleton` `NodeAttribute` a joint usually also
581/// carries adds nothing the scene keeps. It matters exactly where nothing
582/// else says what the node is: a joint no skin cluster names — a bone's
583/// `*_end` tail helper, which holds no weights — and a `Null` grouping node
584/// such as an armature's root object. Without it both rewrite as plain mesh
585/// Models, which is how a round trip turned a rig into joints Blender cannot
586/// form a chain from.
587#[derive(Debug, Clone, Copy, PartialEq, Eq)]
588pub enum FbxNodeKind {
589 /// A `LimbNode` or `Limb` Model: one joint of a skeleton.
590 Joint,
591 /// A `Null` or `Root` Model: a transform-only grouping node, the class an
592 /// armature's root object carries.
593 Null,
594}
595
596/// A node in a hierarchy extracted from or written to FBX Model connections.
597///
598/// The supported raw Model transform stack is retained for source-provenance
599/// FBX exports. Skin and blend-shape data lives on mesh instances.
600#[derive(Debug, Clone)]
601pub struct FbxSceneNode {
602 /// Stable id used by skin clusters and animation channels.
603 pub id: FbxNodeId,
604 /// Name supplied by the FBX model node, when available.
605 pub name: Option<String>,
606 /// Supported local transform properties synthesized into a matrix.
607 pub transform: Option<FbxTransform>,
608 /// Optional authored FBX stack behind `transform`.
609 pub transform_stack: Option<FbxTransformStack>,
610 /// Whether the node's static local transform uses FBX rotation/pivot
611 /// terms beyond plain local TRS. Consumers that only receive the lossy
612 /// matrix can use the skin bind pose as the baked local basis for these
613 /// nodes while preserving raw Model TRS for ordinary nodes.
614 pub has_complex_transform_stack: bool,
615 /// The Model's own class, when it declares the node to be something other
616 /// than a mesh; see [`FbxNodeKind`].
617 pub kind: Option<FbxNodeKind>,
618 /// Geometry attached directly to this model node.
619 pub mesh_instances: Vec<FbxMeshInstance>,
620 /// Camera or light attached to this model node, when it carries one.
621 pub attribute: Option<FbxNodeAttribute>,
622 /// Child model nodes.
623 pub children: Vec<FbxSceneNode>,
624}
625
626#[cfg(feature = "fbx-reader")]
627impl FbxSceneNode {
628 pub(crate) fn new(name: Option<String>) -> Self {
629 Self {
630 id: FbxNodeId(0),
631 name,
632 transform: None,
633 transform_stack: None,
634 has_complex_transform_stack: false,
635 kind: None,
636 mesh_instances: Vec::new(),
637 attribute: None,
638 children: Vec::new(),
639 }
640 }
641}
642
643/// Texture slot targeted by an [`FbxTextureBinding`].
644///
645/// These are the FBX property names commonly used to link a texture to a
646/// material property. The mapping follows the conventions used by the FBX SDK
647/// and Blender's `io_scene_fbx`.
648#[derive(Debug, Clone, Copy, PartialEq, Eq)]
649pub enum FbxTextureSlot {
650 /// `DiffuseColor` / `DiffuseColor` texture link.
651 Diffuse,
652 /// `NormalMap` texture link.
653 Normal,
654 /// `EmissiveColor` texture link.
655 Emissive,
656 /// `SpecularColor` texture link.
657 Specular,
658 /// `Shininess` / roughness texture link.
659 Roughness,
660 /// `ReflectionFactor` / metallic texture link.
661 Metallic,
662 /// `AmbientColor` texture link.
663 Ambient,
664}
665
666impl FbxTextureSlot {
667 /// Returns the FBX property name used to wire a texture to a material.
668 pub fn property_name(self) -> &'static str {
669 match self {
670 FbxTextureSlot::Diffuse => "DiffuseColor",
671 FbxTextureSlot::Normal => "NormalMap",
672 FbxTextureSlot::Emissive => "EmissiveColor",
673 FbxTextureSlot::Specular => "SpecularColor",
674 FbxTextureSlot::Roughness => "ShininessExponent",
675 FbxTextureSlot::Metallic => "ReflectionFactor",
676 FbxTextureSlot::Ambient => "AmbientColor",
677 }
678 }
679
680 /// Parses the FBX connection property name into a slot, if recognized.
681 pub fn from_property_name(name: &str) -> Option<Self> {
682 match name {
683 "DiffuseColor" => Some(FbxTextureSlot::Diffuse),
684 "NormalMap" | "Bump" => Some(FbxTextureSlot::Normal),
685 "EmissiveColor" => Some(FbxTextureSlot::Emissive),
686 "SpecularColor" => Some(FbxTextureSlot::Specular),
687 "ShininessExponent" | "Shininess" => Some(FbxTextureSlot::Roughness),
688 "ReflectionFactor" => Some(FbxTextureSlot::Metallic),
689 "AmbientColor" => Some(FbxTextureSlot::Ambient),
690 _ => None,
691 }
692 }
693}
694
695/// Bind a [`FbxTexture`] to a slot of a material.
696#[derive(Debug, Clone, Copy, PartialEq, Eq)]
697pub struct FbxTextureBinding {
698 /// Material slot the texture feeds.
699 pub slot: FbxTextureSlot,
700 /// Index into [`FbxScene::textures`].
701 pub texture_index: usize,
702}
703
704/// Texture object extracted from an FBX `Texture` / `Video` pair.
705#[derive(Debug, Clone, Default)]
706pub struct FbxTexture {
707 /// Name supplied by the FBX texture node, when available.
708 pub name: Option<String>,
709 /// Embedded image bytes from `Video.Content` (PNG/JPG), when available.
710 pub content: Option<Vec<u8>>,
711 /// `RelativeFilename` / `FileName` from the FBX video/texture node.
712 pub filename: Option<String>,
713}
714
715/// Material object extracted from an FBX `Material` node.
716///
717/// Covers the canonical `KFbxSurfacePhong` / `KFbxSurfaceLambert` property
718/// set. Colors are linear RGB triples; scalar factors are unit-less.
719#[derive(Debug, Clone, Default)]
720pub struct FbxMaterial {
721 /// Name supplied by the FBX material node, when available.
722 pub name: Option<String>,
723 /// `ShadingModel` (`"Phong"`, `"Lambert"`, or empty for PBR/unknown).
724 pub shading_model: Option<String>,
725 /// `DiffuseColor`.
726 pub diffuse: Option<[f32; 3]>,
727 /// `SpecularColor`.
728 pub specular: Option<[f32; 3]>,
729 /// `EmissiveColor`.
730 pub emissive: Option<[f32; 3]>,
731 /// `AmbientColor`.
732 pub ambient: Option<[f32; 3]>,
733 /// `DiffuseFactor`.
734 pub diffuse_factor: Option<f32>,
735 /// `SpecularFactor`.
736 pub specular_factor: Option<f32>,
737 /// `Shininess` (Phong exponent).
738 pub shininess: Option<f32>,
739 /// `EmissiveFactor`.
740 pub emissive_factor: Option<f32>,
741 /// `ReflectionFactor` (≈ metallic).
742 pub reflection_factor: Option<f32>,
743 /// `TransparencyFactor` (1 = fully transparent).
744 pub transparency_factor: Option<f32>,
745 /// `Opacity` (1 = fully opaque; alternate form of `TransparencyFactor`).
746 pub opacity: Option<f32>,
747 /// `BumpFactor`.
748 pub bump_factor: Option<f32>,
749 /// Texture links, indexed into [`FbxScene::textures`].
750 pub textures: Vec<FbxTextureBinding>,
751}
752
753/// Animated TRS property of a node.
754#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
755pub enum FbxAnimChannelPath {
756 /// `Lcl Translation`.
757 Translation,
758 /// `Lcl Rotation`.
759 Rotation,
760 /// `Lcl Scaling`.
761 Scale,
762 /// `DeformPercent` on a blend-shape channel.
763 MorphWeight,
764}
765
766impl FbxAnimChannelPath {
767 /// Returns the FBX property name this channel drives.
768 pub fn property_name(self) -> &'static str {
769 match self {
770 FbxAnimChannelPath::Translation => "Lcl Translation",
771 FbxAnimChannelPath::Rotation => "Lcl Rotation",
772 FbxAnimChannelPath::Scale => "Lcl Scaling",
773 FbxAnimChannelPath::MorphWeight => "DeformPercent",
774 }
775 }
776
777 /// Parses an FBX connection property name into a channel path.
778 pub fn from_property_name(name: &str) -> Option<Self> {
779 match name {
780 "Lcl Translation" => Some(FbxAnimChannelPath::Translation),
781 "Lcl Rotation" => Some(FbxAnimChannelPath::Rotation),
782 "Lcl Scaling" => Some(FbxAnimChannelPath::Scale),
783 "DeformPercent" => Some(FbxAnimChannelPath::MorphWeight),
784 _ => None,
785 }
786 }
787
788 /// Number of output components for scalar and TRS paths.
789 pub fn component_count(self) -> usize {
790 match self {
791 FbxAnimChannelPath::MorphWeight => 1,
792 _ => 3,
793 }
794 }
795}
796
797/// Coarse interpolation kind decoded from `KeyAttrFlags`.
798#[derive(Debug, Clone, Copy, PartialEq, Eq)]
799pub enum FbxAnimInterpolation {
800 /// Hold previous value.
801 Step,
802 /// Linear blend between keys.
803 Linear,
804 /// Cubic Hermite blend with explicit per-key tangents.
805 Cubic,
806}
807
808impl FbxAnimInterpolation {
809 /// Maps to the FBX `KeyAttrFlags` interpolation bits.
810 pub fn to_key_attr_flags(self) -> i32 {
811 match self {
812 // ufbx / Blender FBX flag constants.
813 FbxAnimInterpolation::Step => 0x2,
814 FbxAnimInterpolation::Linear => 0x4,
815 // Explicit user tangents avoid Blender recomputing an auto curve.
816 FbxAnimInterpolation::Cubic => 0x8 | 0x400 | 0x800,
817 }
818 }
819
820 /// Decodes the interpolation mode from a `KeyAttrFlags` entry.
821 pub fn from_key_attr_flags(flags: i32) -> Self {
822 if flags & 0x2 != 0 {
823 FbxAnimInterpolation::Step
824 } else if flags & 0x8 != 0 {
825 FbxAnimInterpolation::Cubic
826 } else {
827 FbxAnimInterpolation::Linear
828 }
829 }
830}
831
832/// One animation sampler (a flat TRS or morph-weight track).
833#[derive(Debug, Clone)]
834pub struct FbxAnimSampler {
835 /// Strictly increasing keyframe times in seconds.
836 pub input: Vec<f32>,
837 /// Flattened keyframe values, `component_count()` values per input entry.
838 pub output: Vec<f32>,
839 /// Coarse interpolation mode.
840 pub interpolation: FbxAnimInterpolation,
841 /// Flattened incoming cubic tangents in output units per second.
842 pub in_tangents: Option<Vec<f32>>,
843 /// Flattened outgoing cubic tangents in output units per second.
844 pub out_tangents: Option<Vec<f32>>,
845}
846
847/// One animation channel: drives one TRS path or blend-shape weight.
848#[derive(Debug, Clone)]
849pub struct FbxAnimChannel {
850 /// Stable target Model id; never resolve a channel by display name.
851 pub node_id: FbxNodeId,
852 /// Name of the target model node (matches [`FbxSceneNode::name`]).
853 pub node_name: String,
854 /// Which node or blend-shape property the channel drives.
855 pub path: FbxAnimChannelPath,
856 /// Blend-shape target slot for [`FbxAnimChannelPath::MorphWeight`].
857 /// `None` for ordinary node TRS channels.
858 pub morph_target_index: Option<u32>,
859 /// Sampler data.
860 pub sampler: FbxAnimSampler,
861}
862
863/// One animation take (derived from an `AnimationStack` + its first layer).
864#[derive(Debug, Clone)]
865pub struct FbxAnimation {
866 /// Name of the `AnimationStack`, when available.
867 pub name: Option<String>,
868 /// Clip duration in seconds (max last sampler input).
869 pub duration: f32,
870 /// Flat list of TRS channels.
871 pub channels: Vec<FbxAnimChannel>,
872}
873
874/// Hierarchy, geometry, materials, and animation extracted from or written to FBX.
875///
876/// Unlike `draco_gltf::Document`, this is a deliberately lossy format-specific
877/// view. Use `FbxReader::read_nodes` when callers need the parsed FBX nodes.
878#[derive(Debug, Clone, Default)]
879pub struct FbxScene {
880 /// Source-only global settings used by compatible FBX re-export.
881 pub global_settings: Option<FbxGlobalSettings>,
882 /// Top-level FBX model nodes.
883 pub root_nodes: Vec<FbxSceneNode>,
884 /// Material objects, referenced by index from `mesh_instances` via
885 /// `FbxMeshInstance::material_indices`.
886 pub materials: Vec<FbxMaterial>,
887 /// Texture objects, referenced by index from `FbxMaterial::textures`.
888 pub textures: Vec<FbxTexture>,
889 /// Animation takes (one per `AnimationStack` + first `AnimationLayer`).
890 pub animations: Vec<FbxAnimation>,
891 /// Non-fatal notices collected while reading: tolerated container-layout
892 /// deviations, and FBX semantics the decoded scene cannot express.
893 ///
894 /// Filter on [`FbxWarningCode::is_data_loss`] to separate "this file is
895 /// unusual" from "something in this file is missing from the result".
896 pub warnings: Vec<FbxWarning>,
897}
898
899impl FbxScene {
900 /// Reads a supported FBX scene from binary bytes.
901 #[cfg(feature = "fbx-reader")]
902 pub fn from_bytes(bytes: &[u8]) -> io::Result<Self> {
903 let mut reader = crate::fbx_reader::FbxMemoryReader::from_bytes(bytes)?;
904 reader.read_scene()
905 }
906
907 /// Reads a supported FBX scene from binary bytes with explicit options.
908 ///
909 /// Use this to tighten [`crate::FbxDecodeLimits`] for untrusted input, or
910 /// to enable strict container validation.
911 #[cfg(feature = "fbx-reader")]
912 pub fn from_bytes_with_options(
913 bytes: &[u8],
914 options: crate::fbx_options::FbxReadOptions,
915 ) -> io::Result<Self> {
916 let mut reader =
917 crate::fbx_reader::FbxMemoryReader::from_bytes_with_options(bytes, options)?;
918 reader.read_scene()
919 }
920
921 /// Writes this scene as binary FBX bytes.
922 ///
923 /// This method is available with the `fbx-writer` feature. It preserves
924 /// mesh geometry, model names, hierarchy, local affine TRS transforms,
925 /// materials, textures, and node-TRS animation.
926 #[cfg(feature = "fbx-writer")]
927 pub fn to_bytes(&self) -> io::Result<Vec<u8>> {
928 let mut writer = crate::fbx_writer::FbxWriter::new();
929 writer.add_scene(self)?;
930 writer.write_to_vec()
931 }
932
933 /// Writes this scene as ASCII FBX text.
934 ///
935 /// The same document as [`Self::to_bytes`], spelled as text rather than as
936 /// records: diffable, and readable by any FBX importer, at the cost of the
937 /// precision [`crate::fbx_writer::FbxFormat`] describes.
938 #[cfg(feature = "fbx-writer")]
939 pub fn to_ascii_bytes(&self) -> io::Result<Vec<u8>> {
940 let mut writer =
941 crate::fbx_writer::FbxWriter::new().with_format(crate::fbx_writer::FbxFormat::Ascii);
942 writer.add_scene(self)?;
943 writer.write_to_vec()
944 }
945
946 /// Writes this scene in the FBX 6100 object model, binary container.
947 ///
948 /// For a pre-7000 source this is the round trip inside its own version;
949 /// the constraints [`FbxWriter::with_legacy_object_model`][legacy]
950 /// describes apply.
951 ///
952 /// [legacy]: crate::fbx_writer::FbxWriter::with_legacy_object_model
953 #[cfg(feature = "fbx-writer")]
954 pub fn to_legacy_bytes(&self) -> io::Result<Vec<u8>> {
955 let mut writer = crate::fbx_writer::FbxWriter::new().with_legacy_object_model();
956 writer.add_scene(self)?;
957 writer.write_to_vec()
958 }
959
960 /// Writes this scene in the FBX 6100 object model, ASCII container.
961 ///
962 /// The same document as [`Self::to_legacy_bytes`], spelled as text.
963 #[cfg(feature = "fbx-writer")]
964 pub fn to_legacy_ascii_bytes(&self) -> io::Result<Vec<u8>> {
965 let mut writer = crate::fbx_writer::FbxWriter::new()
966 .with_legacy_object_model()
967 .with_format(crate::fbx_writer::FbxFormat::Ascii);
968 writer.add_scene(self)?;
969 writer.write_to_vec()
970 }
971}