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