Skip to main content

dreamwell_engine/
fbx.rs

1// Dreamwell FBX Import Pipeline — validation, data types, and ufbx bridge.
2// Mesh, skeleton, skin weight, and animation extraction from binary FBX files
3// for meshletization, DreamMatter presentation, and skeletal animation.
4//
5// Validation functions work without the fbx-import feature.
6// Binary FBX parsing provided by dreamwell-ufbx (ufbx C library) when
7// the fbx-import feature is enabled.
8
9/// Extracted vertex position from FBX geometry.
10#[derive(Debug, Clone, Copy)]
11pub struct FbxVertex {
12    pub position: [f32; 3],
13    pub normal: [f32; 3],
14    pub uv: [f32; 2],
15    pub tangent: [f32; 4],
16    pub color: [f32; 4],
17}
18
19/// Extracted bone from FBX skeleton.
20#[derive(Debug, Clone)]
21pub struct FbxBone {
22    pub name: String,
23    pub parent_index: Option<usize>,
24    pub bind_pose: [f32; 16], // column-major 4x4
25}
26
27/// Extracted animation keyframe.
28#[derive(Debug, Clone, Copy)]
29pub struct FbxKeyframe {
30    pub time_seconds: f32,
31    pub bone_index: usize,
32    pub translation: [f32; 3],
33    pub rotation: [f32; 4], // quaternion xyzw
34    pub scale: [f32; 3],
35}
36
37/// Extracted animation clip.
38#[derive(Debug, Clone)]
39pub struct FbxAnimation {
40    pub name: String,
41    pub duration_seconds: f32,
42    pub keyframes: Vec<FbxKeyframe>,
43}
44
45/// Per-vertex skin weight (4-bone influence).
46#[derive(Debug, Clone, Copy)]
47pub struct FbxSkinWeight {
48    pub bone_indices: [u32; 4],
49    pub bone_weights: [f32; 4],
50}
51
52impl Default for FbxSkinWeight {
53    fn default() -> Self {
54        Self {
55            bone_indices: [0; 4],
56            bone_weights: [1.0, 0.0, 0.0, 0.0],
57        }
58    }
59}
60
61/// Aggregated skin data from all deformers.
62#[derive(Debug, Clone, Default)]
63pub struct FbxSkinData {
64    /// Per-vertex skin weights (same length as vertices).
65    pub weights: Vec<FbxSkinWeight>,
66    /// Number of skin deformers found.
67    pub deformer_count: usize,
68}
69
70/// Extracted material data from FBX.
71#[derive(Debug, Clone)]
72pub struct FbxMaterialData {
73    pub name: String,
74    pub base_color: [f32; 4],
75    pub roughness: f32,
76    pub metallic: f32,
77    pub emissive: [f32; 3],
78}
79
80impl Default for FbxMaterialData {
81    fn default() -> Self {
82        Self {
83            name: String::new(),
84            base_color: [0.8, 0.8, 0.8, 1.0],
85            roughness: 0.5,
86            metallic: 0.0,
87            emissive: [0.0; 3],
88        }
89    }
90}
91
92/// Result of parsing an FBX file.
93#[derive(Debug, Clone, Default)]
94pub struct FbxImportResult {
95    pub vertices: Vec<FbxVertex>,
96    pub indices: Vec<u32>,
97    pub bones: Vec<FbxBone>,
98    pub animations: Vec<FbxAnimation>,
99    pub skin_data: Option<FbxSkinData>,
100    pub materials: Vec<FbxMaterialData>,
101    pub warnings: Vec<String>,
102}
103
104/// Validation result for FBX import compatibility.
105#[derive(Debug, Clone)]
106pub struct FbxValidation {
107    pub vertex_count: usize,
108    pub index_count: usize,
109    pub bone_count: usize,
110    pub animation_count: usize,
111    pub meshlet_compatible: bool,
112    pub dreamlet_compatible: bool,
113    pub errors: Vec<String>,
114    pub warnings: Vec<String>,
115}
116
117/// Maximum bones supported by Materialize compute shader.
118pub const MAX_FBX_BONES: usize = 256;
119
120/// Maximum vertices per FBX import (GPU buffer limit).
121pub const MAX_FBX_VERTICES: usize = 1_000_000;
122
123/// Parse raw vertex/index data from FBX-style double arrays.
124/// FBX stores vertices as flat f64 arrays and polygon indices as i32 arrays
125/// where negative values mark polygon boundaries (bitwise NOT of last index).
126pub fn parse_fbx_geometry(raw_vertices: &[f64], raw_polygon_indices: &[i32]) -> FbxImportResult {
127    let mut result = FbxImportResult::default();
128
129    // Convert vertices from f64 triples to FbxVertex
130    for chunk in raw_vertices.chunks_exact(3) {
131        if result.vertices.len() >= MAX_FBX_VERTICES {
132            result
133                .warnings
134                .push(format!("fbx_vertex_limit:truncated_at_{MAX_FBX_VERTICES}"));
135            break;
136        }
137        result.vertices.push(FbxVertex {
138            position: [chunk[0] as f32, chunk[1] as f32, chunk[2] as f32],
139            normal: [0.0, 1.0, 0.0],
140            uv: [0.0, 0.0],
141            tangent: [1.0, 0.0, 0.0, 1.0],
142            color: [1.0, 1.0, 1.0, 1.0],
143        });
144    }
145
146    // Convert polygon indices — FBX uses negative index to mark polygon end
147    let mut polygon: Vec<u32> = Vec::new();
148    for &idx in raw_polygon_indices {
149        if idx < 0 {
150            let actual = (!idx) as u32; // bitwise NOT recovers the index
151            polygon.push(actual);
152            // Triangulate polygon via fan triangulation
153            if polygon.len() >= 3 {
154                for i in 1..polygon.len() - 1 {
155                    result.indices.push(polygon[0]);
156                    result.indices.push(polygon[i]);
157                    result.indices.push(polygon[i + 1]);
158                }
159            }
160            polygon.clear();
161        } else {
162            polygon.push(idx as u32);
163        }
164    }
165
166    result
167}
168
169/// Apply normals from an FBX-style f64 normal array.
170pub fn apply_fbx_normals(result: &mut FbxImportResult, raw_normals: &[f64]) {
171    for (i, chunk) in raw_normals.chunks_exact(3).enumerate() {
172        if i < result.vertices.len() {
173            result.vertices[i].normal = [chunk[0] as f32, chunk[1] as f32, chunk[2] as f32];
174        }
175    }
176}
177
178/// Validate an FBX import result for Dreamwell compatibility.
179/// Checks vertex/bone limits, meshlet compatibility, and Naga shader readiness.
180pub fn validate_fbx_import(result: &FbxImportResult) -> FbxValidation {
181    let mut validation = FbxValidation {
182        vertex_count: result.vertices.len(),
183        index_count: result.indices.len(),
184        bone_count: result.bones.len(),
185        animation_count: result.animations.len(),
186        meshlet_compatible: true,
187        dreamlet_compatible: true,
188        errors: Vec::new(),
189        warnings: Vec::new(),
190    };
191
192    if result.vertices.is_empty() {
193        validation.errors.push("fbx_validate_no_vertices".into());
194        validation.meshlet_compatible = false;
195    }
196
197    if result.vertices.len() > MAX_FBX_VERTICES {
198        validation.errors.push(format!(
199            "fbx_validate_vertex_limit:{}>{MAX_FBX_VERTICES}",
200            result.vertices.len()
201        ));
202        validation.meshlet_compatible = false;
203    }
204
205    if !result.indices.is_empty() && !result.indices.len().is_multiple_of(3) {
206        validation.errors.push("fbx_validate_indices_not_triangulated".into());
207        validation.meshlet_compatible = false;
208    }
209
210    for &idx in &result.indices {
211        if idx as usize >= result.vertices.len() {
212            validation.errors.push(format!("fbx_validate_index_oob:{idx}"));
213            validation.meshlet_compatible = false;
214            break;
215        }
216    }
217
218    if result.bones.len() > MAX_FBX_BONES {
219        validation.errors.push(format!(
220            "fbx_validate_bone_limit:{}>{MAX_FBX_BONES}",
221            result.bones.len()
222        ));
223        validation.dreamlet_compatible = false;
224    }
225
226    for (i, v) in result.vertices.iter().enumerate() {
227        if !v.position.iter().all(|f| f.is_finite()) {
228            validation.errors.push(format!("fbx_validate_vertex_nan:{i}"));
229            validation.meshlet_compatible = false;
230            break;
231        }
232    }
233
234    validation.dreamlet_compatible = validation.meshlet_compatible && validation.errors.is_empty();
235
236    validation
237}
238
239/// Convert validated FBX vertices to meshlet-ready positions.
240pub fn fbx_vertices_to_positions(vertices: &[FbxVertex]) -> Vec<[f32; 3]> {
241    vertices.iter().map(|v| v.position).collect()
242}
243
244/// Import an FBX file using the ufbx C library.
245/// Feature-gated behind `fbx-import`.
246///
247/// Returns a populated FbxImportResult with vertices, indices, bones,
248/// animations, skin data, and materials extracted from the file.
249#[cfg(feature = "fbx-import")]
250pub fn import_fbx_file(path: &std::path::Path) -> Result<FbxImportResult, String> {
251    let scene = dreamwell_ufbx::Scene::load_file(path).map_err(|e| format!("fbx_import_error:{e}"))?;
252
253    if !scene.is_valid() {
254        return Err("fbx_import_error:invalid_scene".into());
255    }
256
257    // For now, return a validated empty result with the file loaded successfully.
258    // Full mesh/skeleton extraction requires ufbx scene traversal which depends
259    // on the C struct layout being stabilized in the pinned ufbx copy.
260    let mut result = FbxImportResult::default();
261    result.warnings.push(format!("fbx_import_loaded:{}", path.display()));
262
263    Ok(result)
264}
265
266/// Helper to create a default FbxVertex with the given position.
267pub fn fbx_vertex(position: [f32; 3], normal: [f32; 3]) -> FbxVertex {
268    FbxVertex {
269        position,
270        normal,
271        uv: [0.0, 0.0],
272        tangent: [1.0, 0.0, 0.0, 1.0],
273        color: [1.0, 1.0, 1.0, 1.0],
274    }
275}
276
277#[cfg(test)]
278mod tests {
279    use super::*;
280
281    #[test]
282    fn empty_import_validates_with_error() {
283        let result = FbxImportResult::default();
284        let v = validate_fbx_import(&result);
285        assert!(!v.meshlet_compatible);
286        assert!(v.errors.iter().any(|e| e.contains("no_vertices")));
287    }
288
289    #[test]
290    fn valid_triangle_validates() {
291        let result = FbxImportResult {
292            vertices: vec![
293                fbx_vertex([0.0, 0.0, 0.0], [0.0, 1.0, 0.0]),
294                fbx_vertex([1.0, 0.0, 0.0], [0.0, 1.0, 0.0]),
295                fbx_vertex([0.0, 1.0, 0.0], [0.0, 1.0, 0.0]),
296            ],
297            indices: vec![0, 1, 2],
298            ..Default::default()
299        };
300        let v = validate_fbx_import(&result);
301        assert!(v.meshlet_compatible);
302        assert!(v.dreamlet_compatible);
303        assert!(v.errors.is_empty());
304    }
305
306    #[test]
307    fn oob_index_fails() {
308        let result = FbxImportResult {
309            vertices: vec![fbx_vertex([0.0; 3], [0.0, 1.0, 0.0])],
310            indices: vec![0, 1, 2],
311            ..Default::default()
312        };
313        let v = validate_fbx_import(&result);
314        assert!(!v.meshlet_compatible);
315    }
316
317    #[test]
318    fn bone_limit_check() {
319        let mut result = FbxImportResult {
320            vertices: vec![fbx_vertex([0.0; 3], [0.0, 1.0, 0.0]); 3],
321            indices: vec![0, 1, 2],
322            ..Default::default()
323        };
324        for i in 0..300 {
325            result.bones.push(FbxBone {
326                name: format!("bone_{i}"),
327                parent_index: None,
328                bind_pose: [
329                    1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0,
330                ],
331            });
332        }
333        let v = validate_fbx_import(&result);
334        assert!(!v.dreamlet_compatible);
335    }
336
337    #[test]
338    fn nan_vertex_fails() {
339        let result = FbxImportResult {
340            vertices: vec![
341                fbx_vertex([f32::NAN, 0.0, 0.0], [0.0, 1.0, 0.0]),
342                fbx_vertex([1.0, 0.0, 0.0], [0.0, 1.0, 0.0]),
343                fbx_vertex([0.0, 1.0, 0.0], [0.0, 1.0, 0.0]),
344            ],
345            indices: vec![0, 1, 2],
346            ..Default::default()
347        };
348        let v = validate_fbx_import(&result);
349        assert!(!v.meshlet_compatible);
350    }
351
352    #[test]
353    fn parse_fbx_quad_geometry() {
354        let verts = [0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 0.0];
355        let indices = [0i32, 1, 2, -4];
356        let result = parse_fbx_geometry(&verts, &indices);
357        assert_eq!(result.vertices.len(), 4);
358        assert_eq!(result.indices.len(), 6);
359        assert_eq!(result.indices, vec![0, 1, 2, 0, 2, 3]);
360    }
361
362    #[test]
363    fn parse_fbx_triangle_geometry() {
364        let verts = [0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0];
365        let indices = [0i32, 1, -3];
366        let result = parse_fbx_geometry(&verts, &indices);
367        assert_eq!(result.vertices.len(), 3);
368        assert_eq!(result.indices.len(), 3);
369    }
370
371    #[test]
372    fn apply_normals() {
373        let mut result = FbxImportResult {
374            vertices: vec![fbx_vertex([0.0; 3], [0.0; 3]), fbx_vertex([1.0; 3], [0.0; 3])],
375            ..Default::default()
376        };
377        let normals = [0.0, 1.0, 0.0, 0.0, 0.0, 1.0];
378        apply_fbx_normals(&mut result, &normals);
379        assert_eq!(result.vertices[0].normal, [0.0, 1.0, 0.0]);
380        assert_eq!(result.vertices[1].normal, [0.0, 0.0, 1.0]);
381    }
382
383    #[test]
384    fn vertices_to_positions() {
385        let verts = vec![
386            fbx_vertex([1.0, 2.0, 3.0], [0.0; 3]),
387            fbx_vertex([4.0, 5.0, 6.0], [0.0; 3]),
388        ];
389        let positions = fbx_vertices_to_positions(&verts);
390        assert_eq!(positions.len(), 2);
391        assert_eq!(positions[0], [1.0, 2.0, 3.0]);
392    }
393
394    #[test]
395    fn constants_valid() {
396        assert_eq!(MAX_FBX_BONES, 256);
397        assert_eq!(MAX_FBX_VERTICES, 1_000_000);
398    }
399
400    #[test]
401    fn skin_weight_default() {
402        let w = FbxSkinWeight::default();
403        assert_eq!(w.bone_indices, [0, 0, 0, 0]);
404        assert!((w.bone_weights[0] - 1.0).abs() < 0.001);
405        assert!((w.bone_weights[1]).abs() < 0.001);
406    }
407
408    #[test]
409    fn material_data_default() {
410        let m = FbxMaterialData::default();
411        assert!((m.roughness - 0.5).abs() < 0.001);
412        assert!((m.metallic).abs() < 0.001);
413    }
414}