Skip to main content

draco_io/
fbx_reader.rs

1//! FBX scene reader: a tree of [`FbxNode`](crate::fbx_container::FbxNode) in,
2//! an [`FbxScene`] out.
3//!
4//! Supports reading:
5//! - Vertex positions, normals, texture coordinates, colours, and tangents
6//! - Polygon/face indices, edges, smoothing flags, and crease weights
7//! - Model hierarchy and local transforms through [`FbxReader::read_scene`]
8//! - Phong/Lambert materials, textures, and per-polygon material indices
9//! - Skin clusters, bind poses, and blend-shape targets
10//! - Node-TRS animation (`AnimationStack` / `AnimationLayer` /
11//!   `AnimationCurveNode` / `AnimationCurve`) flattened into TRS channels
12//! - Cameras and lights, read into [`crate::FbxScene`] but never written back
13//!
14//! FBX pivots and inheritance rules and arbitrary metadata are not
15//! represented.
16//!
17//! This module reads only the node tree; the containers that produce it are
18//! [`crate::fbx_container`] for binary and [`crate::fbx_ascii`] for text.
19//! Nothing here knows which one it was given, which is what lets one scene
20//! layer serve both.
21//!
22//! # Example
23//!
24//! ```no_run
25//! use draco_io::fbx_reader::FbxReader;
26//! use draco_io::Reader;
27//!
28//! let mut reader = FbxReader::open("model.fbx")?;
29//! let meshes = reader.read_meshes()?;
30//! for mesh in meshes {
31//!     println!("Mesh has {} vertices", mesh.num_points());
32//! }
33//! # Ok::<(), std::io::Error>(())
34//! ```
35
36use std::collections::HashMap;
37use std::fs::{self, File};
38use std::io::{self, BufReader, Cursor, Read, Seek};
39use std::path::Path;
40
41use draco_core::mesh::Mesh;
42
43use crate::fbx_scene::push_warning;
44use crate::fbx_templates::{ObjectProperties, PropertyTemplates};
45use crate::fbx_transform::{
46    collect_transform_warnings, identity_transform, parse_transform, transform_array,
47};
48
49/// The container types, re-exported so `draco_io::fbx_reader::FbxNode` keeps
50/// resolving for callers written before the decoder moved out.
51pub use crate::fbx_container::{FbxMemoryReader, FbxNode, FbxProperty, FbxReader};
52
53#[derive(Debug)]
54struct FbxGeometrySource {
55    mesh: Mesh,
56    material_indices: Vec<i32>,
57    control_points: Vec<[f32; 3]>,
58    polygon_vertex_indices: Vec<i32>,
59    layers: FbxMeshLayers,
60    edges: Vec<i32>,
61}
62
63#[doc(hidden)]
64pub use crate::fbx_scene::{
65    FbxAnimChannel, FbxAnimChannelPath, FbxAnimInterpolation, FbxAnimSampler, FbxAnimation,
66    FbxBinormalSet, FbxColorSet, FbxCreaseKind, FbxCreaseLayer, FbxLayerSet, FbxMeshInstance,
67    FbxMeshLayers, FbxNodeAttribute, FbxNodeId, FbxNormalSet, FbxScene, FbxSceneNode,
68    FbxSmoothingLayer, FbxTangentSet, FbxTexture, FbxTextureBinding, FbxTextureSlot, FbxTransform,
69    FbxUvSet, FbxWarning, FbxWarningCode,
70};
71// Implement the Reader trait for the concrete BufReader<File> specialization.
72impl crate::traits::Reader for FbxReader<BufReader<File>> {
73    fn open<P: AsRef<Path>>(path: P) -> io::Result<Self> {
74        FbxReader::open(path)
75    }
76
77    fn read_meshes(&mut self) -> io::Result<Vec<draco_core::mesh::Mesh>> {
78        // Call the inherent method which already reads all meshes.
79        // Use fully qualified syntax to avoid recursion.
80        FbxReader::read_meshes(self)
81    }
82}
83
84impl crate::traits::Reader for FbxReader<Cursor<Vec<u8>>> {
85    fn open<P: AsRef<Path>>(path: P) -> io::Result<Self> {
86        Self::from_bytes(fs::read(path)?)
87    }
88
89    fn read_meshes(&mut self) -> io::Result<Vec<draco_core::mesh::Mesh>> {
90        FbxReader::read_meshes(self)
91    }
92}
93impl<R: Read + Seek> FbxReader<R> {
94    /// Reads the supported hierarchy, materials, textures, and animation from FBX.
95    ///
96    /// The result retains model names, local transforms, materialized mesh
97    /// geometry (positions, normals, UVs), per-polygon material indices,
98    /// Phong/Lambert materials, textures, and node-TRS animation. It
99    /// intentionally omits FBX pivots, pre/post rotations, inheritance rules,
100    /// cameras, and arbitrary metadata. It retains skin clusters, bind poses,
101    /// blend-shape deltas, and local TRS animation.
102    ///
103    /// ```no_run
104    /// use draco_io::FbxMemoryReader;
105    ///
106    /// let bytes = std::fs::read("model.fbx")?;
107    /// let mut reader = FbxMemoryReader::from_bytes(bytes)?;
108    /// let scene = reader.read_scene()?;
109    /// assert!(!scene.root_nodes.is_empty());
110    /// # Ok::<(), std::io::Error>(())
111    /// ```
112    pub fn read_scene(&mut self) -> io::Result<FbxScene> {
113        let nodes = self.read_nodes()?;
114        let global_settings = parse_global_settings(&nodes);
115
116        let index = FbxObjectIndex::build(&nodes);
117        // Borrow the fields the rest of this function reads. `index` stays
118        // whole so it can be handed to the animation pass as one argument.
119        let FbxObjectIndex {
120            model_map,
121            model_order,
122            geometry_map,
123            attribute_map,
124            material_map,
125            texture_map,
126            video_map,
127            deformer_map,
128            pose_map,
129            connections,
130            templates,
131            ..
132        } = &index;
133
134        // Container-layout notices raised by `read_nodes` above ride along
135        // with the semantic ones, so a caller sees every tolerated deviation.
136        let mut warnings = self.warnings().to_vec();
137        // A pre-7000 document keys its objects and connections by name rather
138        // than by id, and puts geometry on the `Model` itself. None of that is
139        // read, so the scene comes back structurally valid but empty. Saying so
140        // is the difference between "this file has no meshes" and "this reader
141        // did not look for them".
142        if index.name_keyed_objects > 0 {
143            let count = index.name_keyed_objects;
144            push_warning(
145                &mut warnings,
146                FbxWarningCode::NameKeyedObjectModel,
147                format!(
148                    "FBX document identifies its {count} objects by name rather than by id, \
149                     which is the pre-7000 layout; no geometry, materials or animation were \
150                     imported from it"
151                ),
152                None,
153            );
154        }
155        collect_transform_warnings(model_map, model_order, templates, &mut warnings);
156        let node_attributes = parse_node_attributes(
157            attribute_map,
158            model_map,
159            connections,
160            templates,
161            &mut warnings,
162        );
163
164        // ---- Materials and textures ---------------------------------------
165        let (materials, material_index_by_id, textures) = parse_materials_and_textures(
166            material_map,
167            texture_map,
168            video_map,
169            connections,
170            templates,
171        );
172
173        // ---- Model hierarchy + per-model materials -----------------------
174        // Map each model id to the list of material indices connected to it.
175        let mut model_material_ids: HashMap<i64, Vec<i32>> = HashMap::new();
176        for conn in connections {
177            if conn.kind == ConnectionKind::Oo
178                && material_map.contains_key(&conn.child)
179                && model_map.contains_key(&conn.parent)
180            {
181                let mat_index = material_index_by_id[&conn.child] as i32;
182                model_material_ids
183                    .entry(conn.parent)
184                    .or_default()
185                    .push(mat_index);
186            }
187        }
188
189        // Build parent map for models (same as before but over FbxConnection).
190        let mut model_children: HashMap<i64, Vec<i64>> = HashMap::new();
191        for conn in connections.iter() {
192            if model_map.contains_key(&conn.child) || model_map.contains_key(&conn.parent) {
193                model_children
194                    .entry(conn.parent)
195                    .or_default()
196                    .push(conn.child);
197            }
198        }
199
200        let ordered_model_ids = model_order;
201        let model_node_ids: HashMap<i64, FbxNodeId> = ordered_model_ids
202            .iter()
203            .copied()
204            .enumerate()
205            .map(|(index, id)| (id, FbxNodeId((index + 1) as u32)))
206            .collect();
207
208        // Map geometries to models and create mesh instances.
209        let mut model_mesh_instances: std::collections::HashMap<i64, Vec<FbxMeshInstance>> =
210            std::collections::HashMap::new();
211        let mut geometry_ids: Vec<i64> = geometry_map.keys().copied().collect();
212        geometry_ids.sort_unstable();
213        for geom_id in geometry_ids {
214            let geom_node = geometry_map[&geom_id];
215            if let Some(source) = geometry_to_mesh(geom_node, &mut warnings)? {
216                let mesh = &source.mesh;
217                let material_indices = source.material_indices.clone();
218                // find connection mapping geometry -> model
219                for conn in connections.iter() {
220                    if conn.child == geom_id && model_map.contains_key(&conn.parent) {
221                        // If the geometry does not carry its own material layer,
222                        // fall back to materials connected directly to the model.
223                        let mut indices = material_indices.clone();
224                        if let Some(model_mats) = model_material_ids.get(&conn.parent) {
225                            if indices.is_empty() {
226                                if !model_mats.is_empty() {
227                                    let first = model_mats[0];
228                                    // One entry per triangulated face.
229                                    indices = vec![first; mesh.num_faces()];
230                                }
231                            } else {
232                                // LayerElementMaterial values address the
233                                // material slots attached to this Model. Map
234                                // them back to the document-wide material
235                                // indices exposed by FbxScene.
236                                indices = indices
237                                    .into_iter()
238                                    .map(|slot| {
239                                        usize::try_from(slot)
240                                            .ok()
241                                            .and_then(|slot| model_mats.get(slot).copied())
242                                            .unwrap_or(model_mats[0])
243                                    })
244                                    .collect();
245                            }
246                        }
247                        let mesh_instance = FbxMeshInstance {
248                            name: object_name(geom_node),
249                            mesh: source.mesh.clone(),
250                            control_points: source.control_points.clone(),
251                            polygon_vertex_indices: source.polygon_vertex_indices.clone(),
252                            layers: source.layers.clone(),
253                            edges: source.edges.clone(),
254                            material_indices: indices,
255                            skin: parse_skin_for_geometry(
256                                geom_id,
257                                deformer_map,
258                                pose_map,
259                                connections,
260                                &model_node_ids,
261                            ),
262                            morph_targets: parse_morph_targets_for_geometry(
263                                geom_id,
264                                geometry_map,
265                                deformer_map,
266                                connections,
267                            ),
268                        };
269                        model_mesh_instances
270                            .entry(conn.parent)
271                            .or_default()
272                            .push(mesh_instance);
273                    }
274                }
275            }
276        }
277
278        // ---- Animation ----------------------------------------------------
279        let model_name_map: HashMap<i64, String> = model_map
280            .iter()
281            .filter_map(|(id, node)| object_name(node).map(|name| (*id, name)))
282            .collect();
283
284        let animations = self.parse_animations(
285            &nodes,
286            &index,
287            &model_name_map,
288            &model_node_ids,
289            &morph_animation_targets(geometry_map, deformer_map, connections, model_map),
290        );
291
292        // Build root nodes: any model with parent 0 (or with no parent present)
293        let mut root_nodes = Vec::new();
294        // find top-level model ids
295        let top_level: Vec<i64> = ordered_model_ids
296            .iter()
297            .copied()
298            .filter(|id| {
299                !connections
300                    .iter()
301                    .any(|conn| conn.child == *id && model_map.contains_key(&conn.parent))
302            })
303            .collect();
304
305        let graph = ModelGraph {
306            models: model_map,
307            children: &model_children,
308            mesh_instances: &model_mesh_instances,
309            node_ids: &model_node_ids,
310            attributes: &node_attributes,
311            templates,
312        };
313        for id in top_level {
314            root_nodes.push(build_model_node(id, &graph, &mut Vec::new()));
315        }
316
317        Ok(FbxScene {
318            global_settings,
319            root_nodes,
320            materials,
321            textures,
322            animations,
323            warnings,
324        })
325    }
326}
327
328// Build nodes recursively
329fn object_name(node: &FbxNode) -> Option<String> {
330    match node.properties.get(1) {
331        Some(FbxProperty::String(name)) => name
332            .split('\0')
333            .next()
334            .filter(|name| !name.is_empty())
335            .map(str::to_string),
336        _ => None,
337    }
338}
339
340/// Everything `build_model_node` needs about the document's model graph.
341///
342/// These six are only ever passed together, and passing them one by one put
343/// the function at the argument limit before it had a resolver to carry.
344struct ModelGraph<'a, 'n> {
345    models: &'a std::collections::HashMap<i64, &'n FbxNode>,
346    children: &'a std::collections::HashMap<i64, Vec<i64>>,
347    mesh_instances: &'a std::collections::HashMap<i64, Vec<FbxMeshInstance>>,
348    node_ids: &'a std::collections::HashMap<i64, FbxNodeId>,
349    attributes: &'a std::collections::HashMap<i64, FbxNodeAttribute>,
350    templates: &'a PropertyTemplates<'n>,
351}
352
353fn build_model_node(id: i64, graph: &ModelGraph<'_, '_>, ancestors: &mut Vec<i64>) -> FbxSceneNode {
354    let node_src = graph.models.get(&id).unwrap();
355    let mut node = FbxSceneNode::new(object_name(node_src));
356    node.id = graph.node_ids[&id];
357    if let Some((transform, transform_stack, has_complex_transform_stack)) =
358        parse_transform(ObjectProperties::new(node_src, graph.templates))
359    {
360        node.transform = Some(transform);
361        node.transform_stack = Some(transform_stack);
362        node.has_complex_transform_stack = has_complex_transform_stack;
363    }
364    node.attribute = graph.attributes.get(&id).cloned();
365    if let Some(mesh_instances) = graph.mesh_instances.get(&id) {
366        node.mesh_instances.extend(mesh_instances.clone());
367    }
368
369    // The ancestor check below stops a plain cycle. This bounds the
370    // rest: a document can chain models far deeper than any scene
371    // graph needs, and the depth is the file's to choose.
372    const MAX_MODEL_DEPTH: usize = 256;
373    if ancestors.len() >= MAX_MODEL_DEPTH {
374        return node;
375    }
376    if let Some(children) = graph.children.get(&id) {
377        ancestors.push(id);
378        for &cid in children {
379            // A document may connect a Model to one of its own
380            // ancestors -- `synthetic_id_collision_7500` in the ufbx
381            // corpus does -- and following that cycle recurses until
382            // the stack is gone. The scene simply stops there.
383            if graph.models.contains_key(&cid) && !ancestors.contains(&cid) {
384                node.children.push(build_model_node(cid, graph, ancestors));
385            }
386        }
387        ancestors.pop();
388    }
389    node
390}
391
392fn parse_global_settings(nodes: &[FbxNode]) -> Option<crate::fbx_scene::FbxGlobalSettings> {
393    let properties = nodes
394        .iter()
395        .find(|node| node.name == "GlobalSettings")?
396        .children
397        .iter()
398        .find(|node| node.name == "Properties70")?;
399    let integer = |property: &FbxNode| {
400        property.properties.iter().find_map(|value| match value {
401            FbxProperty::I16(value) => Some(*value as i32),
402            FbxProperty::I32(value) => Some(*value),
403            FbxProperty::I64(value) => i32::try_from(*value).ok(),
404            _ => None,
405        })
406    };
407    let number = |property: &FbxNode| {
408        property.properties.iter().find_map(|value| match value {
409            FbxProperty::F32(value) => Some(f64::from(*value)),
410            FbxProperty::F64(value) => Some(*value),
411            _ => None,
412        })
413    };
414    let mut result = crate::fbx_scene::FbxGlobalSettings::default();
415    for property in &properties.children {
416        let Some(FbxProperty::String(name)) = property.properties.first() else {
417            continue;
418        };
419        match name.as_str() {
420            "UpAxis" => result.up_axis = integer(property),
421            "UpAxisSign" => result.up_axis_sign = integer(property),
422            "FrontAxis" => result.front_axis = integer(property),
423            "FrontAxisSign" => result.front_axis_sign = integer(property),
424            "CoordAxis" => result.coord_axis = integer(property),
425            "CoordAxisSign" => result.coord_axis_sign = integer(property),
426            "UnitScaleFactor" => result.unit_scale_factor = number(property),
427            "OriginalUnitScaleFactor" => result.original_unit_scale_factor = number(property),
428            "TimeMode" => result.time_mode = integer(property),
429            _ => {}
430        }
431    }
432    (result != crate::fbx_scene::FbxGlobalSettings::default()).then_some(result)
433}
434
435fn child_i32_array(node: &FbxNode, child_name: &str) -> Vec<i32> {
436    node.children
437        .iter()
438        .find(|child| child.name == child_name)
439        .and_then(|child| child.properties.first())
440        .and_then(|value| match value {
441            FbxProperty::I32Array(values) => Some(values.clone()),
442            _ => None,
443        })
444        .unwrap_or_default()
445}
446
447fn child_f64_array(node: &FbxNode, child_name: &str) -> Vec<f64> {
448    node.children
449        .iter()
450        .find(|child| child.name == child_name)
451        .and_then(|child| child.properties.first())
452        .and_then(|value| match value {
453            FbxProperty::F64Array(values) => Some(values.clone()),
454            FbxProperty::F32Array(values) => Some(values.iter().copied().map(f64::from).collect()),
455            _ => None,
456        })
457        .unwrap_or_default()
458}
459
460fn parse_skin_for_geometry(
461    geometry_id: i64,
462    deformers: &std::collections::HashMap<i64, &FbxNode>,
463    poses: &std::collections::HashMap<i64, &FbxNode>,
464    connections: &[FbxConnection],
465    model_node_ids: &std::collections::HashMap<i64, FbxNodeId>,
466) -> Option<crate::fbx_scene::FbxSkin> {
467    let skin_ids: Vec<i64> = connections
468        .iter()
469        .filter(|connection| {
470            connection.kind == ConnectionKind::Oo && connection.parent == geometry_id
471        })
472        .map(|connection| connection.child)
473        .filter(|id| {
474            deformers
475                .get(id)
476                .and_then(|node| deformer_type(node).map(str::to_string))
477                .as_deref()
478                == Some("Skin")
479        })
480        .collect();
481    if skin_ids.is_empty() {
482        return None;
483    }
484
485    let mut clusters = Vec::new();
486    for skin_id in skin_ids {
487        for cluster_id in connections
488            .iter()
489            .filter(|connection| {
490                connection.kind == ConnectionKind::Oo && connection.parent == skin_id
491            })
492            .map(|connection| connection.child)
493        {
494            let Some(cluster) = deformers.get(&cluster_id) else {
495                continue;
496            };
497            if deformer_type(cluster) != Some("Cluster") {
498                continue;
499            }
500            let Some(joint_model_id) = connections
501                .iter()
502                .find(|connection| {
503                    connection.kind == ConnectionKind::Oo && connection.parent == cluster_id
504                })
505                .map(|connection| connection.child)
506            else {
507                continue;
508            };
509            let Some(&joint_node_id) = model_node_ids.get(&joint_model_id) else {
510                continue;
511            };
512            let indices = child_i32_array(cluster, "Indexes")
513                .into_iter()
514                .filter_map(|index| u32::try_from(index).ok())
515                .collect::<Vec<_>>();
516            let mut weights = child_f64_array(cluster, "Weights")
517                .into_iter()
518                .map(|weight| weight as f32)
519                .collect::<Vec<_>>();
520            weights.truncate(indices.len());
521            if weights.len() != indices.len() {
522                continue;
523            }
524            clusters.push(crate::fbx_scene::FbxSkinCluster {
525                joint_node_id,
526                control_point_indices: indices,
527                weights,
528                mesh_bind_transform: transform_array(cluster, "Transform")
529                    .unwrap_or_else(identity_transform),
530                joint_bind_transform: transform_array(cluster, "TransformLink")
531                    .unwrap_or_else(identity_transform),
532                armature_bind_transform: transform_array(cluster, "TransformAssociateModel"),
533            });
534        }
535    }
536
537    let mut bind_pose = Vec::new();
538    // Walk poses in id order. The dedup below is first-wins, so hash order
539    // would decide which `Pose` supplies a node's matrix when a file has more
540    // than one, and two reads of the same bytes could disagree.
541    let mut pose_ids: Vec<i64> = poses.keys().copied().collect();
542    pose_ids.sort_unstable();
543    for pose in pose_ids.iter().map(|id| poses[id]) {
544        let is_bind_pose = pose
545            .children
546            .iter()
547            .find(|child| child.name == "Type")
548            .and_then(|child| child.properties.first())
549            .and_then(|value| match value {
550                FbxProperty::String(value) => Some(value == "BindPose"),
551                _ => None,
552            })
553            .unwrap_or(false);
554        if !is_bind_pose {
555            continue;
556        }
557        for pose_node in &pose.children {
558            if pose_node.name != "PoseNode" {
559                continue;
560            }
561            let model_id = pose_node
562                .children
563                .iter()
564                .find(|child| child.name == "Node")
565                .and_then(|child| child.properties.first())
566                // Through `object_id`, because ASCII does not record an
567                // integer's width: an id that fits in 32 bits comes back as an
568                // `I32` there and the whole bind pose was dropped. Authored
569                // exports use ids far above that range, so only a document
570                // with small ids -- this crate's own output -- showed it.
571                .and_then(object_id);
572            let matrix = transform_array(pose_node, "Matrix");
573            if let (Some(_model_id), Some(matrix), Some(&node_id)) = (
574                model_id,
575                matrix,
576                model_id.and_then(|id| model_node_ids.get(&id)),
577            ) {
578                if !bind_pose.iter().any(|(existing, _)| *existing == node_id) {
579                    bind_pose.push((node_id, matrix));
580                }
581            }
582        }
583    }
584    Some(crate::fbx_scene::FbxSkin {
585        clusters,
586        bind_pose,
587    })
588}
589
590fn child_f64(node: &FbxNode, name: &str) -> Option<f64> {
591    node.children
592        .iter()
593        .find(|child| child.name == name)
594        .and_then(|child| child.properties.first())
595        .and_then(|value| match value {
596            FbxProperty::F64(value) => Some(*value),
597            FbxProperty::F32(value) => Some(*value as f64),
598            // ASCII writes a whole-valued double without a decimal point, so a
599            // `DeformPercent: 100` arrives as an integer and would otherwise
600            // read as a missing weight.
601            FbxProperty::I32(value) => Some(f64::from(*value)),
602            FbxProperty::I64(value) => Some(*value as f64),
603            _ => None,
604        })
605}
606
607fn parse_morph_targets_for_geometry(
608    geometry_id: i64,
609    geometries: &std::collections::HashMap<i64, &FbxNode>,
610    deformers: &std::collections::HashMap<i64, &FbxNode>,
611    connections: &[FbxConnection],
612) -> Vec<crate::fbx_scene::FbxMorphTarget> {
613    let mut targets = Vec::new();
614    for blend_shape_id in connections
615        .iter()
616        .filter(|connection| {
617            connection.kind == ConnectionKind::Oo && connection.parent == geometry_id
618        })
619        .map(|connection| connection.child)
620    {
621        let Some(blend_shape) = deformers.get(&blend_shape_id) else {
622            continue;
623        };
624        if deformer_type(blend_shape) != Some("BlendShape") {
625            continue;
626        }
627        for channel_id in connections
628            .iter()
629            .filter(|connection| {
630                connection.kind == ConnectionKind::Oo && connection.parent == blend_shape_id
631            })
632            .map(|connection| connection.child)
633        {
634            let Some(channel) = deformers.get(&channel_id) else {
635                continue;
636            };
637            if deformer_type(channel) != Some("BlendShapeChannel") {
638                continue;
639            }
640            for shape_id in connections
641                .iter()
642                .filter(|connection| {
643                    connection.kind == ConnectionKind::Oo && connection.parent == channel_id
644                })
645                .map(|connection| connection.child)
646            {
647                let Some(shape) = geometries.get(&shape_id) else {
648                    continue;
649                };
650                let indices = child_i32_array(shape, "Indexes")
651                    .into_iter()
652                    .filter_map(|index| u32::try_from(index).ok())
653                    .collect::<Vec<_>>();
654                let vertices = child_f64_array(shape, "Vertices");
655                if vertices.len() != indices.len() * 3 {
656                    continue;
657                }
658                let position_deltas = vertices
659                    .chunks_exact(3)
660                    .map(|values| [values[0] as f32, values[1] as f32, values[2] as f32])
661                    .collect();
662                let full_weight = child_f64_array(channel, "FullWeights")
663                    .first()
664                    .copied()
665                    .unwrap_or(100.0) as f32;
666                targets.push(crate::fbx_scene::FbxMorphTarget {
667                    name: match shape.properties.get(1) {
668                        Some(FbxProperty::String(name)) => {
669                            name.split('\0').next().map(str::to_string)
670                        }
671                        _ => None,
672                    },
673                    control_point_indices: indices,
674                    position_deltas,
675                    normal_deltas: None,
676                    default_weight: child_f64(channel, "DeformPercent").unwrap_or(0.0) as f32,
677                    full_weight,
678                });
679            }
680        }
681    }
682    targets
683}
684
685/// Resolve BlendShapeChannel object ids to their owning Model and target slot.
686/// FBX animation curves target the channel deformer rather than the mesh
687/// model, so this bridge is required to expose them through the scene API.
688fn morph_animation_targets(
689    geometries: &std::collections::HashMap<i64, &FbxNode>,
690    deformers: &std::collections::HashMap<i64, &FbxNode>,
691    connections: &[FbxConnection],
692    models: &std::collections::HashMap<i64, &FbxNode>,
693) -> std::collections::HashMap<i64, (i64, u32)> {
694    let mut result = std::collections::HashMap::new();
695    for geometry_id in geometries.keys().copied() {
696        let Some(model_id) = connections.iter().find_map(|connection| {
697            (connection.kind == ConnectionKind::Oo
698                && connection.child == geometry_id
699                && models.contains_key(&connection.parent))
700            .then_some(connection.parent)
701        }) else {
702            continue;
703        };
704        for blend_shape_id in connections
705            .iter()
706            .filter(|connection| {
707                connection.kind == ConnectionKind::Oo
708                    && connection.parent == geometry_id
709                    && deformers
710                        .get(&connection.child)
711                        .and_then(|node| deformer_type(node))
712                        == Some("BlendShape")
713            })
714            .map(|connection| connection.child)
715        {
716            for (index, channel_id) in connections
717                .iter()
718                .filter(|connection| {
719                    connection.kind == ConnectionKind::Oo
720                        && connection.parent == blend_shape_id
721                        && deformers
722                            .get(&connection.child)
723                            .and_then(|node| deformer_type(node))
724                            == Some("BlendShapeChannel")
725                })
726                .map(|connection| connection.child)
727                .enumerate()
728            {
729                result.insert(channel_id, (model_id, index as u32));
730            }
731        }
732    }
733    result
734}
735
736/// FBX object-to-object connection type.
737#[derive(Debug, Clone, Copy, PartialEq, Eq)]
738enum ConnectionKind {
739    /// `OO` object-to-object connection.
740    Oo,
741    /// `OP` object-to-property connection (carries a property name).
742    Op,
743}
744
745/// Every `Objects` entry indexed by id, plus the `Connections` graph.
746///
747/// Built once per document. The maps are for lookup only: iterating a
748/// `HashMap` would make node ids, root order and channel order depend on the
749/// process rather than the file, so anything order-sensitive walks
750/// [`Self::model_order`] or a sorted key list instead.
751struct FbxObjectIndex<'a> {
752    model_map: HashMap<i64, &'a FbxNode>,
753    /// Authored `Model` order, which `model_map` cannot preserve.
754    model_order: Vec<i64>,
755    geometry_map: HashMap<i64, &'a FbxNode>,
756    material_map: HashMap<i64, &'a FbxNode>,
757    texture_map: HashMap<i64, &'a FbxNode>,
758    video_map: HashMap<i64, &'a FbxNode>,
759    astack_map: HashMap<i64, &'a FbxNode>,
760    alayer_map: HashMap<i64, &'a FbxNode>,
761    acnode_map: HashMap<i64, &'a FbxNode>,
762    acurve_map: HashMap<i64, &'a FbxNode>,
763    deformer_map: HashMap<i64, &'a FbxNode>,
764    pose_map: HashMap<i64, &'a FbxNode>,
765    attribute_map: HashMap<i64, &'a FbxNode>,
766    connections: Vec<FbxConnection>,
767    /// `Objects` children skipped because they are keyed by name, not by id.
768    ///
769    /// FBX 6100 and earlier identify objects by a name string ending in a
770    /// class marker, and connect them by that string rather than by the `i64`
771    /// id 7.x uses. Nothing in this index can hold them, so counting them is
772    /// how the reader notices it decoded a document it does not understand.
773    name_keyed_objects: usize,
774    /// Class defaults the document states once, in `Definitions`.
775    templates: PropertyTemplates<'a>,
776}
777
778impl<'a> FbxObjectIndex<'a> {
779    fn build(nodes: &'a [FbxNode]) -> Self {
780        let mut index = Self {
781            model_map: HashMap::new(),
782            model_order: Vec::new(),
783            geometry_map: HashMap::new(),
784            material_map: HashMap::new(),
785            texture_map: HashMap::new(),
786            video_map: HashMap::new(),
787            astack_map: HashMap::new(),
788            alayer_map: HashMap::new(),
789            acnode_map: HashMap::new(),
790            acurve_map: HashMap::new(),
791            deformer_map: HashMap::new(),
792            pose_map: HashMap::new(),
793            attribute_map: HashMap::new(),
794            connections: Vec::new(),
795            name_keyed_objects: 0,
796            templates: PropertyTemplates::build(nodes),
797        };
798
799        for node in nodes {
800            if node.name == "Objects" {
801                for child in &node.children {
802                    let Some(id) = child.properties.first().and_then(object_id) else {
803                        if matches!(child.properties.first(), Some(FbxProperty::String(_))) {
804                            index.name_keyed_objects += 1;
805                        }
806                        continue;
807                    };
808                    let id = &id;
809                    match child.name.as_str() {
810                        "Model" => {
811                            // Keep the authored order only for ids seen first.
812                            let first_occurrence = index.model_map.insert(*id, child).is_none();
813                            if first_occurrence {
814                                index.model_order.push(*id);
815                            }
816                        }
817                        "Geometry" => drop(index.geometry_map.insert(*id, child)),
818                        "Material" => drop(index.material_map.insert(*id, child)),
819                        "Texture" => drop(index.texture_map.insert(*id, child)),
820                        "Video" => drop(index.video_map.insert(*id, child)),
821                        "AnimationStack" => drop(index.astack_map.insert(*id, child)),
822                        "AnimationLayer" => drop(index.alayer_map.insert(*id, child)),
823                        "AnimationCurveNode" => drop(index.acnode_map.insert(*id, child)),
824                        "AnimationCurve" => drop(index.acurve_map.insert(*id, child)),
825                        "Pose" => drop(index.pose_map.insert(*id, child)),
826                        "NodeAttribute" => drop(index.attribute_map.insert(*id, child)),
827                        "Deformer" => drop(index.deformer_map.insert(*id, child)),
828                        _ => {}
829                    }
830                }
831            } else if node.name == "Connections" {
832                index
833                    .connections
834                    .extend(node.children.iter().filter_map(FbxConnection::from_node));
835            }
836        }
837        index
838    }
839}
840
841/// Reads a float array, whatever precision it was stored at.
842///
843/// The binary container tags single and double precision separately, but ASCII
844/// writes a bare number and cannot. A consumer that matched only one width
845/// found nothing in an ASCII document, which is how animation curves came back
846/// empty from files whose objects and connections had parsed perfectly.
847fn float_array(property: &FbxProperty) -> Option<Vec<f32>> {
848    match property {
849        FbxProperty::F32Array(values) => Some(values.clone()),
850        FbxProperty::F64Array(values) => Some(values.iter().map(|v| *v as f32).collect()),
851        _ => None,
852    }
853}
854
855/// Reads an FBX object id, whatever width it was stored at.
856///
857/// The binary container always writes these as `i64`. ASCII writes a bare
858/// number, so an id small enough to fit in `i32` arrives as one -- matching
859/// only `I64` skipped every object in such a document, and the scene came back
860/// empty with nothing to explain it.
861fn object_id(property: &FbxProperty) -> Option<i64> {
862    match property {
863        FbxProperty::I64(value) => Some(*value),
864        FbxProperty::I32(value) => Some(i64::from(*value)),
865        _ => None,
866    }
867}
868
869/// A parsed FBX connection entry.
870#[derive(Debug, Clone)]
871struct FbxConnection {
872    kind: ConnectionKind,
873    child: i64,
874    parent: i64,
875    property: Option<String>,
876}
877
878impl FbxConnection {
879    /// Parses one `C` entry, skipping relation codes this reader ignores.
880    fn from_node(node: &FbxNode) -> Option<Self> {
881        let kind = match node.properties.first() {
882            Some(FbxProperty::String(code)) if code == "OO" => ConnectionKind::Oo,
883            Some(FbxProperty::String(code)) if code == "OP" => ConnectionKind::Op,
884            _ => return None,
885        };
886        let child = node.properties.get(1).and_then(object_id)?;
887        let parent = node.properties.get(2).and_then(object_id)?;
888        let property = match node.properties.get(3) {
889            Some(FbxProperty::String(name)) => Some(name.clone()),
890            _ => None,
891        };
892        Some(Self {
893            kind,
894            child,
895            parent,
896            property,
897        })
898    }
899}
900
901/// Decodes every `Material` and `Texture` object, resolving each material's
902/// texture bindings to indices into the returned texture list.
903///
904/// Both lists are ordered by FBX object id rather than hash order, so a
905/// document always decodes to the same material and texture indices.
906fn parse_materials_and_textures<'a>(
907    material_map: &HashMap<i64, &'a FbxNode>,
908    texture_map: &HashMap<i64, &FbxNode>,
909    video_map: &HashMap<i64, &FbxNode>,
910    connections: &[FbxConnection],
911    templates: &PropertyTemplates<'a>,
912) -> (
913    Vec<crate::fbx_scene::FbxMaterial>,
914    HashMap<i64, usize>,
915    Vec<crate::fbx_scene::FbxTexture>,
916) {
917    let mut materials: Vec<crate::fbx_scene::FbxMaterial> = Vec::new();
918    let mut material_index_by_id: HashMap<i64, usize> = HashMap::new();
919    let mut material_ids: Vec<i64> = material_map.keys().copied().collect();
920    material_ids.sort_unstable();
921    for id in material_ids {
922        let mut material = parse_material(ObjectProperties::new(material_map[&id], templates));
923        material.textures = collect_material_texture_bindings(id, texture_map, connections);
924        material_index_by_id.insert(id, materials.len());
925        materials.push(material);
926    }
927
928    // Map each Texture to the Video that carries its bytes. FBX writes the
929    // connection in either direction, so accept both.
930    let mut texture_video: HashMap<i64, i64> = HashMap::new();
931    for conn in connections {
932        if conn.kind != ConnectionKind::Oo {
933            continue;
934        }
935        if texture_map.contains_key(&conn.child) && video_map.contains_key(&conn.parent) {
936            texture_video.entry(conn.child).or_insert(conn.parent);
937        }
938        if video_map.contains_key(&conn.child) && texture_map.contains_key(&conn.parent) {
939            texture_video.entry(conn.parent).or_insert(conn.child);
940        }
941    }
942
943    let mut textures: Vec<crate::fbx_scene::FbxTexture> = Vec::new();
944    let mut texture_index_by_id: HashMap<i64, usize> = HashMap::new();
945    let mut texture_ids: Vec<i64> = texture_map.keys().copied().collect();
946    texture_ids.sort_unstable();
947    for id in texture_ids {
948        let mut texture = parse_texture(texture_map[&id]);
949        if let Some(video) = texture_video.get(&id).and_then(|id| video_map.get(id)) {
950            let from_video = parse_texture(video);
951            texture.content = texture.content.or(from_video.content);
952            texture.filename = texture.filename.or(from_video.filename);
953            texture.name = texture.name.or(from_video.name);
954        }
955        texture_index_by_id.insert(id, textures.len());
956        textures.push(texture);
957    }
958
959    // Bindings carry FBX texture ids until now; rewrite them as scene indices.
960    for material in &mut materials {
961        for binding in &mut material.textures {
962            let fbx_id = binding.texture_index as i64;
963            if let Some(&resolved) = texture_index_by_id.get(&fbx_id) {
964                binding.texture_index = resolved;
965            }
966        }
967    }
968
969    (materials, material_index_by_id, textures)
970}
971
972/// FBX Deformer objects carry their effective kind in the third object
973/// property; the second name component is merely `Deformer`/`SubDeformer`.
974fn deformer_type(node: &FbxNode) -> Option<&str> {
975    match node.properties.get(2) {
976        Some(FbxProperty::String(value)) if !value.is_empty() => Some(value.as_str()),
977        _ => None,
978    }
979}
980
981/// Collects material property texture bindings as placeholders; the FBX
982/// texture id is stored in `texture_index` and resolved to a scene index by
983/// the caller after the texture list is finalized.
984fn collect_material_texture_bindings(
985    material_id: i64,
986    texture_map: &std::collections::HashMap<i64, &FbxNode>,
987    connections: &[FbxConnection],
988) -> Vec<crate::fbx_scene::FbxTextureBinding> {
989    let mut bindings = Vec::new();
990    for conn in connections {
991        if conn.kind != ConnectionKind::Op || conn.parent != material_id {
992            continue;
993        }
994        let Some(slot_name) = conn.property.as_deref() else {
995            continue;
996        };
997        let Some(slot) = crate::fbx_scene::FbxTextureSlot::from_property_name(slot_name) else {
998            continue;
999        };
1000        if !texture_map.contains_key(&conn.child) {
1001            continue;
1002        }
1003        bindings.push(crate::fbx_scene::FbxTextureBinding {
1004            slot,
1005            texture_index: conn.child as usize,
1006        });
1007    }
1008    bindings
1009}
1010
1011/// Resolves each Model's `NodeAttribute`, keeping the classes this crate
1012/// represents and reporting the rest.
1013///
1014/// Attributes are attached to their Model by an ordinary object connection, so
1015/// this walks the connection list once rather than searching per node. Ids are
1016/// visited in sorted order so a document always produces the same warnings in
1017/// the same order.
1018fn parse_node_attributes<'a>(
1019    attribute_map: &HashMap<i64, &'a FbxNode>,
1020    model_map: &HashMap<i64, &FbxNode>,
1021    connections: &[FbxConnection],
1022    templates: &PropertyTemplates<'a>,
1023    warnings: &mut Vec<FbxWarning>,
1024) -> HashMap<i64, FbxNodeAttribute> {
1025    let mut by_model: Vec<(i64, i64)> = connections
1026        .iter()
1027        .filter(|conn| {
1028            attribute_map.contains_key(&conn.child) && model_map.contains_key(&conn.parent)
1029        })
1030        .map(|conn| (conn.parent, conn.child))
1031        .collect();
1032    by_model.sort_unstable();
1033
1034    let mut resolved = HashMap::new();
1035    for (model_id, attribute_id) in by_model {
1036        let node = attribute_map[&attribute_id];
1037        let class = match node.properties.get(2) {
1038            Some(FbxProperty::String(class)) => class.as_str(),
1039            _ => continue,
1040        };
1041        match class {
1042            "Camera" => {
1043                let properties = ObjectProperties::new(node, templates);
1044                resolved.insert(model_id, FbxNodeAttribute::Camera(parse_camera(properties)));
1045            }
1046            "Light" => {
1047                let properties = ObjectProperties::new(node, templates);
1048                resolved.insert(model_id, FbxNodeAttribute::Light(parse_light(properties)));
1049            }
1050            // A skeleton attribute is consumed by the skin path and a `Null`
1051            // carries nothing but a transform, so neither is a loss worth
1052            // reporting. The rest describe something the scene will not have.
1053            "LimbNode" | "Limb" | "Null" | "Root" => {}
1054            other => push_warning(
1055                warnings,
1056                FbxWarningCode::DroppedNodeAttribute,
1057                format!(
1058                    "FBX NodeAttribute of class {other} is not represented, so its properties \
1059                     are absent from the scene"
1060                ),
1061                Some(other),
1062            ),
1063        }
1064    }
1065    resolved
1066}
1067
1068fn parse_camera(properties: ObjectProperties<'_>) -> crate::fbx_scene::FbxCamera {
1069    let scalar = |name: &str| properties.get(name).and_then(property_scalar);
1070    let vector = |name: &str| properties.get(name).and_then(property_vec3);
1071    crate::fbx_scene::FbxCamera {
1072        position: vector("Position"),
1073        interest_position: vector("InterestPosition"),
1074        up_vector: vector("UpVector"),
1075        projection_type: scalar("CameraProjectionType").map(|v| v as i32),
1076        field_of_view: scalar("FieldOfView"),
1077        field_of_view_x: scalar("FieldOfViewX"),
1078        field_of_view_y: scalar("FieldOfViewY"),
1079        focal_length: scalar("FocalLength"),
1080        near_plane: scalar("NearPlane"),
1081        far_plane: scalar("FarPlane"),
1082        aspect_width: scalar("AspectWidth"),
1083        aspect_height: scalar("AspectHeight"),
1084        film_width: scalar("FilmWidth"),
1085        film_height: scalar("FilmHeight"),
1086        film_aspect_ratio: scalar("FilmAspectRatio"),
1087        aperture_mode: scalar("ApertureMode").map(|v| v as i32),
1088        ortho_zoom: scalar("OrthoZoom"),
1089    }
1090}
1091
1092fn parse_light(properties: ObjectProperties<'_>) -> crate::fbx_scene::FbxLight {
1093    let scalar = |name: &str| properties.get(name).and_then(property_scalar);
1094    crate::fbx_scene::FbxLight {
1095        light_type: scalar("LightType").map(|v| v as i32),
1096        color: properties.get("Color").and_then(property_vec3),
1097        intensity: scalar("Intensity"),
1098        cast_light: scalar("CastLight").map(|v| v != 0.0),
1099        cast_shadows: scalar("CastShadows").map(|v| v != 0.0),
1100        decay_type: scalar("DecayType").map(|v| v as i32),
1101        decay_start: scalar("DecayStart"),
1102    }
1103}
1104
1105fn property_scalar(prop: &FbxNode) -> Option<f32> {
1106    // Properties70 P node layout: [name, type, subtype, flags, value(s)...]
1107    // Scalar properties start at index 4.
1108    for value in prop.properties.iter().skip(4) {
1109        match value {
1110            FbxProperty::F64(v) => return Some(*v as f32),
1111            FbxProperty::F32(v) => return Some(*v),
1112            FbxProperty::I32(v) => return Some(*v as f32),
1113            FbxProperty::I64(v) => return Some(*v as f32),
1114            _ => {}
1115        }
1116    }
1117    None
1118}
1119
1120fn property_vec3(prop: &FbxNode) -> Option<[f32; 3]> {
1121    let values: Vec<f32> = prop
1122        .properties
1123        .iter()
1124        .skip(4)
1125        .filter_map(|value| match value {
1126            FbxProperty::F64(v) => Some(*v as f32),
1127            FbxProperty::F32(v) => Some(*v),
1128            _ => None,
1129        })
1130        .take(3)
1131        .collect();
1132    (values.len() == 3).then(|| [values[0], values[1], values[2]])
1133}
1134
1135fn parse_material(properties: ObjectProperties<'_>) -> crate::fbx_scene::FbxMaterial {
1136    let name = match properties.node().properties.get(1) {
1137        Some(FbxProperty::String(raw)) => raw
1138            .split('\0')
1139            .next()
1140            .filter(|s| !s.is_empty())
1141            .map(str::to_string),
1142        _ => None,
1143    };
1144    let shading_model = read_shading_model(properties);
1145
1146    let get_color = |name: &str| properties.get(name).and_then(property_vec3);
1147    let get_scalar = |name: &str| properties.get(name).and_then(property_scalar);
1148
1149    crate::fbx_scene::FbxMaterial {
1150        name,
1151        shading_model,
1152        diffuse: get_color("DiffuseColor"),
1153        specular: get_color("SpecularColor"),
1154        emissive: get_color("EmissiveColor"),
1155        ambient: get_color("AmbientColor"),
1156        diffuse_factor: get_scalar("DiffuseFactor"),
1157        specular_factor: get_scalar("SpecularFactor"),
1158        shininess: get_scalar("Shininess"),
1159        emissive_factor: get_scalar("EmissiveFactor"),
1160        reflection_factor: get_scalar("ReflectionFactor"),
1161        transparency_factor: get_scalar("TransparencyFactor"),
1162        opacity: get_scalar("Opacity"),
1163        bump_factor: get_scalar("BumpFactor"),
1164        textures: Vec::new(),
1165    }
1166}
1167
1168/// Reads `ShadingModel`, which a document may state in any of four places.
1169///
1170/// In order, because they disagree: a `Properties70` entry on the material, a
1171/// `ShadingModel` node beside `Properties70`, the class template, and finally
1172/// the object record's own class string.
1173///
1174/// The middle two are what make the order matter. Maya writes the material's
1175/// real model -- `lambert`, `phong`, `unknown`, differing per material -- as
1176/// the sibling node, and 174 of the 188 materials in this crate's corpus get
1177/// theirs only from the template. Consulting the template before the sibling
1178/// would relabel every one of those Maya materials with the template's single
1179/// class default, which is how a rewrite turned a `phong` material into a
1180/// `Lambert` one.
1181fn read_shading_model(properties: ObjectProperties<'_>) -> Option<String> {
1182    let object = properties.node();
1183    let from_own_properties = properties
1184        .node()
1185        .children
1186        .iter()
1187        .filter(|child| child.name == "Properties70")
1188        .find_map(|block| crate::fbx_templates::find_property(block, "ShadingModel"))
1189        .and_then(string_value);
1190    let from_sibling_node = object
1191        .children
1192        .iter()
1193        .find(|child| child.name == "ShadingModel")
1194        .and_then(|child| match child.properties.first() {
1195            Some(FbxProperty::String(model)) if !model.is_empty() => Some(model.clone()),
1196            _ => None,
1197        });
1198    let from_template = properties
1199        .template()
1200        .and_then(|block| crate::fbx_templates::find_property(block, "ShadingModel"))
1201        .and_then(string_value);
1202    let from_class = match object.properties.get(2) {
1203        Some(FbxProperty::String(raw)) if !raw.is_empty() => Some(raw.clone()),
1204        _ => None,
1205    };
1206
1207    from_own_properties
1208        .or(from_sibling_node)
1209        .or(from_template)
1210        .or(from_class)
1211}
1212
1213/// The first string value of a `P` record, past its four name and type fields.
1214fn string_value(property: &FbxNode) -> Option<String> {
1215    property
1216        .properties
1217        .iter()
1218        .skip(4)
1219        .find_map(|value| match value {
1220            FbxProperty::String(text) => Some(text.clone()),
1221            _ => None,
1222        })
1223}
1224
1225fn parse_texture(node: &FbxNode) -> crate::fbx_scene::FbxTexture {
1226    let name = match node.properties.get(1) {
1227        Some(FbxProperty::String(raw)) => raw
1228            .split('\0')
1229            .next()
1230            .filter(|s| !s.is_empty())
1231            .map(str::to_string),
1232        _ => None,
1233    };
1234    let mut filename = None;
1235    let mut content = None;
1236    for child in &node.children {
1237        match child.name.as_str() {
1238            "RelativeFilename" | "FileName" | "Filename" if filename.is_none() => {
1239                if let Some(FbxProperty::String(s)) = child.properties.first() {
1240                    if !s.is_empty() {
1241                        filename = Some(s.clone());
1242                    }
1243                }
1244            }
1245            "Content" => {
1246                if let Some(FbxProperty::Raw(bytes)) = child.properties.first() {
1247                    if !bytes.is_empty() {
1248                        content = Some(bytes.clone());
1249                    }
1250                }
1251            }
1252            _ => {}
1253        }
1254    }
1255    crate::fbx_scene::FbxTexture {
1256        name,
1257        content,
1258        filename,
1259    }
1260}
1261impl<R: Read + Seek> FbxReader<R> {
1262    /// Read meshes from the FBX file.
1263    pub fn read_meshes(&mut self) -> io::Result<Vec<Mesh>> {
1264        let nodes = self.read_nodes()?;
1265        let mut meshes = Vec::new();
1266        // Collected separately because `geometry_to_mesh` borrows `self`
1267        // immutably; merged back afterwards so this path reports the same
1268        // geometry notices `read_scene` does.
1269        let mut warnings = Vec::new();
1270
1271        // Find Objects node
1272        for node in &nodes {
1273            if node.name == "Objects" {
1274                for child in &node.children {
1275                    if child.name == "Geometry" {
1276                        if let Some(source) = geometry_to_mesh(child, &mut warnings)? {
1277                            meshes.push(source.mesh);
1278                        }
1279                    }
1280                }
1281            }
1282        }
1283
1284        self.extend_warnings(warnings);
1285        Ok(meshes)
1286    }
1287}
1288
1289/// The `LayerElement*` children of one geometry node, bucketed by family.
1290///
1291/// Collecting them before parsing keeps the dispatch over node names -- which
1292/// has to be exhaustive so an unknown family raises a warning rather than
1293/// vanishing -- separate from the per-family decoding.
1294#[derive(Default)]
1295struct RawLayerNodes<'a> {
1296    normals: Vec<&'a FbxNode>,
1297    uvs: Vec<&'a FbxNode>,
1298    colors: Vec<&'a FbxNode>,
1299    tangents: Vec<&'a FbxNode>,
1300    binormals: Vec<&'a FbxNode>,
1301    smoothing: Vec<&'a FbxNode>,
1302    creases: Vec<(FbxCreaseKind, &'a FbxNode)>,
1303    material: Option<&'a FbxNode>,
1304}
1305
1306/// The element counts a non-corner layer's length has to agree with.
1307#[derive(Clone, Copy)]
1308struct LayerDomains {
1309    edges: Option<usize>,
1310    polygons: usize,
1311    control_points: usize,
1312}
1313
1314impl LayerDomains {
1315    /// Resolves what a mapping name claims about a layer's length.
1316    ///
1317    /// `ByEdge` with no `Edges` array is deliberately unverifiable rather than
1318    /// wrong: FBX does not require the array, and the layer then addresses the
1319    /// edges an importer would reconstruct from the faces. This crate does not
1320    /// reconstruct them, so it cannot check the length -- but it must not
1321    /// destroy the data either, since preserving it verbatim is what makes a
1322    /// rewrite lossless.
1323    fn check(self, mapping: Option<&str>) -> DomainCheck {
1324        match mapping {
1325            Some("ByEdge") => match self.edges {
1326                Some(count) => DomainCheck::Expect(count),
1327                None => DomainCheck::Unverifiable,
1328            },
1329            Some("ByPolygon") => DomainCheck::Expect(self.polygons),
1330            Some("ByVertice") | Some("ByVertex") | Some("ByControlPoint") => {
1331                DomainCheck::Expect(self.control_points)
1332            }
1333            _ => DomainCheck::Unknown,
1334        }
1335    }
1336}
1337
1338/// Convert a Geometry node to a Mesh, plus per-triangle material indices.
1339///
1340/// The returned `material_indices` align with the fan-triangulated face
1341/// order of the Draco `Mesh` (one entry per triangle). The list is empty
1342/// when the geometry does not carry a `LayerElementMaterial` layer.
1343fn geometry_to_mesh(
1344    geometry: &FbxNode,
1345    warnings: &mut Vec<FbxWarning>,
1346) -> io::Result<Option<FbxGeometrySource>> {
1347    let mut vertices: Option<Vec<f64>> = None;
1348    let mut polygon_indices: Option<Vec<i32>> = None;
1349    let mut edges: Vec<i32> = Vec::new();
1350    let mut raw = RawLayerNodes::default();
1351
1352    for child in &geometry.children {
1353        match child.name.as_str() {
1354            "Vertices" => {
1355                if let Some(FbxProperty::F64Array(arr)) = child.properties.first() {
1356                    vertices = Some(arr.clone());
1357                }
1358            }
1359            "Edges" => {
1360                if let Some(FbxProperty::I32Array(arr)) = child.properties.first() {
1361                    edges = arr.clone();
1362                }
1363            }
1364            "PolygonVertexIndex" => {
1365                if let Some(FbxProperty::I32Array(arr)) = child.properties.first() {
1366                    polygon_indices = Some(arr.clone());
1367                }
1368            }
1369            "LayerElementNormal" => raw.normals.push(child),
1370            "LayerElementColor" => raw.colors.push(child),
1371            "LayerElementUV" => raw.uvs.push(child),
1372            "LayerElementTangent" => raw.tangents.push(child),
1373            "LayerElementBinormal" => raw.binormals.push(child),
1374            "LayerElementSmoothing" => raw.smoothing.push(child),
1375            "LayerElementEdgeCrease" => raw.creases.push((FbxCreaseKind::Edge, child)),
1376            "LayerElementVertexCrease" => raw.creases.push((FbxCreaseKind::Vertex, child)),
1377            "LayerElementMaterial" if raw.material.is_none() => {
1378                raw.material = Some(child);
1379            }
1380            // Any layer family this crate does not import lands here. They
1381            // used to vanish without a trace; naming them makes the gap
1382            // visible to a caller instead of only to the source code.
1383            other if other.starts_with("LayerElement") => push_warning(
1384                warnings,
1385                FbxWarningCode::DroppedLayerElement,
1386                format!("FBX {other} is not imported, so its data is absent from the scene"),
1387                Some(other),
1388            ),
1389            _ => {}
1390        }
1391    }
1392
1393    let vertices = match vertices {
1394        Some(v) => v,
1395        None => return Ok(None),
1396    };
1397    let polygon_indices = match polygon_indices {
1398        Some(p) => p,
1399        None => return Ok(None),
1400    };
1401
1402    let control_points = vertices
1403        .chunks_exact(3)
1404        .map(|value| [value[0] as f32, value[1] as f32, value[2] as f32])
1405        .collect::<Vec<_>>();
1406
1407    // Track the polygon each fan triangle came from, so `ByPolygon`
1408    // material indices can be remapped onto triangle order.
1409    let mut tri_polygon_index: Vec<usize> = Vec::new();
1410    let mut polygon_count = 0usize;
1411    let mut corners_in_polygon = 0usize;
1412    for &idx in &polygon_indices {
1413        corners_in_polygon += 1;
1414        if idx < 0 {
1415            for _ in 0..corners_in_polygon.saturating_sub(2) {
1416                tri_polygon_index.push(polygon_count);
1417            }
1418            corners_in_polygon = 0;
1419            polygon_count += 1;
1420        }
1421    }
1422
1423    // Per-triangle material indices.
1424    let material_indices = raw
1425        .material
1426        .and_then(|layer| {
1427            let mapping = layer_string(layer, "MappingInformationType");
1428            let reference = layer_string(layer, "ReferenceInformationType");
1429            let data = layer_int_array(layer, "Materials");
1430            expand_material_indices(
1431                mapping.as_deref(),
1432                reference.as_deref(),
1433                data.as_deref(),
1434                polygon_count,
1435                &tri_polygon_index,
1436            )
1437        })
1438        .unwrap_or_default();
1439
1440    let domains = LayerDomains {
1441        edges: (!edges.is_empty()).then_some(edges.len()),
1442        polygons: polygon_count,
1443        control_points: control_points.len(),
1444    };
1445    let layers = parse_geometry_layers(raw, domains, warnings);
1446
1447    // Build the Draco mesh on the polygon-corner domain. Resolving layer
1448    // elements onto control points cannot represent a UV or hard-normal
1449    // seam, and silently averaged them away.
1450    let render = crate::fbx_render_mesh::expand_to_render_mesh(
1451        crate::fbx_render_mesh::FbxGeometryLayers::new(&control_points, &polygon_indices, &layers),
1452    );
1453    let mesh = crate::fbx_render_mesh::build_draco_mesh(&render);
1454
1455    Ok(Some(FbxGeometrySource {
1456        mesh,
1457        material_indices,
1458        control_points,
1459        polygon_vertex_indices: polygon_indices,
1460        layers,
1461        edges,
1462    }))
1463}
1464
1465/// Decodes each layer-element family into the form the scene retains.
1466fn parse_geometry_layers(
1467    raw: RawLayerNodes<'_>,
1468    domains: LayerDomains,
1469    warnings: &mut Vec<FbxWarning>,
1470) -> FbxMeshLayers {
1471    let uv_sets: Vec<FbxUvSet> = raw
1472        .uvs
1473        .into_iter()
1474        .filter_map(|layer| {
1475            let values = chunk_layer_values(&read_layer_floats(layer, "UV")?);
1476            Some(layer_set(layer, values, &["UVIndex"]))
1477        })
1478        .collect();
1479    let normal_sets: Vec<FbxNormalSet> = raw
1480        .normals
1481        .into_iter()
1482        .filter_map(|layer| {
1483            let values = chunk_layer_values(&read_layer_floats(layer, "Normals")?);
1484            // Exporters disagree on the index node's name.
1485            Some(layer_set(layer, values, &["NormalsIndex", "NormalIndex"]))
1486        })
1487        .collect();
1488    for set in &uv_sets {
1489        warn_unsupported_layer_mapping("LayerElementUV", set, warnings);
1490    }
1491    for set in &normal_sets {
1492        warn_unsupported_layer_mapping("LayerElementNormal", set, warnings);
1493    }
1494    let color_sets: Vec<FbxColorSet> = raw
1495        .colors
1496        .into_iter()
1497        .filter_map(|layer| {
1498            let floats = read_layer_floats(layer, "Colors")?;
1499            // FBX writes RGBA here, but a three-component source is legal
1500            // in the wild; pad it opaque rather than dropping the layer.
1501            let values = if floats.len() % 4 == 0 {
1502                chunk_layer_values(&floats)
1503            } else {
1504                floats
1505                    .chunks_exact(3)
1506                    .map(|value| [value[0], value[1], value[2], 1.0])
1507                    .collect()
1508            };
1509            Some(layer_set(layer, values, &["ColorIndex"]))
1510        })
1511        .collect();
1512    for set in &color_sets {
1513        warn_unsupported_layer_mapping("LayerElementColor", set, warnings);
1514    }
1515    let tangent_sets: Vec<FbxTangentSet> = raw
1516        .tangents
1517        .into_iter()
1518        .filter_map(|layer| parse_tangent_like(layer, "Tangents", "TangentsW", "TangentIndex"))
1519        .collect();
1520    let binormal_sets: Vec<FbxBinormalSet> = raw
1521        .binormals
1522        .into_iter()
1523        .filter_map(|layer| parse_tangent_like(layer, "Binormals", "BinormalsW", "BinormalIndex"))
1524        .collect();
1525    for set in &tangent_sets {
1526        warn_unsupported_layer_mapping("LayerElementTangent", &set.layer, warnings);
1527    }
1528    for set in &binormal_sets {
1529        warn_unsupported_layer_mapping("LayerElementBinormal", &set.layer, warnings);
1530    }
1531
1532    // Smoothing and crease layers address edges, polygons or control points --
1533    // never polygon corners -- so they are kept raw beside `edges` rather than
1534    // resolved onto the render mesh. A layer whose length disagrees with the
1535    // domain its mapping names is misaligned data, and keeping it would
1536    // silently sharpen the wrong edges.
1537    let mut smoothing_layers = Vec::new();
1538    for layer in raw.smoothing {
1539        let mapping = layer_string(layer, "MappingInformationType");
1540        let Some(values) = layer_int_array(layer, "Smoothing") else {
1541            continue;
1542        };
1543        if domains.check(mapping.as_deref()).accepts(values.len()) {
1544            smoothing_layers.push(FbxSmoothingLayer { mapping, values });
1545        } else {
1546            warn_misaligned_layer(
1547                "LayerElementSmoothing",
1548                mapping.as_deref(),
1549                values.len(),
1550                warnings,
1551            );
1552        }
1553    }
1554    let mut crease_layers = Vec::new();
1555    for (kind, layer) in raw.creases {
1556        let element = match kind {
1557            FbxCreaseKind::Edge => "LayerElementEdgeCrease",
1558            FbxCreaseKind::Vertex => "LayerElementVertexCrease",
1559        };
1560        let mapping = layer_string(layer, "MappingInformationType");
1561        let Some(values) = layer_f64_array(layer, element.trim_start_matches("LayerElement"))
1562        else {
1563            continue;
1564        };
1565        match domains.check(mapping.as_deref()) {
1566            domain if domain.accepts(values.len()) => {
1567                crease_layers.push(FbxCreaseLayer {
1568                    kind,
1569                    mapping,
1570                    values,
1571                });
1572            }
1573            _ => warn_misaligned_layer(element, mapping.as_deref(), values.len(), warnings),
1574        }
1575    }
1576
1577    FbxMeshLayers {
1578        uv_sets,
1579        normal_sets,
1580        color_sets,
1581        tangent_sets,
1582        binormal_sets,
1583        smoothing_layers,
1584        crease_layers,
1585    }
1586}
1587
1588impl<R: Read + Seek> FbxReader<R> {
1589    /// Flatten the FBX animation graph into one [`FbxAnimation`] per
1590    /// `AnimationStack` + first connected `AnimationLayer`.
1591    fn parse_animations(
1592        &self,
1593        nodes: &[FbxNode],
1594        index: &FbxObjectIndex<'_>,
1595        model_name_map: &HashMap<i64, String>,
1596        model_node_ids: &HashMap<i64, FbxNodeId>,
1597        morph_targets: &HashMap<i64, (i64, u32)>,
1598    ) -> Vec<FbxAnimation> {
1599        let FbxObjectIndex {
1600            connections,
1601            astack_map,
1602            alayer_map,
1603            acnode_map,
1604            acurve_map,
1605            model_map,
1606            ..
1607        } = index;
1608        let fbx_ktime = fbx_ktime_for(nodes, self.version());
1609        // Held as `f64`, and divided as `f64`, even though the sampler stores
1610        // seconds as `f32`. A tick count is around 2e10 for a one-second key,
1611        // where one `f32` step is 2048 ticks: narrowing either the count or
1612        // the divisor before the division quantizes the result to about
1613        // 4e-8 s, far coarser than the `f32` seconds can hold. Narrowing after
1614        // it costs nothing.
1615        let ktime_f = match fbx_ktime {
1616            0 => 1.0,
1617            v => v as f64,
1618        };
1619
1620        // acnode_id -> (layer_id, model_id, path). The FBX convention (and
1621        // Blender's io_scene_fbx) wires the AnimationCurveNode as the *child*
1622        // of an OP connection whose parent is the animated Model, with the
1623        // animated property name ("Lcl Translation" etc.) as the 4th field.
1624        let mut acnode_targets: std::collections::HashMap<
1625            i64,
1626            (i64, i64, FbxAnimChannelPath, Option<u32>),
1627        > = std::collections::HashMap::new();
1628        for conn in connections {
1629            if conn.kind != ConnectionKind::Op {
1630                continue;
1631            }
1632            if !acnode_map.contains_key(&conn.child) {
1633                continue;
1634            }
1635            let Some(property) = conn.property.as_deref() else {
1636                continue;
1637            };
1638            let Some(path) = FbxAnimChannelPath::from_property_name(property) else {
1639                continue;
1640            };
1641            let (model_id, morph_target_index) = if model_map.contains_key(&conn.parent) {
1642                (conn.parent, None)
1643            } else if path == FbxAnimChannelPath::MorphWeight {
1644                let Some(&(model_id, target_index)) = morph_targets.get(&conn.parent) else {
1645                    continue;
1646                };
1647                (model_id, Some(target_index))
1648            } else {
1649                continue;
1650            };
1651            // Find the layer that owns this curve node (OO curvenode -> layer).
1652            let mut layer_id = None;
1653            for c2 in connections {
1654                if c2.kind == ConnectionKind::Oo
1655                    && c2.child == conn.child
1656                    && alayer_map.contains_key(&c2.parent)
1657                {
1658                    layer_id = Some(c2.parent);
1659                    break;
1660                }
1661            }
1662            if let Some(layer_id) = layer_id {
1663                acnode_targets.insert(conn.child, (layer_id, model_id, path, morph_target_index));
1664            }
1665        }
1666
1667        // acnode_id -> { component -> (times, values, flags) }
1668        let mut acnode_curves: std::collections::HashMap<
1669            i64,
1670            std::collections::BTreeMap<u32, FbxAnimCurveData>,
1671        > = std::collections::HashMap::new();
1672        for conn in connections {
1673            if conn.kind != ConnectionKind::Op {
1674                continue;
1675            }
1676            if !acurve_map.contains_key(&conn.child) {
1677                continue;
1678            }
1679            if !acnode_targets.contains_key(&conn.parent) {
1680                continue;
1681            }
1682            let component = match conn.property.as_deref() {
1683                Some("d|X") => 0,
1684                Some("d|Y") => 1,
1685                Some("d|Z") => 2,
1686                _ => continue,
1687            };
1688            if let Some(curve) = parse_curve(acurve_map[&conn.child]) {
1689                acnode_curves
1690                    .entry(conn.parent)
1691                    .or_default()
1692                    .insert(component, curve);
1693            }
1694        }
1695
1696        // Group curve nodes by (stack, layer, model, path).
1697        //
1698        // Every iteration below walks ids in sorted order rather than hash
1699        // order. FBX object ids are stable within a document, so this makes
1700        // the channel list a property of the file instead of the process --
1701        // otherwise two reads of the same bytes produce differently ordered
1702        // channels and any positional comparison comes out garbage.
1703        let mut stacks_layers: StacksLayers = std::collections::HashMap::new();
1704        let mut acnode_ids_sorted: Vec<i64> = acnode_targets.keys().copied().collect();
1705        acnode_ids_sorted.sort_unstable();
1706        for acnode_id in &acnode_ids_sorted {
1707            let (layer_id, model_id, path, morph_target_index) = &acnode_targets[acnode_id];
1708            // Find stacks owning this layer.
1709            let mut stack_ids = Vec::new();
1710            for c2 in connections {
1711                if c2.kind == ConnectionKind::Oo
1712                    && c2.child == *layer_id
1713                    && astack_map.contains_key(&c2.parent)
1714                {
1715                    stack_ids.push(c2.parent);
1716                }
1717            }
1718            for stack_id in stack_ids {
1719                stacks_layers
1720                    .entry(stack_id)
1721                    .or_default()
1722                    .entry(*layer_id)
1723                    .or_default()
1724                    .push((*acnode_id, *model_id, *path, *morph_target_index));
1725            }
1726        }
1727
1728        let mut animations = Vec::new();
1729        let mut stack_ids_sorted: Vec<i64> = stacks_layers.keys().copied().collect();
1730        stack_ids_sorted.sort_unstable();
1731        for stack_id in stack_ids_sorted {
1732            let layers = &stacks_layers[&stack_id];
1733            let stack_node = astack_map.get(&stack_id);
1734            let name = stack_node.and_then(|n| match n.properties.get(1) {
1735                Some(FbxProperty::String(raw)) => raw
1736                    .split('\0')
1737                    .next()
1738                    .filter(|s| !s.is_empty())
1739                    .map(str::to_string),
1740                _ => None,
1741            });
1742            // One clip per layer, which is what Blender's importer does: it
1743            // "does not mix layers, each layer results in an independent set
1744            // of actions". Merging them instead produced several channels
1745            // driving the same node and path, and any consumer applying them
1746            // in order silently kept only the last.
1747            let mut layer_ids_sorted: Vec<i64> = layers.keys().copied().collect();
1748            layer_ids_sorted.sort_unstable();
1749            let multiple_layers = layer_ids_sorted.len() > 1;
1750            for (layer_index, layer_id) in layer_ids_sorted.iter().copied().enumerate() {
1751                let mut channels = Vec::new();
1752                let mut max_time = 0.0f32;
1753                let entries = &layers[&layer_id];
1754                // Group curve nodes by (model, path) before flattening.
1755                let mut groups: std::collections::HashMap<
1756                    (i64, FbxAnimChannelPath, Option<u32>),
1757                    Vec<i64>,
1758                > = std::collections::HashMap::new();
1759                for &(acnode_id, model_id, path, morph_target_index) in entries {
1760                    groups
1761                        .entry((model_id, path, morph_target_index))
1762                        .or_default()
1763                        .push(acnode_id);
1764                }
1765                let mut group_keys: Vec<(i64, FbxAnimChannelPath, Option<u32>)> =
1766                    groups.keys().copied().collect();
1767                group_keys.sort_unstable_by_key(|(model_id, path, morph_target_index)| {
1768                    (*model_id, *path as u8, *morph_target_index)
1769                });
1770                for (model_id, path, morph_target_index) in group_keys {
1771                    let acnode_ids = &groups[&(model_id, path, morph_target_index)];
1772                    // Combine the X/Y/Z curves across all matching curve nodes
1773                    // (Blender notes that each curve node has a unique set of
1774                    // channels, so in practice there is exactly one entry).
1775                    let mut by_component: std::collections::BTreeMap<u32, FbxAnimCurveData> =
1776                        std::collections::BTreeMap::new();
1777                    for acnode_id in acnode_ids {
1778                        if let Some(curves) = acnode_curves.get(acnode_id) {
1779                            for (component, curve) in curves {
1780                                by_component
1781                                    .entry(*component)
1782                                    .or_insert_with(|| curve.clone());
1783                            }
1784                        }
1785                    }
1786                    let Some(channel) = flatten_curve(&by_component, path, ktime_f) else {
1787                        continue;
1788                    };
1789                    if let (Some(node_name), Some(&node_id)) =
1790                        (model_name_map.get(&model_id), model_node_ids.get(&model_id))
1791                    {
1792                        max_time =
1793                            max_time.max(channel.sampler.input.last().copied().unwrap_or(0.0));
1794                        channels.push(FbxAnimChannel {
1795                            node_id,
1796                            node_name: node_name.clone(),
1797                            path,
1798                            morph_target_index,
1799                            sampler: channel.sampler,
1800                        });
1801                    }
1802                }
1803                if channels.is_empty() {
1804                    continue;
1805                }
1806                // Name extra layers so they stay distinguishable; a
1807                // single-layer stack keeps the stack name unchanged.
1808                let clip_name = if multiple_layers {
1809                    let layer_name = alayer_map
1810                        .get(&layer_id)
1811                        .and_then(|node| match node.properties.get(1) {
1812                            Some(FbxProperty::String(raw)) => raw
1813                                .split('\0')
1814                                .next()
1815                                .filter(|part| !part.is_empty())
1816                                .map(str::to_string),
1817                            _ => None,
1818                        })
1819                        .unwrap_or_else(|| format!("Layer{layer_index}"));
1820                    Some(match &name {
1821                        Some(stack) => format!("{stack}|{layer_name}"),
1822                        None => layer_name,
1823                    })
1824                } else {
1825                    name.clone()
1826                };
1827                animations.push(FbxAnimation {
1828                    name: clip_name,
1829                    duration: max_time,
1830                    channels,
1831                });
1832            }
1833        }
1834        animations
1835    }
1836}
1837
1838/// Curve nodes grouped by `AnimationStack` id, then by `AnimationLayer` id.
1839///
1840/// Each entry is `(curve_node_id, model_id, path, morph_target_index)`.
1841type StacksLayers = std::collections::HashMap<
1842    i64,
1843    std::collections::HashMap<i64, Vec<(i64, i64, FbxAnimChannelPath, Option<u32>)>>,
1844>;
1845
1846#[derive(Debug, Clone)]
1847struct FbxAnimCurveData {
1848    key_times: Vec<i64>,
1849    key_values: Vec<f32>,
1850    key_attr_flags: Vec<i32>,
1851    in_tangents: Vec<f32>,
1852    out_tangents: Vec<f32>,
1853}
1854
1855fn parse_curve(node: &FbxNode) -> Option<FbxAnimCurveData> {
1856    let mut key_times = None;
1857    let mut key_values = None;
1858    let mut key_attr_flags = None;
1859    let mut key_attr_data = None;
1860    let mut key_attr_ref_count = None;
1861    for child in &node.children {
1862        match child.name.as_str() {
1863            "KeyTime" => {
1864                if let Some(FbxProperty::I64Array(arr)) = child.properties.first() {
1865                    key_times = Some(arr.clone());
1866                }
1867            }
1868            "KeyValueFloat" => {
1869                key_values = child.properties.first().and_then(float_array);
1870            }
1871            "KeyAttrFlags" => {
1872                if let Some(FbxProperty::I32Array(arr)) = child.properties.first() {
1873                    key_attr_flags = Some(arr.clone());
1874                }
1875            }
1876            "KeyAttrDataFloat" => {
1877                key_attr_data = child.properties.first().and_then(float_array);
1878            }
1879            "KeyAttrRefCount" => {
1880                if let Some(FbxProperty::I32Array(arr)) = child.properties.first() {
1881                    key_attr_ref_count = Some(arr.clone());
1882                }
1883            }
1884            _ => {}
1885        }
1886    }
1887    let key_times = key_times?;
1888    let key_values = key_values?;
1889    if key_times.is_empty() || key_values.len() != key_times.len() {
1890        return None;
1891    }
1892    let mut expanded_flags = Vec::with_capacity(key_times.len());
1893    let mut expanded_attrs = Vec::with_capacity(key_times.len());
1894    if let (Some(flags), Some(data), Some(refs)) =
1895        (key_attr_flags, key_attr_data, key_attr_ref_count)
1896    {
1897        if flags.len() == refs.len() && data.len() == refs.len() * 4 {
1898            for ((flag, count), attrs) in flags.into_iter().zip(refs).zip(data.chunks_exact(4)) {
1899                for _ in 0..count.max(0) {
1900                    expanded_flags.push(flag);
1901                    expanded_attrs.push([attrs[0], attrs[1]]);
1902                }
1903            }
1904        }
1905    }
1906    if expanded_flags.len() != key_times.len() {
1907        expanded_flags = vec![0x4; key_times.len()];
1908        expanded_attrs = vec![[0.0, 0.0]; key_times.len()];
1909    }
1910    let mut in_tangents = vec![0.0; key_times.len()];
1911    let mut out_tangents = vec![0.0; key_times.len()];
1912    for (index, attrs) in expanded_attrs.iter().enumerate() {
1913        out_tangents[index] = attrs[0];
1914        if index + 1 < in_tangents.len() {
1915            in_tangents[index + 1] = attrs[1];
1916        }
1917    }
1918    Some(FbxAnimCurveData {
1919        key_times,
1920        key_values,
1921        key_attr_flags: expanded_flags,
1922        in_tangents,
1923        out_tangents,
1924    })
1925}
1926
1927/// Combine per-component curves into a single TRS channel sampler.
1928///
1929/// Times are taken from the X (component 0) curve when available, then Y, then
1930/// Z. Missing components default to 0. Interpolation is read from the first
1931/// `KeyAttrFlags` entry of the chosen time axis.
1932fn flatten_curve(
1933    by_component: &std::collections::BTreeMap<u32, FbxAnimCurveData>,
1934    path: FbxAnimChannelPath,
1935    ktime_f: f64,
1936) -> Option<FbxAnimChannel> {
1937    let time_axis = by_component
1938        .get(&0)
1939        .or_else(|| by_component.get(&1))
1940        .or_else(|| by_component.get(&2))?;
1941    let n = time_axis.key_times.len();
1942    let mut input = Vec::with_capacity(n);
1943    let component_count = path.component_count();
1944    let mut output = Vec::with_capacity(n * component_count);
1945    let mut in_tangents = Vec::with_capacity(n * component_count);
1946    let mut out_tangents = Vec::with_capacity(n * component_count);
1947    let flags = time_axis.key_attr_flags.first().copied().unwrap_or(0);
1948    let interpolation = FbxAnimInterpolation::from_key_attr_flags(flags);
1949    for i in 0..n {
1950        input.push((time_axis.key_times[i] as f64 / ktime_f) as f32);
1951        for component in 0..component_count as u32 {
1952            let value = by_component.get(&component).and_then(|curve| {
1953                if i < curve.key_values.len() {
1954                    Some(curve.key_values[i])
1955                } else {
1956                    None
1957                }
1958            });
1959            output.push(value.unwrap_or(0.0));
1960            in_tangents.push(
1961                by_component
1962                    .get(&component)
1963                    .and_then(|curve| curve.in_tangents.get(i))
1964                    .copied()
1965                    .unwrap_or(0.0),
1966            );
1967            out_tangents.push(
1968                by_component
1969                    .get(&component)
1970                    .and_then(|curve| curve.out_tangents.get(i))
1971                    .copied()
1972                    .unwrap_or(0.0),
1973            );
1974        }
1975    }
1976    // FBX stores Euler rotations in degrees; convert to radians so the JS
1977    // viewer's Euler→quaternion helper matches expectations. Translation and
1978    // scale are passed through unchanged.
1979    //
1980    // Through `f64`, and narrowing once at the end. `f32::to_radians` rounds
1981    // its own factor and then rounds the product, so composing it with the
1982    // writer's inverse moved an angle by a bit on every rewrite.
1983    let radians = |value: f32| f64::from(value).to_radians() as f32;
1984    if path == FbxAnimChannelPath::Rotation {
1985        for chunk in output.chunks_mut(3) {
1986            for value in chunk.iter_mut() {
1987                *value = radians(*value);
1988            }
1989        }
1990        for chunk in in_tangents.chunks_mut(3) {
1991            for value in chunk.iter_mut() {
1992                *value = radians(*value);
1993            }
1994        }
1995        for chunk in out_tangents.chunks_mut(3) {
1996            for value in chunk.iter_mut() {
1997                *value = radians(*value);
1998            }
1999        }
2000    }
2001    Some(FbxAnimChannel {
2002        node_id: FbxNodeId(0),
2003        node_name: String::new(),
2004        path,
2005        morph_target_index: None,
2006        sampler: FbxAnimSampler {
2007            input,
2008            output,
2009            interpolation,
2010            in_tangents: (interpolation == FbxAnimInterpolation::Cubic).then_some(in_tangents),
2011            out_tangents: (interpolation == FbxAnimInterpolation::Cubic).then_some(out_tangents),
2012        },
2013    })
2014}
2015
2016/// Determine the FBX KTime ticks-per-second value.
2017///
2018/// Pre-7.7 files use `46186158000`. FBX 2019.5+ (version 7700+) introduced an
2019/// opt-in `141120000` ticks/second default; the legacy value is selected by
2020/// `FBXHeaderExtension/OtherFlags/TCDefinition == 127`. See Blender's
2021/// `io_scene_fbx` `FBX_KTIME` constants for the canonical encoding.
2022fn fbx_ktime_for(nodes: &[FbxNode], version: u32) -> u64 {
2023    const KTIME_V7: u64 = 46_186_158_000;
2024    const KTIME_V8: u64 = 141_120_000;
2025    if version >= 8000 {
2026        return KTIME_V8;
2027    }
2028    if version >= 7700 {
2029        // Inspect OtherFlags/TCDefinition. 127 selects the legacy V7 value;
2030        // anything else (or missing) opts into V8.
2031        for n in nodes {
2032            if n.name != "FBXHeaderExtension" {
2033                continue;
2034            }
2035            let mut header_version = 0;
2036            let mut other_flags: Option<&FbxNode> = None;
2037            for child in &n.children {
2038                if child.name == "FBXHeaderVersion" {
2039                    if let Some(FbxProperty::I32(v)) = child.properties.first() {
2040                        header_version = *v;
2041                    }
2042                } else if child.name == "OtherFlags" && other_flags.is_none() {
2043                    other_flags = Some(child);
2044                }
2045            }
2046            if header_version >= 1004 {
2047                if let Some(flags) = other_flags {
2048                    for flag in &flags.children {
2049                        if flag.name == "TCDefinition" {
2050                            if let Some(FbxProperty::I32(v)) = flag.properties.first() {
2051                                return if *v == 127 { KTIME_V7 } else { KTIME_V8 };
2052                            }
2053                        }
2054                    }
2055                }
2056            }
2057        }
2058        // Pre-8000 default for 7.7+ files without an explicit TCDefinition is V7.
2059        return KTIME_V7;
2060    }
2061    KTIME_V7
2062}
2063
2064fn layer_string(layer: &FbxNode, name: &str) -> Option<String> {
2065    for child in &layer.children {
2066        if child.name == name {
2067            if let Some(FbxProperty::String(s)) = child.properties.first() {
2068                return Some(s.clone());
2069            }
2070        }
2071    }
2072    None
2073}
2074
2075fn layer_int_array(layer: &FbxNode, name: &str) -> Option<Vec<i32>> {
2076    for child in &layer.children {
2077        if child.name == name {
2078            if let Some(FbxProperty::I32Array(arr)) = child.properties.first() {
2079                return Some(arr.clone());
2080            }
2081        }
2082    }
2083    None
2084}
2085
2086/// Reports a layer element whose mapping or reference mode this crate does not
2087/// recognize.
2088///
2089/// The value is still resolved on the control-point domain, which is the most
2090/// likely intent and what the reader has always done. The warning exists so a
2091/// caller learns the substitution happened rather than inferring it from
2092/// unexpected geometry.
2093fn warn_unsupported_layer_mapping<const N: usize>(
2094    element: &str,
2095    set: &FbxLayerSet<N>,
2096    warnings: &mut Vec<FbxWarning>,
2097) {
2098    const KNOWN_MAPPINGS: [&str; 7] = [
2099        "ByPolygonVertex",
2100        "ByPolygon",
2101        "ByVertice",
2102        "ByVertex",
2103        "ByControlPoint",
2104        "AllSame",
2105        "AllSameOrPolygon",
2106    ];
2107    if let Some(mapping) = set.mapping.as_deref() {
2108        if !KNOWN_MAPPINGS.contains(&mapping) {
2109            let subject = format!("{element}/{mapping}");
2110            push_warning(
2111                warnings,
2112                FbxWarningCode::UnsupportedLayerMapping,
2113                format!(
2114                    "FBX {element} uses mapping {mapping}, which was resolved on the \
2115                     control-point domain"
2116                ),
2117                Some(&subject),
2118            );
2119        }
2120    }
2121    if let Some(reference) = set.reference.as_deref() {
2122        if reference != "Direct" && reference != "IndexToDirect" {
2123            let subject = format!("{element}/{reference}");
2124            push_warning(
2125                warnings,
2126                FbxWarningCode::UnsupportedLayerMapping,
2127                format!("FBX {element} uses reference mode {reference}, which was read as Direct"),
2128                Some(&subject),
2129            );
2130        }
2131    }
2132}
2133
2134/// What can be said about the length a non-corner layer element should have.
2135#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2136enum DomainCheck {
2137    /// The domain has a known size, and the layer must match it exactly.
2138    Expect(usize),
2139    /// The domain exists but its size is not known here, so the layer is kept
2140    /// as authored rather than judged.
2141    Unverifiable,
2142    /// The mapping names no domain this crate recognizes.
2143    Unknown,
2144}
2145
2146impl DomainCheck {
2147    fn accepts(self, len: usize) -> bool {
2148        match self {
2149            DomainCheck::Expect(expected) => expected == len,
2150            DomainCheck::Unverifiable => true,
2151            DomainCheck::Unknown => false,
2152        }
2153    }
2154}
2155
2156/// Reports a smoothing or crease layer whose length disagrees with the domain
2157/// its mapping names, and which was therefore dropped.
2158fn warn_misaligned_layer(
2159    element: &str,
2160    mapping: Option<&str>,
2161    len: usize,
2162    warnings: &mut Vec<FbxWarning>,
2163) {
2164    let mapping = mapping.unwrap_or("no mapping");
2165    let subject = format!("{element}/{mapping}");
2166    push_warning(
2167        warnings,
2168        FbxWarningCode::UnsupportedLayerMapping,
2169        format!(
2170            "FBX {element} has {len} values, which does not match the domain \
2171             {mapping} addresses, so the layer was dropped"
2172        ),
2173        Some(&subject),
2174    );
2175}
2176
2177/// Reads a layer element's `f64` payload, for the crease weights that are not
2178/// vectors and so do not go through [`chunk_layer_values`].
2179fn layer_f64_array(layer: &FbxNode, name: &str) -> Option<Vec<f64>> {
2180    for child in &layer.children {
2181        if child.name == name {
2182            return match child.properties.first() {
2183                Some(FbxProperty::F64Array(arr)) => Some(arr.clone()),
2184                Some(FbxProperty::F32Array(arr)) => {
2185                    Some(arr.iter().map(|v| f64::from(*v)).collect())
2186                }
2187                _ => None,
2188            };
2189        }
2190    }
2191    None
2192}
2193
2194/// Reads a `LayerElementTangent` or `LayerElementBinormal`.
2195///
2196/// The handedness sign is a separate sibling array present only from FBX 7500
2197/// on; when it is missing, or disagrees with the vector count, `w` defaults to
2198/// `+1.0` and the set records that it was synthesized.
2199fn parse_tangent_like(
2200    layer: &FbxNode,
2201    values_node: &str,
2202    handedness_node: &str,
2203    index_node: &str,
2204) -> Option<FbxTangentSet> {
2205    let vectors: Vec<[f32; 3]> = chunk_layer_values(&read_layer_floats(layer, values_node)?);
2206    let handedness = read_layer_floats(layer, handedness_node)
2207        .filter(|signs| signs.len() == vectors.len())
2208        .unwrap_or_default();
2209    let has_handedness = !handedness.is_empty();
2210    let values = vectors
2211        .iter()
2212        .enumerate()
2213        .map(|(index, v)| {
2214            let sign = handedness.get(index).copied().unwrap_or(1.0);
2215            [v[0], v[1], v[2], sign]
2216        })
2217        .collect();
2218    Some(FbxTangentSet {
2219        layer: layer_set(layer, values, &[index_node]),
2220        has_handedness,
2221    })
2222}
2223
2224/// Reads the parts every float layer element shares.
2225///
2226/// `index_nodes` lists the names the index array may appear under, tried in
2227/// order: exporters disagree on some of them.
2228fn layer_set<const N: usize>(
2229    layer: &FbxNode,
2230    values: Vec<[f32; N]>,
2231    index_nodes: &[&str],
2232) -> FbxLayerSet<N> {
2233    FbxLayerSet {
2234        name: layer_string(layer, "Name"),
2235        mapping: layer_string(layer, "MappingInformationType"),
2236        reference: layer_string(layer, "ReferenceInformationType"),
2237        values,
2238        indices: index_nodes
2239            .iter()
2240            .find_map(|name| layer_int_array(layer, name))
2241            .unwrap_or_default(),
2242    }
2243}
2244
2245/// Groups a flat float payload into `N`-component values, dropping a trailing
2246/// partial value.
2247fn chunk_layer_values<const N: usize>(raw: &[f32]) -> Vec<[f32; N]> {
2248    raw.chunks_exact(N)
2249        .map(|value| std::array::from_fn(|i| value[i]))
2250        .collect()
2251}
2252
2253/// Reads a layer element's flat float payload, whatever its component count.
2254///
2255/// FBX writes these as `f64` arrays; some exporters use `f32`.
2256fn read_layer_floats(layer: &FbxNode, name: &str) -> Option<Vec<f32>> {
2257    for child in &layer.children {
2258        if child.name == name {
2259            if let Some(FbxProperty::F64Array(arr)) = child.properties.first() {
2260                return Some(arr.iter().map(|v| *v as f32).collect());
2261            }
2262            if let Some(FbxProperty::F32Array(arr)) = child.properties.first() {
2263                return Some(arr.clone());
2264            }
2265        }
2266    }
2267    None
2268}
2269
2270/// Expand a `LayerElementMaterial` data array to per-triangle material indices.
2271fn expand_material_indices(
2272    mapping: Option<&str>,
2273    reference: Option<&str>,
2274    data: Option<&[i32]>,
2275    polygon_count: usize,
2276    tri_polygon_index: &[usize],
2277) -> Option<Vec<i32>> {
2278    let mapping = mapping.unwrap_or("AllSame");
2279    let data = data?;
2280    // `IndexToDirect` semantics: each entry of `Materials` is itself the
2281    // absolute material index (FBX rarely uses a separate index array for
2282    // materials, but we honour `IndexToDirect` by treating `data` as the
2283    // direct list when no separate index exists).
2284    let _ = reference;
2285    let per_polygon: Vec<i32> = match mapping {
2286        "AllSame" => {
2287            let value = data.first().copied().unwrap_or(0);
2288            vec![value; polygon_count.max(1)]
2289        }
2290        "ByPolygon" | "ByPolygonSide" => data.to_vec(),
2291        "ByPolygonVertex" => {
2292            // We do not retain per-vertex polygon order here; pick the first
2293            // vertex entry of each polygon. The caller passes
2294            // `tri_polygon_index` keyed by polygon index.
2295            // Without polygon-vertex correspondence we fall back to AllSame.
2296            let value = data.first().copied().unwrap_or(0);
2297            vec![value; polygon_count.max(1)]
2298        }
2299        _ => return None,
2300    };
2301    if per_polygon.is_empty() {
2302        return Some(Vec::new());
2303    }
2304    let mut out = Vec::with_capacity(tri_polygon_index.len());
2305    for &polygon_index in tri_polygon_index {
2306        let value = per_polygon
2307            .get(polygon_index)
2308            .copied()
2309            .unwrap_or(per_polygon[0]);
2310        out.push(value);
2311    }
2312    Some(out)
2313}