Skip to main content

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}