1use std::collections::HashMap;
32
33use super::math::{self, V3};
34use super::texture::{self, Texture};
35use super::{Material, Mesh, Model, ParseError, Result};
36
37const TEMP_LIMIT: usize = 1 << 30;
40const RESULT_LIMIT: usize = 2 << 30;
41const NODE_DEPTH_LIMIT: u32 = 256;
43const MAX_VERTICES: usize = 50_000_000;
45
46const TARGET_UNIT_METERS: f64 = 0.01;
48
49fn limits(memory_limit: usize) -> ufbx::AllocatorOpts {
50 ufbx::AllocatorOpts { memory_limit, ..Default::default() }
51}
52
53pub fn parse(bytes: &[u8], side: &dyn Fn(&str) -> Option<Vec<u8>>) -> Result<Model> {
54 let opts = ufbx::LoadOpts {
55 temp_allocator: limits(TEMP_LIMIT),
56 result_allocator: limits(RESULT_LIMIT),
57 ignore_animation: true,
58 ignore_embedded: false,
61 load_external_files: false,
62 generate_missing_normals: true,
63 file_format: ufbx::FileFormat::Fbx,
66 node_depth_limit: NODE_DEPTH_LIMIT,
67 space_conversion: ufbx::SpaceConversion::ModifyGeometry,
68 target_axes: ufbx::CoordinateAxes {
69 right: ufbx::CoordinateAxis::PositiveX,
70 up: ufbx::CoordinateAxis::PositiveY,
71 front: ufbx::CoordinateAxis::PositiveZ,
72 },
73 target_unit_meters: TARGET_UNIT_METERS,
74 ..Default::default()
75 };
76 let scene = ufbx::load_memory(bytes, opts).map_err(|e| ParseError(format!("not a readable FBX file: {}", e.description.trim())))?;
77 let mut model = Model::default();
78 let mut vertices = 0usize;
79 let mut loader = texture::Loader::new(side);
80 for node in &scene.nodes {
81 let Some(mesh) = node.mesh.as_ref() else { continue };
82 for out in node_meshes(node, mesh, &mut loader) {
83 vertices += out.positions.len();
84 if vertices > MAX_VERTICES {
85 return Err(ParseError::new("the FBX has too many vertices"));
86 }
87 model.meshes.push(out);
88 }
89 }
90 model.finish()
91}
92
93fn v3(v: ufbx::Vec3) -> V3 {
94 [v.x as f32, v.y as f32, v.z as f32]
95}
96
97fn node_meshes(node: &ufbx::Node, mesh: &ufbx::Mesh, loader: &mut texture::Loader) -> Vec<Mesh> {
99 let to_world = &node.geometry_to_world;
100 let normal_matrix = ufbx::matrix_for_normals(to_world);
101 let mirrored = ufbx::matrix_determinant(to_world) < 0.0;
104 let materials = if node.materials.is_empty() { &mesh.materials } else { &node.materials };
106 let positions = &mesh.vertex_position;
107 let normals = &mesh.vertex_normal;
108 let uvs = &mesh.vertex_uv;
109 let mut tri = vec![0u32; mesh.max_face_triangles.max(1) * 3];
110 let mut out = Vec::new();
111 for part in mesh.material_parts.iter() {
112 if part.num_triangles == 0 {
113 continue;
114 }
115 let mut m = Mesh { name: node.element.name.to_string(), ..Default::default() };
116 let mat = materials.get(part.index as usize);
117 let tex = mat.and_then(|mat| map_texture(mat, loader));
118 let (normal, bump) = match mat {
121 Some(mat) if uvs.exists => (
122 loader.for_map(|l| normal_texture(mat).and_then(|t| file_texture(t, l))),
123 loader.for_map(|l| mat.fbx.bump.texture.as_ref().and_then(|t| file_texture(t, l))),
124 ),
125 _ => (None, None),
126 };
127 let transform = tex.as_ref().or(normal.as_ref()).or(bump.as_ref()).and_then(|t| t.1);
129 let mut ns: Vec<V3> = Vec::new();
130 let mut uv_out: Vec<[f32; 2]> = Vec::new();
131 let mut seen: HashMap<(u32, u32, u32), u32> = HashMap::new();
133 for &fi in part.face_indices.iter() {
134 let Some(&face) = mesh.faces.get(fi as usize) else { continue };
135 if face.num_indices < 3 {
136 continue;
137 }
138 let n = ufbx::triangulate_face(&mut tri, mesh, face) as usize;
139 for t in tri[..n * 3].as_chunks::<3>().0 {
140 let t = if mirrored { [t[0], t[2], t[1]] } else { *t };
141 for ix in t {
142 let ix = ix as usize;
144 let pi = positions.indices.get(ix).copied().unwrap_or(0);
145 let ni = if normals.exists { normals.indices.get(ix).copied().unwrap_or(0) } else { u32::MAX };
146 let ui = if uvs.exists { uvs.indices.get(ix).copied().unwrap_or(0) } else { u32::MAX };
147 let next = m.positions.len() as u32;
148 let v = *seen.entry((pi, ni, ui)).or_insert_with(|| {
149 let p = positions.values.get(pi as usize).copied().unwrap_or_default();
150 m.positions.push(v3(ufbx::transform_position(to_world, p)));
151 let nv = normals.values.get(ni as usize).copied().unwrap_or_default();
152 ns.push(math::normalize(v3(ufbx::transform_direction(&normal_matrix, nv))));
153 if uvs.exists {
154 let uv = uvs.values.get(ui as usize).copied().unwrap_or_default();
155 uv_out.push(picture_uv(uv, transform.as_ref()));
156 }
157 next
158 });
159 m.indices.push(v);
160 }
161 }
162 }
163 if normals.exists {
164 m.normals = Some(ns);
165 }
166 if uvs.exists {
167 m.uvs = Some(uv_out);
168 }
169 m.texture = tex.filter(|_| uvs.exists).map(|t| t.0);
171 m.material = mat.map(|m| material(m));
172 m.surface.normal_map = normal.map(|t| t.0);
173 m.surface.bump_map = bump.map(|t| t.0);
174 m.surface.blend = mat.is_some_and(|mat| see_through(mat, m.material.as_ref().map_or(1.0, |m| m.opacity)));
175 out.push(m);
176 }
177 out
178}
179
180fn picture_uv(uv: ufbx::Vec2, transform: Option<&ufbx::Matrix>) -> [f32; 2] {
183 let (mut u, mut v) = (uv.x, uv.y);
184 if let Some(t) = transform {
185 (u, v) = (t.m00 * uv.x + t.m01 * uv.y + t.m03, t.m10 * uv.x + t.m11 * uv.y + t.m13);
186 }
187 [u as f32, 1.0 - v as f32]
188}
189
190fn map_texture(mat: &ufbx::Material, loader: &mut texture::Loader) -> Option<(Texture, Option<ufbx::Matrix>)> {
192 let tex = mat.pbr.base_color.texture.as_ref().or(mat.fbx.diffuse_color.texture.as_ref())?;
193 file_texture(tex, loader)
194}
195
196fn normal_texture(mat: &ufbx::Material) -> Option<&ufbx::Texture> {
199 mat.fbx.normal_map.texture.as_ref().or(mat.pbr.normal_map.texture.as_ref()).map(|t| &**t)
200}
201
202fn file_texture(tex: &ufbx::Texture, loader: &mut texture::Loader) -> Option<(Texture, Option<ufbx::Matrix>)> {
204 let file = if tex.type_ == ufbx::TextureType::File { Some(tex) } else { tex.file_textures.first().map(|t| &**t) }?;
206 let name = |p: &str| p.rsplit(['/', '\\']).next().unwrap_or(p).to_string();
207 let image = if file.content.size > 0 {
209 loader.embedded(&name(&file.filename), &file.content)
210 } else {
211 [&file.relative_filename, &file.filename]
212 .into_iter()
213 .filter(|p| !p.is_empty())
214 .find_map(|p| loader.file(&p.replace('\\', "/")))
215 }?;
216 Some((Texture::new(image), file.has_uv_transform.then_some(file.uv_to_texture)))
217}
218
219fn see_through(mat: &ufbx::Material, opacity: f32) -> bool {
222 opacity < 1.0 - 1e-6 || mat.fbx.transparency_color.texture.is_some() || mat.fbx.transparency_factor.texture.is_some()
223}
224
225fn material(mat: &ufbx::Material) -> Material {
226 let pbr = &mat.pbr;
227 let c = pbr.base_color.value_vec4;
228 let factor = if pbr.base_factor.has_value { pbr.base_factor.value_vec4.x } else { 1.0 };
229 let color = [c.x, c.y, c.z].map(|v| ((v * factor) as f32).clamp(0.0, 1.0));
230 let color = if pbr.base_color.has_value { color } else { super::DEFAULT_COLOR };
231 let opacity = ufbx::find_prop(&mat.element.props, "Opacity").map_or(1.0f32, |p| p.value_vec4.x as f32);
234 let opacity = if opacity.is_nan() { 1.0 } else { opacity.clamp(0.0, 1.0) };
236 Material { name: mat.element.name.to_string(), color, opacity }
237}
238
239#[cfg(test)]
240mod tests {
241 use super::*;
242
243 enum P {
245 Id(i64),
247 Int(i32),
249 D(f64),
250 S(&'static str),
251 Name(&'static str, &'static str),
254 Ints(Vec<i32>),
255 Doubles(Vec<f64>),
256 Bytes(Vec<u8>),
258 }
259
260 struct N {
261 name: &'static str,
262 props: Vec<P>,
263 children: Vec<N>,
264 }
265
266 fn n(name: &'static str, props: Vec<P>, children: Vec<N>) -> N {
267 N { name, props, children }
268 }
269
270 fn prop(name: &'static str, ty: &'static str, values: Vec<P>) -> N {
272 let mut props = vec![P::S(name), P::S(ty), P::S(""), P::S("")];
273 props.extend(values);
274 n("P", props, vec![])
275 }
276
277 fn ascii(nodes: &[N]) -> Vec<u8> {
278 fn write(out: &mut String, node: &N, depth: usize) {
279 let pad = "\t".repeat(depth);
280 out.push_str(&format!("{pad}{}: ", node.name));
281 let mut arrays = Vec::new();
282 let parts: Vec<String> = node
283 .props
284 .iter()
285 .map(|p| match p {
286 P::Id(v) => v.to_string(),
287 P::Int(v) => v.to_string(),
288 P::D(v) => format!("{v:?}"),
289 P::S(s) => format!("\"{s}\""),
290 P::Name(class, name) => format!("\"{class}::{name}\""),
291 P::Ints(v) => {
292 arrays.push(v.iter().map(|x| x.to_string()).collect::<Vec<_>>().join(","));
293 format!("*{}", v.len())
294 }
295 P::Bytes(_) => unreachable!("raw bytes in an ASCII file"),
296 P::Doubles(v) => {
297 arrays.push(v.iter().map(|x| format!("{x:?}")).collect::<Vec<_>>().join(","));
298 format!("*{}", v.len())
299 }
300 })
301 .collect();
302 out.push_str(&parts.join(", "));
303 if let Some(a) = arrays.first() {
304 out.push_str(&format!(" {{\n{pad}\ta: {a}\n{pad}}}\n"));
305 } else if node.children.is_empty() {
306 out.push('\n');
307 } else {
308 out.push_str(" {\n");
309 for c in &node.children {
310 write(out, c, depth + 1);
311 }
312 out.push_str(&format!("{pad}}}\n"));
313 }
314 }
315 let mut out = String::from("; FBX 7.4.0 project file\n");
316 for node in nodes {
317 write(&mut out, node, 0);
318 }
319 out.into_bytes()
320 }
321
322 fn binary(nodes: &[N]) -> Vec<u8> {
323 fn write(out: &mut Vec<u8>, node: &N) {
324 let start = out.len();
325 out.extend([0u8; 12]);
326 out.push(node.name.len() as u8);
327 out.extend(node.name.as_bytes());
328 let props_start = out.len();
329 for p in &node.props {
330 match p {
331 P::Id(v) => {
332 out.push(b'L');
333 out.extend(v.to_le_bytes());
334 }
335 P::Int(v) => {
336 out.push(b'I');
337 out.extend(v.to_le_bytes());
338 }
339 P::D(v) => {
340 out.push(b'D');
341 out.extend(v.to_le_bytes());
342 }
343 P::S(s) => {
344 out.push(b'S');
345 out.extend((s.len() as u32).to_le_bytes());
346 out.extend(s.as_bytes());
347 }
348 P::Name(class, name) => {
349 let s = format!("{name}\0\x01{class}");
350 out.push(b'S');
351 out.extend((s.len() as u32).to_le_bytes());
352 out.extend(s.as_bytes());
353 }
354 P::Bytes(v) => {
355 out.push(b'R');
356 out.extend((v.len() as u32).to_le_bytes());
357 out.extend(v);
358 }
359 P::Ints(v) => {
360 out.push(b'i');
361 out.extend((v.len() as u32).to_le_bytes());
362 out.extend(0u32.to_le_bytes());
363 out.extend((v.len() as u32 * 4).to_le_bytes());
364 v.iter().for_each(|x| out.extend(x.to_le_bytes()));
365 }
366 P::Doubles(v) => {
367 out.push(b'd');
368 out.extend((v.len() as u32).to_le_bytes());
369 out.extend(0u32.to_le_bytes());
370 out.extend((v.len() as u32 * 8).to_le_bytes());
371 v.iter().for_each(|x| out.extend(x.to_le_bytes()));
372 }
373 }
374 }
375 let props_len = (out.len() - props_start) as u32;
376 if !node.children.is_empty() {
377 for c in &node.children {
378 write(out, c);
379 }
380 out.extend([0u8; 13]);
381 }
382 let end = out.len() as u32;
383 out[start..start + 4].copy_from_slice(&end.to_le_bytes());
384 out[start + 4..start + 8].copy_from_slice(&(node.props.len() as u32).to_le_bytes());
385 out[start + 8..start + 12].copy_from_slice(&props_len.to_le_bytes());
386 }
387 let mut out = b"Kaydara FBX Binary \x00\x1a\x00".to_vec();
388 out.extend(7400u32.to_le_bytes());
389 for node in nodes {
390 write(&mut out, node);
391 }
392 out.extend([0u8; 13]);
393 out
394 }
395
396 fn header() -> N {
397 n("FBXHeaderExtension", vec![], vec![n("FBXHeaderVersion", vec![P::Int(1003)], vec![]), n("FBXVersion", vec![P::Int(7400)], vec![])])
398 }
399
400 fn settings(up: i32, unit_scale: f64) -> N {
403 let front = if up == 2 { 1 } else { 2 };
404 let front_sign = if up == 2 { -1 } else { 1 };
405 n(
406 "GlobalSettings",
407 vec![],
408 vec![
409 n("Version", vec![P::Int(1000)], vec![]),
410 n(
411 "Properties70",
412 vec![],
413 vec![
414 prop("UpAxis", "int", vec![P::Int(up)]),
415 prop("UpAxisSign", "int", vec![P::Int(1)]),
416 prop("FrontAxis", "int", vec![P::Int(front)]),
417 prop("FrontAxisSign", "int", vec![P::Int(front_sign)]),
418 prop("CoordAxis", "int", vec![P::Int(0)]),
419 prop("CoordAxisSign", "int", vec![P::Int(1)]),
420 prop("UnitScaleFactor", "double", vec![P::D(unit_scale)]),
421 prop("OriginalUnitScaleFactor", "double", vec![P::D(unit_scale)]),
422 ],
423 ),
424 ],
425 )
426 }
427
428 fn box_file(up: i32, unit_scale: f64, tx: f64, depth: f64) -> Vec<N> {
431 let corners = [[0., 0., 0.], [1., 0., 0.], [1., 1., 0.], [0., 1., 0.], [0., 0., 1.], [1., 0., 1.], [1., 1., 1.], [0., 1., 1.]];
432 let verts: Vec<f64> = corners.iter().flat_map(|c: &[f64; 3]| [c[0], c[1], c[2] * depth]).collect();
433 let quads = [[0, 3, 2, 1], [4, 5, 6, 7], [0, 1, 5, 4], [2, 3, 7, 6], [1, 2, 6, 5], [0, 4, 7, 3]];
434 let idx: Vec<i32> = quads.iter().flat_map(|q| [q[0], q[1], q[2], -q[3] - 1]).collect();
436 vec![
437 header(),
438 settings(up, unit_scale),
439 n(
440 "Objects",
441 vec![],
442 vec![
443 n(
444 "Geometry",
445 vec![P::Id(1000), P::Name("Geometry", "Box"), P::S("Mesh")],
446 vec![n("Vertices", vec![P::Doubles(verts)], vec![]), n("PolygonVertexIndex", vec![P::Ints(idx)], vec![])],
447 ),
448 n(
449 "Model",
450 vec![P::Id(2000), P::Name("Model", "Box"), P::S("Mesh")],
451 vec![
452 n("Version", vec![P::Int(232)], vec![]),
453 n("Properties70", vec![], vec![prop("Lcl Translation", "Lcl Translation", vec![P::D(tx), P::D(0.0), P::D(0.0)])]),
454 ],
455 ),
456 n(
457 "Material",
458 vec![P::Id(3000), P::Name("Material", "Red"), P::S("")],
459 vec![
460 n("Version", vec![P::Int(102)], vec![]),
461 n("ShadingModel", vec![P::S("phong")], vec![]),
462 n("Properties70", vec![], vec![prop("DiffuseColor", "Color", vec![P::D(1.0), P::D(0.0), P::D(0.0)])]),
463 ],
464 ),
465 ],
466 ),
467 n(
468 "Connections",
469 vec![],
470 vec![
471 n("C", vec![P::S("OO"), P::Id(1000), P::Id(2000)], vec![]),
472 n("C", vec![P::S("OO"), P::Id(2000), P::Id(0)], vec![]),
473 n("C", vec![P::S("OO"), P::Id(3000), P::Id(2000)], vec![]),
474 ],
475 ),
476 ]
477 }
478
479 fn size(m: &Model) -> V3 {
480 let (lo, hi) = m.bounds().unwrap();
481 math::sub(hi, lo)
482 }
483
484 fn close(a: V3, b: V3) -> bool {
485 a.iter().zip(b).all(|(x, y)| (x - y).abs() < 1e-4)
486 }
487
488 #[test]
489 fn ascii_and_binary_boxes_read_the_same() {
490 for (kind, bytes) in [("ascii", ascii(&box_file(1, 1.0, 10.0, 1.0))), ("binary", binary(&box_file(1, 1.0, 10.0, 1.0)))] {
491 let m = parse(&bytes, &|_| None).unwrap_or_else(|e| panic!("{kind}: {e}"));
492 assert_eq!(m.meshes.len(), 1, "{kind}");
493 assert_eq!(m.triangle_count(), 12, "{kind}: six quads, two triangles each");
494 let mesh = &m.meshes[0];
495 assert_eq!(mesh.name, "Box");
496 let mat = mesh.material.as_ref().unwrap();
497 assert_eq!((mat.name.as_str(), mat.color), ("Red", [1.0, 0.0, 0.0]), "{kind}");
498 let (lo, hi) = m.bounds().unwrap();
500 assert!(close(lo, [10.0, 0.0, 0.0]) && close(hi, [11.0, 1.0, 1.0]), "{kind}: {lo:?} {hi:?}");
501 let ns = mesh.normals.as_ref().expect("generated normals");
503 assert_eq!(ns.len(), mesh.positions.len());
504 assert!(ns.iter().all(|v| (math::length(*v) - 1.0).abs() < 1e-4));
505 let s = super::super::geom::stats(&m);
506 assert!((s.volume.unwrap() - 1.0).abs() < 1e-4, "{kind}: a closed box");
507 }
508 }
509
510 fn textured_file(content: Option<Vec<u8>>) -> Vec<N> {
515 textured_on("DiffuseColor", content, true, vec![])
516 }
517
518 fn textured_on(on: &'static str, content: Option<Vec<u8>>, uvs: bool, extra: Vec<N>) -> Vec<N> {
522 let verts = vec![0., 0., 0., 1., 0., 0., 1., 1., 0., 0., 1., 0.];
523 let uv = vec![0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.25, 0.0, 0.0, 1.0];
524 let mut texture = vec![
525 n("Type", vec![P::S("TextureVideoClip")], vec![]),
526 n("FileName", vec![P::S("/elsewhere/tex.png")], vec![]),
527 n("RelativeFilename", vec![P::S("tex.png")], vec![]),
528 ];
529 let mut objects = vec![
530 n(
531 "Geometry",
532 vec![P::Id(1000), P::Name("Geometry", "Quad"), P::S("Mesh")],
533 vec![
534 n("Vertices", vec![P::Doubles(verts)], vec![]),
535 n("PolygonVertexIndex", vec![P::Ints(vec![0, 1, -3, 0, 2, -4])], vec![]),
536 n(
537 if uvs { "LayerElementUV" } else { "Unused" },
538 vec![P::Int(0)],
539 vec![
540 n("Version", vec![P::Int(101)], vec![]),
541 n("Name", vec![P::S("UVMap")], vec![]),
542 n("MappingInformationType", vec![P::S("ByPolygonVertex")], vec![]),
543 n("ReferenceInformationType", vec![P::S("IndexToDirect")], vec![]),
544 n("UV", vec![P::Doubles(uv)], vec![]),
545 n("UVIndex", vec![P::Ints(vec![0, 1, 2, 3, 2, 4])], vec![]),
546 ],
547 ),
548 ],
549 ),
550 n("Model", vec![P::Id(2000), P::Name("Model", "Quad"), P::S("Mesh")], vec![n("Version", vec![P::Int(232)], vec![])]),
551 n(
552 "Material",
553 vec![P::Id(3000), P::Name("Material", "Red"), P::S("")],
554 vec![
555 n("ShadingModel", vec![P::S("phong")], vec![]),
556 n("Properties70", vec![], std::iter::once(prop("DiffuseColor", "Color", vec![P::D(1.0), P::D(0.0), P::D(0.0)])).chain(extra).collect()),
557 ],
558 ),
559 ];
560 let mut connections = vec![
561 n("C", vec![P::S("OO"), P::Id(1000), P::Id(2000)], vec![]),
562 n("C", vec![P::S("OO"), P::Id(2000), P::Id(0)], vec![]),
563 n("C", vec![P::S("OO"), P::Id(3000), P::Id(2000)], vec![]),
564 n("C", vec![P::S("OP"), P::Id(4000), P::Id(3000), P::S(on)], vec![]),
565 ];
566 if let Some(bytes) = content {
567 objects.push(n(
570 "Video",
571 vec![P::Id(5000), P::Name("Video", "Clip"), P::S("Clip")],
572 vec![
573 n("Type", vec![P::S("Clip")], vec![]),
574 n("RelativeFilename", vec![P::S("tex.png")], vec![]),
575 n("Content", vec![P::Bytes(bytes)], vec![]),
576 ],
577 ));
578 connections.push(n("C", vec![P::S("OO"), P::Id(5000), P::Id(4000)], vec![]));
579 }
580 texture.push(n("Version", vec![P::Int(202)], vec![]));
581 objects.push(n("Texture", vec![P::Id(4000), P::Name("Texture", "Tex"), P::S("")], texture));
582 vec![header(), settings(1, 1.0), n("Objects", vec![], objects), n("Connections", vec![], connections)]
583 }
584
585 #[test]
586 fn uvs_and_a_sidecar_texture_are_read() {
587 let png = super::texture::tests::png_2x2();
588 let side = |name: &str| (name == "tex.png").then(|| png.clone());
589 let m = parse(&ascii(&textured_file(None)), &side).unwrap();
590 let mesh = &m.meshes[0];
591 assert_eq!(mesh.positions.len(), 6);
594 let uvs = mesh.uvs.as_ref().expect("uvs");
595 assert_eq!(uvs.len(), 6);
596 let origin: Vec<[f32; 2]> = (0..6).filter(|&i| close(mesh.positions[i], [0.0; 3])).map(|i| uvs[i]).collect();
598 assert!(origin.contains(&[0.0, 1.0]) && origin.contains(&[0.25, 1.0]), "{origin:?}");
599 let at = |p: V3| mesh.positions.iter().position(|q| close(*q, p)).map(|i| uvs[i]);
601 assert_eq!(at([1.0, 1.0, 0.0]), Some([1.0, 0.0]));
602 assert_eq!(at([1.0, 0.0, 0.0]), Some([1.0, 1.0]));
603 assert!(uvs.contains(&[0.0, 0.0]));
604 let tex = mesh.texture.as_ref().expect("texture");
605 assert_eq!((tex.image.width, tex.image.height), (2, 2));
606 assert_eq!(tex.tint, [1.0; 3]);
607 let mat = mesh.material.as_ref().unwrap();
608 assert_eq!(mat.color, [1.0, 0.0, 0.0]);
609 }
610
611 #[test]
612 fn a_missing_texture_keeps_the_colour() {
613 let m = parse(&ascii(&textured_file(None)), &|_| None).unwrap();
614 let mesh = &m.meshes[0];
615 assert!(mesh.texture.is_none());
616 assert!(mesh.uvs.is_some(), "UVs are still read");
617 assert_eq!(mesh.material.as_ref().unwrap().color, [1.0, 0.0, 0.0]);
618 let m = parse(&ascii(&textured_file(None)), &|_| Some(b"nope".to_vec())).unwrap();
620 assert!(m.meshes[0].texture.is_none());
621 let m = parse(&ascii(&box_file(1, 1.0, 0.0, 1.0)), &|_| Some(super::texture::tests::png_2x2())).unwrap();
623 assert!(m.meshes[0].uvs.is_none() && m.meshes[0].texture.is_none());
624 }
625
626 #[test]
627 fn bump_and_normal_sidecar_maps_are_read() {
628 let png = super::texture::tests::png_2x2();
629 let side = |name: &str| (name == "tex.png").then(|| png.clone());
630 for (on, want_normal) in [("Bump", false), ("NormalMap", true)] {
631 let m = parse(&ascii(&textured_on(on, None, true, vec![])), &side).unwrap();
632 let mesh = &m.meshes[0];
633 let s = &mesh.surface;
634 assert!(mesh.texture.is_none(), "{on}: not the base map");
635 let got = if want_normal { &s.normal_map } else { &s.bump_map };
636 assert_eq!(got.as_ref().map(|t| t.image.width), Some(2), "{on}");
637 assert!(if want_normal { s.bump_map.is_none() } else { s.normal_map.is_none() }, "{on}");
638 assert!(s.emissive_map.is_none() && s.emissive == [0.0; 3] && !s.blend, "{on}");
639 assert!(mesh.uvs.is_some());
640 }
641 }
642
643 #[test]
644 fn an_embedded_normal_map_is_read_from_binary() {
645 let embedded = super::texture::tests::png(3, 1, [9, 8, 7, 255]);
646 let m = parse(&binary(&textured_on("NormalMap", Some(embedded), true, vec![])), &|_| None).unwrap();
647 let tex = m.meshes[0].surface.normal_map.as_ref().expect("normal map");
648 assert_eq!((tex.image.width, tex.image.height), (3, 1));
649 }
650
651 #[test]
652 fn opacity_and_transparency_textures_set_blend() {
653 let opacity = |v: f64| vec![prop("Opacity", "double", vec![P::D(v)])];
654 let blend = |file: Vec<N>| parse(&ascii(&file), &|_| None).unwrap().meshes[0].surface.blend;
655 assert!(blend(textured_on("DiffuseColor", None, true, opacity(0.5))));
656 assert!(!blend(textured_on("DiffuseColor", None, true, opacity(1.0))));
657 assert!(!blend(textured_on("DiffuseColor", None, true, vec![])));
658 let factor = vec![prop("TransparencyFactor", "double", vec![P::D(1.0)])];
661 assert!(!blend(textured_on("DiffuseColor", None, true, factor)));
662 assert!(blend(textured_on("TransparencyFactor", None, true, vec![])));
664 assert!(blend(textured_on("TransparentColor", None, true, vec![])));
665 let png = super::texture::tests::png_2x2();
666 let m = parse(&ascii(&textured_on("TransparencyFactor", None, true, vec![])), &|_| Some(png.clone())).unwrap();
667 assert!(m.meshes[0].surface.blend && m.meshes[0].texture.is_none(), "the transparency picture is not drawn");
668 assert_eq!(m.meshes[0].material.as_ref().unwrap().opacity, 1.0);
669 }
670
671 #[test]
672 fn a_missing_or_bad_map_stays_none() {
673 for side in [&(|_: &str| None) as &dyn Fn(&str) -> Option<Vec<u8>>, &|_: &str| Some(b"nope".to_vec())] {
674 let m = parse(&ascii(&textured_on("Bump", None, true, vec![])), side).unwrap();
675 assert_eq!(m.meshes.len(), 1);
676 assert!(m.meshes[0].surface.bump_map.is_none() && m.meshes[0].uvs.is_some());
677 assert_eq!(m.meshes[0].material.as_ref().unwrap().color, [1.0, 0.0, 0.0]);
678 }
679 }
680
681 #[test]
682 fn maps_without_uvs_are_dropped() {
683 let png = super::texture::tests::png_2x2();
684 for on in ["Bump", "NormalMap"] {
685 let m = parse(&ascii(&textured_on(on, None, false, vec![])), &|_| Some(png.clone())).unwrap();
686 let mesh = &m.meshes[0];
687 assert!(mesh.uvs.is_none() && mesh.surface.bump_map.is_none() && mesh.surface.normal_map.is_none(), "{on}");
688 }
689 }
690
691 #[test]
692 fn an_embedded_picture_wins_over_the_sidecar() {
693 let embedded = super::texture::tests::png(3, 1, [9, 8, 7, 255]);
694 let sidecar = super::texture::tests::png_2x2();
695 let side = |name: &str| (name == "tex.png").then(|| sidecar.clone());
696 let m = parse(&binary(&textured_file(Some(embedded))), &side).unwrap();
697 let tex = m.meshes[0].texture.as_ref().expect("texture");
698 assert_eq!((tex.image.width, tex.image.height), (3, 1));
699 let m = parse(&binary(&textured_file(None)), &side).unwrap();
701 assert_eq!(m.meshes[0].texture.as_ref().unwrap().image.width, 2);
702 }
703
704 #[test]
705 fn unit_scale_and_up_axis_are_converted() {
706 let m = parse(&binary(&box_file(1, 100.0, 0.0, 1.0)), &|_| None).unwrap();
708 assert!(close(size(&m), [100.0, 100.0, 100.0]), "{:?}", size(&m));
709 let m = parse(&ascii(&box_file(1, 0.1, 0.0, 1.0)), &|_| None).unwrap();
711 assert!(close(size(&m), [0.1, 0.1, 0.1]), "{:?}", size(&m));
712 let m = parse(&ascii(&box_file(2, 1.0, 0.0, 3.0)), &|_| None).unwrap();
714 assert!(close(size(&m), [1.0, 3.0, 1.0]), "{:?}", size(&m));
715 }
716
717 #[test]
718 fn files_load_by_extension_and_get_a_thumbnail() {
719 let dir = tempfile::tempdir().unwrap();
720 let path = dir.path().join("Box.FBX");
721 std::fs::write(&path, binary(&box_file(1, 1.0, 0.0, 1.0))).unwrap();
722 assert_eq!(super::super::load(&path, "FBX").unwrap().triangle_count(), 12);
723 let img = super::super::raster::thumbnail(&path, "fbx").expect("a drawn thumbnail");
724 assert!(img.pixels().any(|p| p[3] == 255 && p[0] > p[1]), "drawn in the red material");
725 }
726
727 #[test]
728 fn empty_scene_is_an_error_not_a_panic() {
729 let e = parse(&ascii(&[header(), settings(1, 1.0)]), &|_| None).unwrap_err();
730 assert!(e.0.contains("no triangles"), "{e}");
731 assert!(parse(&binary(&[header(), settings(1, 1.0)]), &|_| None).is_err());
732 }
733
734 #[test]
735 fn corrupt_and_truncated_files_fail_cleanly() {
736 assert!(parse(b"", &|_| None).is_err());
737 assert!(parse(b"not an fbx at all", &|_| None).is_err());
738 assert!(parse(b"v 0 0 0\nv 1 0 0\nv 0 1 0\nf 1 2 3\n", &|_| None).is_err());
740 let whole = binary(&box_file(1, 1.0, 0.0, 1.0));
741 for cut in [30, whole.len() / 3, whole.len() / 2, whole.len() - 20] {
742 assert!(parse(&whole[..cut], &|_| None).is_err(), "cut at {cut}");
743 }
744 let mut huge = whole.clone();
746 let at = huge.windows(4).position(|w| w == b"Vert").unwrap() + 8;
747 assert_eq!(huge[at], b'd');
748 huge[at + 1..at + 5].copy_from_slice(&u32::MAX.to_le_bytes());
749 let t = std::time::Instant::now();
750 assert!(parse(&huge, &|_| None).is_err());
751 assert!(t.elapsed() < std::time::Duration::from_secs(5));
752 let mut junk = whole[..27].to_vec();
754 let mut x = 0x2545_f491_u32;
755 junk.extend((0..4096).map(|_| {
756 x ^= x << 13;
757 x ^= x >> 17;
758 x ^= x << 5;
759 x as u8
760 }));
761 assert!(parse(&junk, &|_| None).is_err());
762 }
763}