1use std::collections::HashMap;
27
28use super::geom::{self, face_normal};
29use super::math::{self, V3};
30use super::texture::{Loader, Texture};
31use super::{DEFAULT_COLOR, Material, Mesh, Model, ParseError, Result};
32
33const MAIN: u16 = 0x4D4D;
34const EDITOR: u16 = 0x3D3D;
35const OBJECT: u16 = 0x4000;
36const TRI_MESH: u16 = 0x4100;
37const VERTICES: u16 = 0x4110;
38const FACES: u16 = 0x4120;
39const FACE_MATERIAL: u16 = 0x4130;
40const TEX_COORDS: u16 = 0x4140;
41const SMOOTHING: u16 = 0x4150;
42const LOCAL_AXES: u16 = 0x4160;
43const MATERIAL: u16 = 0xAFFF;
44const MAT_NAME: u16 = 0xA000;
45const MAT_DIFFUSE: u16 = 0xA020;
46const MAT_TRANSPARENCY: u16 = 0xA050;
47const MAT_TEXTURE: u16 = 0xA200;
48const MAT_TEXTURE_FILE: u16 = 0xA300;
49#[cfg(test)]
51const MAP_TILING: u16 = 0xA351;
52const MAP_U_SCALE: u16 = 0xA354;
53const MAP_V_SCALE: u16 = 0xA356;
54const MAP_U_OFFSET: u16 = 0xA358;
55const MAP_V_OFFSET: u16 = 0xA35A;
56const MAP_ROTATION: u16 = 0xA35C;
57const COLOR_FLOAT: u16 = 0x0010;
58const COLOR_BYTE: u16 = 0x0011;
59const COLOR_BYTE_GAMMA: u16 = 0x0012;
60const COLOR_FLOAT_GAMMA: u16 = 0x0013;
61const PERCENT_INT: u16 = 0x0030;
62const PERCENT_FLOAT: u16 = 0x0031;
63
64struct Chunks<'a> {
68 data: &'a [u8],
69 pos: usize,
70}
71
72fn chunks(data: &[u8]) -> Chunks<'_> {
73 Chunks { data, pos: 0 }
74}
75
76impl<'a> Iterator for Chunks<'a> {
77 type Item = Result<(u16, &'a [u8])>;
78
79 fn next(&mut self) -> Option<Self::Item> {
80 let rest = self.data.get(self.pos..)?;
81 if rest.len() < 6 {
82 self.pos = self.data.len();
83 return None;
84 }
85 let id = u16::from_le_bytes([rest[0], rest[1]]);
86 let len = u32::from_le_bytes([rest[2], rest[3], rest[4], rest[5]]) as usize;
87 if len < 6 || len > rest.len() {
88 self.pos = self.data.len();
89 return Some(Err(ParseError::new("bad chunk length in 3DS file")));
90 }
91 self.pos += len;
92 Some(Ok((id, &rest[6..len])))
93 }
94}
95
96struct Reader<'a> {
98 b: &'a [u8],
99}
100
101fn short() -> ParseError {
102 ParseError::new("truncated 3DS chunk")
103}
104
105impl<'a> Reader<'a> {
106 fn take(&mut self, n: usize) -> Result<&'a [u8]> {
107 if n > self.b.len() {
108 return Err(short());
109 }
110 let (head, tail) = self.b.split_at(n);
111 self.b = tail;
112 Ok(head)
113 }
114
115 fn records(&mut self, count: usize, size: usize) -> Result<&'a [u8]> {
118 let n = count.checked_mul(size).ok_or_else(short)?;
119 self.take(n)
120 }
121
122 fn u16(&mut self) -> Result<u16> {
123 let b = self.take(2)?;
124 Ok(u16::from_le_bytes([b[0], b[1]]))
125 }
126
127 fn f32(&mut self) -> Result<f32> {
128 let b = self.take(4)?;
129 Ok(f32::from_le_bytes([b[0], b[1], b[2], b[3]]))
130 }
131
132 fn cstr(&mut self) -> Result<String> {
134 let end = self.b.iter().position(|&c| c == 0).ok_or_else(short)?;
135 let s = String::from_utf8_lossy(&self.b[..end]).into_owned();
136 self.b = &self.b[end + 1..];
137 Ok(s)
138 }
139
140 fn rest(self) -> &'a [u8] {
141 self.b
142 }
143}
144
145fn f32_at(b: &[u8], i: usize) -> f32 {
146 f32::from_le_bytes([b[i], b[i + 1], b[i + 2], b[i + 3]])
147}
148
149#[derive(Debug, Clone, Default)]
150struct Mat {
151 name: String,
152 color: Option<V3>,
153 gamma: bool,
156 opacity: f32,
157 texture: Option<String>,
159 map: MapTransform,
161 loaded: Option<Texture>,
163}
164
165#[derive(Debug, Clone, Copy)]
167struct MapTransform {
168 scale: [f32; 2],
169 offset: [f32; 2],
170 rotation: f32,
172}
173
174impl Default for MapTransform {
175 fn default() -> Self {
176 MapTransform { scale: [1.0; 2], offset: [0.0; 2], rotation: 0.0 }
177 }
178}
179
180impl MapTransform {
181 fn apply(&self, uv: [f32; 2]) -> [f32; 2] {
188 let (s, c) = self.rotation.to_radians().sin_cos();
189 let (u, v) = (c * uv[0] + s * uv[1], -s * uv[0] + c * uv[1]);
190 let k = |x: f32| if x == 0.0 { 1.0 } else { x };
191 [u * k(self.scale[0]) + self.offset[0], 1.0 - (v * k(self.scale[1]) + self.offset[1])]
192 }
193}
194
195#[derive(Default)]
196struct Object {
197 name: String,
198 positions: Vec<V3>,
199 faces: Vec<[u16; 3]>,
200 groups: Vec<(String, Vec<u16>)>,
202 smoothing: Option<Vec<u32>>,
204 uvs: Option<Vec<[f32; 2]>>,
206}
207
208#[derive(Default)]
209struct Scene {
210 materials: Vec<Mat>,
211 objects: Vec<Object>,
212}
213
214pub fn parse(bytes: &[u8], side: &dyn Fn(&str) -> Option<Vec<u8>>) -> Result<Model> {
216 let mut top = chunks(bytes);
217 let main = match top.next() {
218 Some(Ok((MAIN, body))) => body,
219 _ => return Err(ParseError::new("not a 3DS file")),
220 };
221 let mut scene = Scene::default();
222 for c in chunks(main) {
223 let (id, body) = c?;
224 if id == EDITOR {
225 read_editor(body, &mut scene)?;
226 }
227 }
228 let mut loader = Loader::new(side);
229 for m in &mut scene.materials {
230 if let Some(image) = m.texture.as_deref().and_then(|t| loader.file(t)) {
231 if let Some(c) = image.average() {
232 m.color = Some(c);
233 }
234 m.loaded = Some(Texture { image, tint: [1.0; 3], alpha_cutoff: None });
235 }
236 }
237 Model { meshes: build(&scene) }.finish()
238}
239
240fn read_editor(body: &[u8], scene: &mut Scene) -> Result<()> {
241 for c in chunks(body) {
242 let (id, body) = c?;
243 match id {
244 OBJECT => read_object(body, scene)?,
245 MATERIAL => scene.materials.push(read_material(body)?),
246 _ => {}
247 }
248 }
249 Ok(())
250}
251
252fn read_object(body: &[u8], scene: &mut Scene) -> Result<()> {
253 let mut r = Reader { b: body };
254 let name = r.cstr()?;
255 for c in chunks(r.rest()) {
256 let (id, body) = c?;
257 if id == TRI_MESH {
258 scene.objects.push(read_mesh(name.clone(), body)?);
259 }
260 }
261 Ok(())
262}
263
264fn read_mesh(name: String, body: &[u8]) -> Result<Object> {
265 let mut o = Object { name, ..Object::default() };
266 for c in chunks(body) {
267 let (id, body) = c?;
268 match id {
269 VERTICES => o.positions = read_vertices(body)?,
270 FACES => read_faces(body, &mut o)?,
271 TEX_COORDS => o.uvs = Some(read_tex_coords(body)?),
272 LOCAL_AXES => {
273 Reader { b: body }.take(48)?;
274 }
275 _ => {}
276 }
277 }
278 Ok(o)
279}
280
281fn read_vertices(body: &[u8]) -> Result<Vec<V3>> {
282 let mut r = Reader { b: body };
283 let n = r.u16()? as usize;
284 let data = r.records(n, 12)?;
285 Ok(data.as_chunks::<12>().0.iter().map(|v| [f32_at(v, 0), f32_at(v, 4), f32_at(v, 8)]).collect())
286}
287
288fn read_tex_coords(body: &[u8]) -> Result<Vec<[f32; 2]>> {
289 let mut r = Reader { b: body };
290 let n = r.u16()? as usize;
291 let data = r.records(n, 8)?;
292 Ok(data.as_chunks::<8>().0.iter().map(|v| [f32_at(v, 0), f32_at(v, 4)].map(|x| if x.is_finite() { x } else { 0.0 })).collect())
293}
294
295fn read_faces(body: &[u8], o: &mut Object) -> Result<()> {
296 let mut r = Reader { b: body };
297 let n = r.u16()? as usize;
298 let data = r.records(n, 8)?;
299 o.faces = data.as_chunks::<8>().0.iter().map(|f| [0, 2, 4].map(|k| u16::from_le_bytes([f[k], f[k + 1]]))).collect();
300 for c in chunks(r.rest()) {
301 let (id, body) = c?;
302 match id {
303 FACE_MATERIAL => {
304 let mut r = Reader { b: body };
305 let name = r.cstr()?;
306 let n = r.u16()? as usize;
307 let data = r.records(n, 2)?;
308 let faces = data.as_chunks::<2>().0.iter().map(|f| u16::from_le_bytes([f[0], f[1]])).collect();
309 o.groups.push((name, faces));
310 }
311 SMOOTHING => {
312 let mut masks: Vec<u32> = body.as_chunks::<4>().0.iter().take(o.faces.len()).map(|m| u32::from_le_bytes([m[0], m[1], m[2], m[3]])).collect();
314 masks.resize(o.faces.len(), 0);
315 o.smoothing = Some(masks);
316 }
317 _ => {}
318 }
319 }
320 Ok(())
321}
322
323fn read_material(body: &[u8]) -> Result<Mat> {
324 let mut m = Mat { opacity: 1.0, ..Mat::default() };
325 for c in chunks(body) {
326 let (id, body) = c?;
327 match id {
328 MAT_NAME => m.name = Reader { b: body }.cstr()?,
329 MAT_DIFFUSE => read_diffuse(body, &mut m)?,
330 MAT_TRANSPARENCY => read_transparency(body, &mut m)?,
331 MAT_TEXTURE => {
332 for c in chunks(body) {
333 let (id, body) = c?;
334 let mut r = Reader { b: body };
335 match id {
336 MAT_TEXTURE_FILE => m.texture = Some(r.cstr()?),
337 MAP_U_SCALE => m.map.scale[0] = finite(r.f32()?, 1.0),
338 MAP_V_SCALE => m.map.scale[1] = finite(r.f32()?, 1.0),
339 MAP_U_OFFSET => m.map.offset[0] = finite(r.f32()?, 0.0),
340 MAP_V_OFFSET => m.map.offset[1] = finite(r.f32()?, 0.0),
341 MAP_ROTATION => m.map.rotation = finite(r.f32()?, 0.0),
342 _ => {}
343 }
344 }
345 }
346 _ => {}
347 }
348 }
349 Ok(m)
350}
351
352fn finite(x: f32, default: f32) -> f32 {
353 if x.is_finite() { x } else { default }
354}
355
356fn read_diffuse(body: &[u8], m: &mut Mat) -> Result<()> {
357 for c in chunks(body) {
358 let (id, body) = c?;
359 let mut r = Reader { b: body };
360 let (color, gamma) = match id {
361 COLOR_FLOAT | COLOR_FLOAT_GAMMA => ([r.f32()?, r.f32()?, r.f32()?], id == COLOR_FLOAT_GAMMA),
362 COLOR_BYTE | COLOR_BYTE_GAMMA => {
363 let b = r.take(3)?;
364 ([b[0], b[1], b[2]].map(|c| f32::from(c) / 255.0), id == COLOR_BYTE_GAMMA)
365 }
366 _ => continue,
367 };
368 if m.color.is_none() || (gamma && !m.gamma) {
369 m.color = Some(color.map(|c| if c.is_finite() { c.clamp(0.0, 1.0) } else { 0.0 }));
370 m.gamma = gamma;
371 }
372 }
373 Ok(())
374}
375
376fn read_transparency(body: &[u8], m: &mut Mat) -> Result<()> {
377 for c in chunks(body) {
378 let (id, body) = c?;
379 let mut r = Reader { b: body };
380 let percent = match id {
381 PERCENT_INT => f32::from(r.u16()?) / 100.0,
382 PERCENT_FLOAT => r.f32()?,
383 _ => continue,
384 };
385 if percent.is_finite() {
386 m.opacity = 1.0 - percent.clamp(0.0, 1.0);
387 }
388 }
389 Ok(())
390}
391
392fn build(scene: &Scene) -> Vec<Mesh> {
393 scene.objects.iter().flat_map(|o| build_object(o, &scene.materials)).collect()
394}
395
396struct Face {
398 corners: [usize; 3],
399 smooth: u32,
400}
401
402fn build_object(o: &Object, materials: &[Mat]) -> Vec<Mesh> {
403 let n = o.positions.len();
404 let faces: Vec<Option<Face>> = o
405 .faces
406 .iter()
407 .enumerate()
408 .map(|(i, f)| {
409 let corners = f.map(usize::from);
410 let smooth = o.smoothing.as_ref().and_then(|s| s.get(i)).copied().unwrap_or(0);
411 corners.iter().all(|&c| c < n).then_some(Face { corners, smooth })
412 })
413 .collect();
414 let mut group_of: Vec<Option<usize>> = vec![None; faces.len()];
416 let mut names: Vec<&str> = Vec::new();
417 for (name, list) in &o.groups {
418 let g = names.iter().position(|n| n == name).unwrap_or_else(|| {
419 names.push(name);
420 names.len() - 1
421 });
422 for &f in list {
423 if let Some(slot) = group_of.get_mut(usize::from(f)).filter(|s| s.is_none()) {
424 *slot = Some(g);
425 }
426 }
427 }
428 let uvs = o.uvs.as_deref().filter(|u| u.len() == n);
430 let normals = o.smoothing.is_some().then(|| corner_normals(&o.positions, &faces));
431 let mut meshes = Vec::new();
432 for g in (0..names.len()).map(Some).chain([None]) {
434 let members: Vec<usize> = (0..faces.len()).filter(|&i| group_of[i] == g && faces[i].is_some()).collect();
435 if members.is_empty() {
436 continue;
437 }
438 let mat = g.and_then(|g| materials.iter().find(|m| m.name == names[g]));
439 let material = mat.map(|m| Material {
440 name: m.name.clone(),
441 color: m.color.unwrap_or(DEFAULT_COLOR),
442 opacity: m.opacity,
443 });
444 let mut mesh = match &normals {
445 Some(nrm) => split_mesh(&o.positions, &faces, nrm, uvs, &members),
446 None => shared_mesh(&o.positions, &faces, uvs, &members),
447 };
448 mesh.name = o.name.clone();
449 mesh.material = material;
450 let map = mat.map(|m| m.map).unwrap_or_default();
453 if let Some(uvs) = &mut mesh.uvs {
454 for uv in uvs {
455 *uv = map.apply(*uv);
456 }
457 }
458 mesh.texture = mat.and_then(|m| m.loaded.clone());
459 mesh.surface.blend = mat.is_some_and(|m| (m.opacity - 1.0).abs() > 1e-6);
460 meshes.push(mesh);
461 }
462 meshes
463}
464
465fn shared_mesh(positions: &[V3], faces: &[Option<Face>], uvs: Option<&[[f32; 2]]>, members: &[usize]) -> Mesh {
467 let mut remap: Vec<Option<u32>> = vec![None; positions.len()];
468 let mut mesh = Mesh::default();
469 let mut out_uvs = Vec::new();
470 for f in members.iter().filter_map(|&i| faces[i].as_ref()) {
471 for &c in &f.corners {
472 let slot = &mut remap[c];
473 let i = *slot.get_or_insert_with(|| {
474 mesh.positions.push(positions[c]);
475 out_uvs.extend(uvs.map(|u| u[c]));
476 (mesh.positions.len() - 1) as u32
477 });
478 mesh.indices.push(i);
479 }
480 }
481 mesh.uvs = uvs.map(|_| out_uvs);
482 mesh
483}
484
485fn split_mesh(positions: &[V3], faces: &[Option<Face>], normals: &[[V3; 3]], uvs: Option<&[[f32; 2]]>, members: &[usize]) -> Mesh {
488 let mut mesh = Mesh::default();
489 let mut out_uvs = Vec::with_capacity(if uvs.is_some() { members.len() * 3 } else { 0 });
490 let mut out_normals = Vec::with_capacity(members.len() * 3);
491 for &i in members {
492 let Some(f) = &faces[i] else { continue };
493 for k in 0..3 {
494 mesh.indices.push(mesh.positions.len() as u32);
495 mesh.positions.push(positions[f.corners[k]]);
496 out_normals.push(normals[i][k]);
497 out_uvs.extend(uvs.map(|u| u[f.corners[k]]));
498 }
499 }
500 mesh.normals = Some(out_normals);
501 mesh.uvs = uvs.map(|_| out_uvs);
502 mesh
503}
504
505fn corner_normals(positions: &[V3], faces: &[Option<Face>]) -> Vec<[V3; 3]> {
510 let weld = geom::weld(positions);
511 let tri = |f: &Face| f.corners.map(|c| positions[c]);
512 const MAX_MASKS: usize = 256;
516 let mut at: HashMap<u32, Vec<(u32, V3)>> = HashMap::new();
517 let mut slot: HashMap<(u32, u32), usize> = HashMap::new();
518 for f in faces.iter().flatten() {
519 let [a, b, c] = tri(f);
520 let area_normal = math::cross(math::sub(b, a), math::sub(c, a));
521 for &corner in &f.corners {
522 let place = weld[corner];
523 let list = at.entry(place).or_default();
524 match slot.get(&(place, f.smooth)) {
525 Some(&k) => list[k].1 = math::add(list[k].1, area_normal),
526 None if list.len() < MAX_MASKS => {
527 slot.insert((place, f.smooth), list.len());
528 list.push((f.smooth, area_normal));
529 }
530 None => {}
531 }
532 }
533 }
534 let mut cache: HashMap<(u32, u32), V3> = HashMap::new();
535 faces
536 .iter()
537 .map(|f| {
538 let Some(f) = f else { return [[0.0; 3]; 3] };
539 let [a, b, c] = tri(f);
540 if f.smooth == 0 {
541 return [face_normal(a, b, c); 3];
542 }
543 f.corners.map(|corner| {
544 let place = weld[corner];
545 *cache.entry((place, f.smooth)).or_insert_with(|| {
546 let sum = at.get(&place).into_iter().flatten().filter(|(m, _)| m & f.smooth != 0).fold([0.0; 3], |s, (_, n)| math::add(s, *n));
547 let n = math::normalize(sum);
548 if n == [0.0; 3] { face_normal(a, b, c) } else { n }
549 })
550 })
551 })
552 .collect()
553}
554
555#[cfg(test)]
556mod tests {
557 use super::*;
558
559 fn parse(bytes: &[u8]) -> Result<Model> {
560 super::parse(bytes, &|_| None)
561 }
562
563 #[test]
564 fn texture_next_to_the_file_gives_the_colour() {
565 let mut png = Vec::new();
566 image::RgbImage::from_pixel(2, 2, image::Rgb([0, 0, 255])).write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png).unwrap();
567 let mat = cat(&[chunk(MAT_NAME, &cstr("t")), red_byte(), chunk(MAT_TEXTURE, &chunk(MAT_TEXTURE_FILE, &cstr("BLUE.PNG")))]);
568 let f = file(&[mat], &[("t", all(12))], None);
569 let side = |n: &str| (n == "BLUE.PNG").then(|| png.clone());
570 assert_eq!(super::parse(&f, &side).unwrap().meshes[0].material.as_ref().unwrap().color, [0.0, 0.0, 1.0]);
571 assert_eq!(parse(&f).unwrap().meshes[0].material.as_ref().unwrap().color, [1.0, 0.0, 0.0]);
573 }
574
575 fn corner_uv(i: usize) -> [f32; 2] {
577 [i as f32 * 0.1, i as f32 * 0.05]
578 }
579
580 fn cube_uvs() -> Vec<[f32; 2]> {
581 (0..8).map(corner_uv).collect()
582 }
583
584 fn near(a: [f32; 2], b: [f32; 2]) -> bool {
585 (a[0] - b[0]).abs() < 1e-5 && (a[1] - b[1]).abs() < 1e-5
586 }
587
588 fn uvs_match(m: &Model, want: impl Fn(usize) -> [f32; 2]) -> bool {
591 m.meshes.iter().all(|mesh| {
592 let uvs = mesh.uvs.as_ref().unwrap();
593 uvs.len() == mesh.positions.len() && mesh.positions.iter().zip(uvs).all(|(p, uv)| near(*uv, want((0..8).find(|&i| corner(i) == *p).unwrap())))
594 })
595 }
596
597 fn map_chunk(name: &str, subs: &[Vec<u8>]) -> Vec<u8> {
599 chunk(MAT_TEXTURE, &cat(&[chunk(MAT_TEXTURE_FILE, &cstr(name)), subs.concat()]))
600 }
601
602 fn float_chunk(id: u16, v: f32) -> Vec<u8> {
603 chunk(id, &v.to_le_bytes())
604 }
605
606 #[test]
607 fn mapping_coordinates_are_read_and_flipped() {
608 for smooth in [None, Some(&[1u32; 12][..])] {
610 let m = parse(&file_uv(&[], &[], smooth, &cube_uvs())).unwrap();
611 assert_eq!(m.meshes[0].positions.len(), if smooth.is_some() { 36 } else { 8 });
612 assert!(uvs_match(&m, |i| [corner_uv(i)[0], 1.0 - corner_uv(i)[1]]));
613 assert!(m.meshes[0].texture.is_none());
615 }
616 let m = parse(&file_uv(&[], &[], None, &cube_uvs()[..5])).unwrap();
618 assert!(m.meshes[0].uvs.is_none());
619 let mats = [material("a", red_byte())];
621 let m = parse(&file_uv(&mats, &[("a", all(5))], None, &cube_uvs())).unwrap();
622 assert_eq!(m.meshes.len(), 2);
623 assert!(uvs_match(&m, |i| [corner_uv(i)[0], 1.0 - corner_uv(i)[1]]));
624 }
625
626 #[test]
627 fn texture_map_gives_texture_and_transform() {
628 let png = crate::model3d::texture::tests::png_2x2();
629 let side = |n: &str| (n == "tex.png").then(|| png.clone());
630 let flags = |f: u16| chunk(MAP_TILING, &f.to_le_bytes());
631 let with = |subs: &[Vec<u8>]| {
632 let mat = cat(&[chunk(MAT_NAME, &cstr("t")), red_byte(), map_chunk("tex.png", subs)]);
633 super::parse(&file_uv(&[mat], &[("t", all(12))], None, &cube_uvs()), &side).unwrap()
634 };
635 let m = with(&[flags(0x0010)]);
636 let mesh = &m.meshes[0];
637 let t = mesh.texture.as_ref().unwrap();
638 assert_eq!((t.image.width, t.image.height, t.tint, t.alpha_cutoff), (2, 2, [1.0; 3], None));
639 let avg = mesh.material.as_ref().unwrap().color;
641 assert!((avg[1] - 0.5).abs() < 1e-3 && (avg[0] - 0.5).abs() < 1e-3);
642 assert!(mesh.textured().is_some());
643 let m = with(&[float_chunk(MAP_U_SCALE, 2.0), float_chunk(MAP_V_SCALE, 3.0), float_chunk(MAP_U_OFFSET, 0.25), float_chunk(MAP_V_OFFSET, 0.5)]);
645 assert!(uvs_match(&m, |i| [corner_uv(i)[0] * 2.0 + 0.25, 1.0 - (corner_uv(i)[1] * 3.0 + 0.5)]));
646 let m = with(&[float_chunk(MAP_ROTATION, 90.0)]);
648 assert!(uvs_match(&m, |i| [corner_uv(i)[1], 1.0 + corner_uv(i)[0]]));
649 }
650
651 #[test]
652 fn missing_texture_keeps_the_diffuse_colour_and_the_coordinates() {
653 let mat = cat(&[chunk(MAT_NAME, &cstr("t")), red_byte(), map_chunk("gone.png", &[chunk(MAP_TILING, &0x10u16.to_le_bytes())])]);
654 let m = parse(&file_uv(&[mat], &[("t", all(12))], None, &cube_uvs())).unwrap();
655 let mesh = &m.meshes[0];
656 assert!(mesh.texture.is_none());
657 assert_eq!(mesh.material.as_ref().unwrap().color, [1.0, 0.0, 0.0]);
658 assert!(uvs_match(&m, |i| [corner_uv(i)[0], 1.0 - corner_uv(i)[1]]));
659 }
660
661 #[test]
662 fn texture_outside_the_model_folder_is_not_read() {
663 let png = crate::model3d::texture::tests::png_2x2();
664 let asked = std::cell::RefCell::new(Vec::new());
665 let side = |n: &str| {
666 asked.borrow_mut().push(n.to_string());
667 (!n.contains("..")).then(|| png.clone())
668 };
669 let mat = cat(&[chunk(MAT_NAME, &cstr("t")), red_byte(), map_chunk("../up/tex.png", &[])]);
670 super::parse(&file(&[mat], &[("t", all(12))], None), &side).unwrap();
671 assert!(asked.borrow().iter().all(|n| !n.contains("..")));
672 }
673
674 #[test]
675 fn import_thumbnail_draws_the_red_cube() {
676 let dir = tempfile::tempdir().unwrap();
677 let path = dir.path().join("cube.3ds");
678 std::fs::write(&path, red_cube()).unwrap();
679 let img = super::super::raster::thumbnail(&path, "3ds").unwrap();
680 assert!(img.pixels().any(|p| p[3] == 255 && p[0] > 100 && p[1] < 40 && p[2] < 40));
681 }
682
683 #[test]
684 fn many_smoothing_masks_at_one_vertex_stay_fast() {
685 let faces: Vec<[u16; 3]> = (0..65535u32).map(|i| [0, (i % 6 + 1) as u16, (i % 6 + 2) as u16]).collect();
686 let smooth: Vec<u32> = (0..65535u32).map(|i| i + 1).collect();
687 let mut obj = cstr("fan");
688 obj.extend(chunk(TRI_MESH, &mesh_chunk(&faces, &[], Some(&smooth))));
689 let f = chunk(MAIN, &chunk(EDITOR, &chunk(OBJECT, &obj)));
690 let t = std::time::Instant::now();
691 assert_eq!(parse(&f).unwrap().triangle_count(), 65535);
692 assert!(t.elapsed().as_secs() < 20);
693 }
694
695 fn chunk(id: u16, body: &[u8]) -> Vec<u8> {
696 let mut out = id.to_le_bytes().to_vec();
697 out.extend(((body.len() + 6) as u32).to_le_bytes());
698 out.extend(body);
699 out
700 }
701
702 fn cat(parts: &[Vec<u8>]) -> Vec<u8> {
703 parts.concat()
704 }
705
706 fn cstr(s: &str) -> Vec<u8> {
707 let mut v = s.as_bytes().to_vec();
708 v.push(0);
709 v
710 }
711
712 fn corner(i: usize) -> V3 {
714 [(i & 1) as f32, (i >> 1 & 1) as f32, (i >> 2 & 1) as f32]
715 }
716
717 fn index(axis: usize, s: usize, u: usize, v: usize) -> usize {
718 s << axis | u << ((axis + 1) % 3) | v << ((axis + 2) % 3)
719 }
720
721 fn cube_faces() -> Vec<[u16; 3]> {
724 let mut out = Vec::new();
725 for axis in 0..3 {
726 for s in 0..2 {
727 let p = |u, v| index(axis, s, u, v) as u16;
728 if s == 1 {
729 out.push([p(0, 0), p(1, 0), p(0, 1)]);
730 out.push([p(1, 0), p(1, 1), p(0, 1)]);
731 } else {
732 out.push([p(0, 0), p(0, 1), p(1, 0)]);
733 out.push([p(1, 0), p(0, 1), p(1, 1)]);
734 }
735 }
736 }
737 out
738 }
739
740 fn color_chunk(id: u16, body: &[u8]) -> Vec<u8> {
741 chunk(MAT_DIFFUSE, &chunk(id, body))
742 }
743
744 fn red_byte() -> Vec<u8> {
745 color_chunk(COLOR_BYTE, &[255, 0, 0])
746 }
747
748 fn mesh_chunk(faces: &[[u16; 3]], groups: &[(&str, Vec<u16>)], smooth: Option<&[u32]>) -> Vec<u8> {
749 mesh_chunk_uv(faces, groups, smooth, &[[0.0; 2]; 8])
750 }
751
752 fn mesh_chunk_uv(faces: &[[u16; 3]], groups: &[(&str, Vec<u16>)], smooth: Option<&[u32]>, uvs: &[[f32; 2]]) -> Vec<u8> {
753 let mut verts = 8u16.to_le_bytes().to_vec();
754 for i in 0..8 {
755 verts.extend(corner(i).iter().flat_map(|c| c.to_le_bytes()));
756 }
757 let mut fl = (faces.len() as u16).to_le_bytes().to_vec();
758 for f in faces {
759 fl.extend(f.iter().flat_map(|i| i.to_le_bytes()));
760 fl.extend([0u8; 2]);
761 }
762 for (name, list) in groups {
763 let mut g = cstr(name);
764 g.extend((list.len() as u16).to_le_bytes());
765 g.extend(list.iter().flat_map(|i| i.to_le_bytes()));
766 fl.extend(chunk(FACE_MATERIAL, &g));
767 }
768 if let Some(s) = smooth {
769 fl.extend(chunk(SMOOTHING, &s.iter().flat_map(|m| m.to_le_bytes()).collect::<Vec<_>>()));
770 }
771 let mut uv = (uvs.len() as u16).to_le_bytes().to_vec();
772 uv.extend(uvs.iter().flatten().flat_map(|c| c.to_le_bytes()));
773 cat(&[chunk(VERTICES, &verts), chunk(FACES, &fl), chunk(TEX_COORDS, &uv), chunk(LOCAL_AXES, &[0u8; 48])])
774 }
775
776 fn file(materials: &[Vec<u8>], groups: &[(&str, Vec<u16>)], smooth: Option<&[u32]>) -> Vec<u8> {
777 file_uv(materials, groups, smooth, &[[0.0; 2]; 8])
778 }
779
780 fn file_uv(materials: &[Vec<u8>], groups: &[(&str, Vec<u16>)], smooth: Option<&[u32]>, uvs: &[[f32; 2]]) -> Vec<u8> {
781 let mut obj = cstr("cube");
782 obj.extend(chunk(TRI_MESH, &mesh_chunk_uv(&cube_faces(), groups, smooth, uvs)));
783 let mut editor = chunk(OBJECT, &obj);
784 for m in materials {
785 editor.extend(chunk(MATERIAL, m));
786 }
787 chunk(MAIN, &chunk(EDITOR, &editor))
788 }
789
790 fn material(name: &str, color: Vec<u8>) -> Vec<u8> {
791 cat(&[chunk(MAT_NAME, &cstr(name)), color])
792 }
793
794 fn all(n: u16) -> Vec<u16> {
795 (0..n).collect()
796 }
797
798 fn red_cube() -> Vec<u8> {
799 file(&[material("red", red_byte())], &[("red", all(12))], None)
800 }
801
802 #[test]
803 fn coloured_cube() {
804 let m = parse(&red_cube()).unwrap();
805 assert_eq!(m.meshes.len(), 1);
806 assert_eq!(m.triangle_count(), 12);
807 assert_eq!(m.bounds(), Some(([0.0; 3], [1.0; 3])));
808 let mat = m.meshes[0].material.as_ref().unwrap();
809 assert_eq!((mat.name.as_str(), mat.color, mat.opacity), ("red", [1.0, 0.0, 0.0], 1.0));
810 assert_eq!(m.meshes[0].name, "cube");
811 assert_eq!(m.meshes[0].positions.len(), 8);
812 assert!(m.meshes[0].normals.is_none());
813 }
814
815 #[test]
816 fn colour_forms() {
817 let f32s = |c: [f32; 3]| c.iter().flat_map(|v| v.to_le_bytes()).collect::<Vec<_>>();
818 for (c, want) in [
819 (color_chunk(COLOR_FLOAT, &f32s([0.0, 0.5, 1.0])), [0.0, 0.5, 1.0]),
820 (color_chunk(COLOR_FLOAT_GAMMA, &f32s([1.0, 0.0, 0.25])), [1.0, 0.0, 0.25]),
821 (color_chunk(COLOR_BYTE_GAMMA, &[0, 255, 0]), [0.0, 1.0, 0.0]),
822 ] {
823 let m = parse(&file(&[material("c", c)], &[("c", all(12))], None)).unwrap();
824 assert_eq!(m.meshes[0].material.as_ref().unwrap().color, want);
825 }
826 let both = chunk(MAT_DIFFUSE, &cat(&[chunk(COLOR_BYTE, &[255, 0, 0]), chunk(COLOR_BYTE_GAMMA, &[0, 0, 255])]));
828 let m = parse(&file(&[material("c", both)], &[("c", all(12))], None)).unwrap();
829 assert_eq!(m.meshes[0].material.as_ref().unwrap().color, [0.0, 0.0, 1.0]);
830 }
831
832 #[test]
833 fn transparency_and_texture() {
834 let mut mat = material("glass", red_byte());
835 mat.extend(chunk(MAT_TRANSPARENCY, &chunk(PERCENT_INT, &25u16.to_le_bytes())));
836 let m = parse(&file(&[mat], &[("glass", all(12))], None)).unwrap();
837 assert_eq!(m.meshes[0].material.as_ref().unwrap().opacity, 0.75);
838 assert!(m.meshes[0].surface.blend);
839 let mut mat = material("glass", red_byte());
840 mat.extend(chunk(MAT_TRANSPARENCY, &chunk(PERCENT_FLOAT, &0.5f32.to_le_bytes())));
841 let m = parse(&file(&[mat], &[("glass", all(12))], None)).unwrap();
842 assert_eq!(m.meshes[0].material.as_ref().unwrap().opacity, 0.5);
843 let mat = read_material(&cat(&[chunk(MAT_NAME, &cstr("t")), chunk(MAT_TEXTURE, &chunk(MAT_TEXTURE_FILE, &cstr("wood.jpg")))])).unwrap();
844 assert_eq!(mat.texture.as_deref(), Some("wood.jpg"));
845 }
846
847 #[test]
848 fn blend_follows_transparency() {
849 let transparent = |bytes: Vec<u8>| {
850 let mut mat = material("glass", red_byte());
851 mat.extend(chunk(MAT_TRANSPARENCY, &bytes));
852 mat
853 };
854 for smooth in [None, Some(&[1u32; 12][..])] {
856 let m = parse(&file(&[transparent(chunk(PERCENT_INT, &50u16.to_le_bytes()))], &[("glass", all(12))], smooth)).unwrap();
857 assert!(m.meshes[0].surface.blend);
858 assert!(m.meshes[0].surface.bump_map.is_none() && m.meshes[0].surface.normal_map.is_none());
859 let m = parse(&file(&[transparent(chunk(PERCENT_INT, &0u16.to_le_bytes()))], &[("glass", all(12))], smooth)).unwrap();
860 assert!(!m.meshes[0].surface.blend);
861 let m = parse(&file(&[transparent(chunk(PERCENT_FLOAT, &f32::NAN.to_le_bytes()))], &[("glass", all(12))], smooth)).unwrap();
862 assert!(!m.meshes[0].surface.blend);
863 }
864 assert!(!parse(&red_cube()).unwrap().meshes[0].surface.blend);
865 }
866
867 #[test]
868 fn faces_split_across_materials() {
869 let mats = [material("red", red_byte()), material("blue", color_chunk(COLOR_BYTE, &[0, 0, 255]))];
870 let groups = [("red", all(4)), ("blue", (4..9).collect()), ("ghost", vec![11])];
873 let m = parse(&file(&mats, &groups, None)).unwrap();
874 let counts: Vec<_> = m.meshes.iter().map(|m| (m.material.as_ref().map(|m| m.name.clone()), m.triangle_count())).collect();
875 assert_eq!(counts, [(Some("red".into()), 4), (Some("blue".into()), 5), (None, 1), (None, 2)]);
876 }
877
878 #[test]
879 fn ungrouped_faces_make_a_mesh_without_material() {
880 let m = parse(&file(&[material("red", red_byte())], &[("red", all(4))], None)).unwrap();
881 let counts: Vec<_> = m.meshes.iter().map(|m| (m.material.is_some(), m.triangle_count())).collect();
882 assert_eq!(counts, [(true, 4), (false, 8)]);
883 }
884
885 #[test]
886 fn one_smoothing_group_rounds_the_corners() {
887 let m = parse(&file(&[], &[], Some(&[1; 12]))).unwrap();
888 assert_eq!(m.triangle_count(), 12);
889 assert_eq!(m.meshes[0].positions.len(), 36, "triangles are un-shared");
890 let ns = normals_at(&m, [0.0; 3]);
891 assert_eq!(ns.len(), 3);
892 let d = 1.0 / 3f32.sqrt();
893 for n in ns {
894 for c in n {
895 assert!((c + d).abs() < 1e-4, "{n:?}");
896 }
897 }
898 }
899
900 fn normals_at(m: &Model, p: V3) -> Vec<V3> {
901 let mesh = &m.meshes[0];
902 let n = mesh.normals.as_ref().unwrap();
903 mesh.indices.iter().filter(|&&i| mesh.positions[i as usize] == p).map(|&i| n[i as usize]).collect()
904 }
905
906 #[test]
907 fn distinct_groups_keep_faces_flat() {
908 let smooth: Vec<u32> = (0..12).map(|i| 1 << (i / 2)).collect();
909 check_flat(&parse(&file(&[], &[], Some(&smooth))).unwrap());
910 }
911
912 #[test]
913 fn smoothing_group_zero_is_flat() {
914 check_flat(&parse(&file(&[], &[], Some(&[0; 12]))).unwrap());
915 }
916
917 fn check_flat(m: &Model) {
918 let mesh = &m.meshes[0];
919 let n = mesh.normals.as_ref().unwrap();
920 for t in mesh.indices.as_chunks::<3>().0 {
921 let [a, b, c] = t.map(|i| mesh.positions[i as usize]);
922 let want = face_normal(a, b, c);
923 for &i in t {
924 assert_eq!(n[i as usize], want);
925 }
926 }
927 }
928
929 #[test]
930 fn smoothing_only_joins_faces_sharing_a_bit() {
931 let smooth: Vec<u32> = (0..12).map(|i| (1 << (i / 2 + 1)) | if i / 2 == 2 || i / 2 == 4 { 1 } else { 0 }).collect();
934 let m = parse(&file(&[], &[], Some(&smooth))).unwrap();
935 let at = normals_at(&m, [0.0; 3]);
936 let d = 1.0 / 2f32.sqrt();
937 let rounded = at.iter().filter(|n| n[0].abs() < 1e-4 && (n[1] + d).abs() < 1e-4 && (n[2] + d).abs() < 1e-4).count();
938 assert_eq!(rounded, 2, "{at:?}");
939 assert!(at.iter().any(|n| (n[0] + 1.0).abs() < 1e-4), "the x = 0 side stays flat");
940 }
941
942 #[test]
943 fn truncations_and_corruption_do_not_panic() {
944 let full = file(&[material("red", red_byte())], &[("red", all(12))], Some(&[1; 12]));
945 assert!(parse(&full).is_ok());
946 for n in 0..full.len() {
947 let _ = parse(&full[..n]);
948 }
949 for i in 0..full.len() {
950 let mut f = full.clone();
951 f[i] ^= 0xFF;
952 let _ = parse(&f);
953 }
954 }
955
956 #[test]
957 fn bad_chunk_lengths_are_errors() {
958 for len in [0u32, 1, 5, u32::MAX] {
959 let mut f = red_cube();
960 f[2..6].copy_from_slice(&len.to_le_bytes());
961 assert!(parse(&f).is_err(), "main length {len}");
962 let mut f = red_cube();
964 f[8..12].copy_from_slice(&len.to_le_bytes());
965 assert!(parse(&f).is_err(), "editor length {len}");
966 }
967 }
968
969 #[test]
970 fn huge_counts_are_refused() {
971 let mut verts = 65535u16.to_le_bytes().to_vec();
972 verts.extend([0u8; 24]);
973 assert!(read_vertices(&verts).is_err());
974 let mut o = Object::default();
975 let mut faces = 65535u16.to_le_bytes().to_vec();
976 faces.extend([0u8; 16]);
977 assert!(read_faces(&faces, &mut o).is_err());
978 let mut group = cstr("m");
979 group.extend(65535u16.to_le_bytes());
980 let mut faces = 0u16.to_le_bytes().to_vec();
981 faces.extend(chunk(FACE_MATERIAL, &group));
982 assert!(read_faces(&faces, &mut o).is_err());
983 let mut obj = cstr("x");
985 obj.extend(chunk(TRI_MESH, &chunk(VERTICES, &verts)));
986 let f = chunk(MAIN, &chunk(EDITOR, &chunk(OBJECT, &obj)));
987 assert!(parse(&f).is_err());
988 }
989
990 #[test]
991 fn out_of_range_faces_are_dropped() {
992 let mut o = Object {
993 positions: vec![[0.0; 3], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]],
994 faces: vec![[0, 1, 2], [0, 1, 9]],
995 smoothing: Some(vec![1, 1]),
996 ..Object::default()
997 };
998 let meshes = build_object(&o, &[]);
999 assert_eq!(meshes.len(), 1);
1000 assert_eq!(meshes[0].triangle_count(), 1);
1001 o.smoothing = None;
1002 assert_eq!(build_object(&o, &[])[0].triangle_count(), 1);
1003 }
1004
1005 #[test]
1006 fn not_a_3ds_file() {
1007 assert!(parse(b"hello").is_err());
1008 assert!(parse(b"").is_err());
1009 assert!(parse(&chunk(EDITOR, &[])).is_err());
1010 assert!(parse(&chunk(MAIN, &[])).is_err(), "no triangles");
1011 }
1012
1013 #[test]
1014 fn keyframer_is_skipped() {
1015 let f = red_cube();
1016 let junk = chunk(0xB000, &[0xFF; 40]);
1017 let with = chunk(MAIN, &cat(&[junk.clone(), f[6..].to_vec(), junk]));
1018 assert_eq!(parse(&with).unwrap().triangle_count(), 12);
1019 }
1020}