1#[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#[derive(Debug, Clone)]
21pub struct FbxBone {
22 pub name: String,
23 pub parent_index: Option<usize>,
24 pub bind_pose: [f32; 16], }
26
27#[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], pub scale: [f32; 3],
35}
36
37#[derive(Debug, Clone)]
39pub struct FbxAnimation {
40 pub name: String,
41 pub duration_seconds: f32,
42 pub keyframes: Vec<FbxKeyframe>,
43}
44
45#[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#[derive(Debug, Clone, Default)]
63pub struct FbxSkinData {
64 pub weights: Vec<FbxSkinWeight>,
66 pub deformer_count: usize,
68}
69
70#[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#[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#[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
117pub const MAX_FBX_BONES: usize = 256;
119
120pub const MAX_FBX_VERTICES: usize = 1_000_000;
122
123pub fn parse_fbx_geometry(raw_vertices: &[f64], raw_polygon_indices: &[i32]) -> FbxImportResult {
127 let mut result = FbxImportResult::default();
128
129 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 let mut polygon: Vec<u32> = Vec::new();
148 for &idx in raw_polygon_indices {
149 if idx < 0 {
150 let actual = (!idx) as u32; polygon.push(actual);
152 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
169pub 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
178pub 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
239pub fn fbx_vertices_to_positions(vertices: &[FbxVertex]) -> Vec<[f32; 3]> {
241 vertices.iter().map(|v| v.position).collect()
242}
243
244#[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 let mut result = FbxImportResult::default();
261 result.warnings.push(format!("fbx_import_loaded:{}", path.display()));
262
263 Ok(result)
264}
265
266pub 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}