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}