diff --git a/crates/mesh/src/obj.rs b/crates/mesh/src/obj.rs index 1ceba5dfe..092a4e0ff 100644 --- a/crates/mesh/src/obj.rs +++ b/crates/mesh/src/obj.rs @@ -236,6 +236,13 @@ impl Reader<'_> { fn use_material(&mut self, name: &str) -> Result<()> { self.slot = None; + if name.trim().is_empty() { + // A bare `usemtl` selects no named material. In particular, do not create a + // material with an empty name: callers can map the missing material to their own + // default, and writers need not emit a meaningless empty material name. + self.material = None; + return Ok(()); + } self.material = match self.material_ids.get(name) { Some(i) => Some(*i), None if self.scene.materials.len() >= self.limits.max_materials => { @@ -518,6 +525,7 @@ pub fn write(scene: &Scene, mtllib: Option<&str>, limits: &Limits) -> Result> = None; for (index, world) in scene.instances(limits)? { let Some(mesh) = scene.meshes.get(index) else { continue }; mesh.validate()?; @@ -546,8 +554,17 @@ pub fn write(scene: &Scene, mtllib: Option<&str>, limits: &Limits) -> Result { + let _ = writeln!(text, "usemtl {m}"); + } + None if current_material.is_some() => { + let _ = writeln!(text, "usemtl"); + } + None => {} + } + current_material = Some(part.material); } if part.smoothing != smoothing { smoothing = part.smoothing; diff --git a/crates/mesh/src/tests/obj.rs b/crates/mesh/src/tests/obj.rs index 530073f62..9c61773cb 100644 --- a/crates/mesh/src/tests/obj.rs +++ b/crates/mesh/src/tests/obj.rs @@ -125,6 +125,30 @@ fn objects_groups_quads_and_negative_indices() { assert_eq!((s.meshes.len(), s.meshes[0].name.as_str(), s.meshes[0].parts.len()), (1, "", 2)); } +#[test] +fn bare_usemtl_does_not_create_an_empty_named_material() { + for line in ["usemtl", "usemtl "] { + let src = format!("v 0 0 0\nv 1 0 0\nv 0 1 0\n{line}\nf 1 2 3\n"); + let scene = read(src.as_bytes(), &Limits::default(), &mut |_| None).unwrap(); + assert!(scene.materials.is_empty(), "{line:?}: {:?}", scene.materials); + assert_eq!(scene.meshes.iter().map(Mesh::triangle_count).sum::(), 1); + assert_eq!(scene.meshes[0].parts[0].material, None); + scene.validate().unwrap(); + } +} + +#[test] +fn writing_a_bare_usemtl_preserves_a_material_reset() { + let src = b"v 0 0 0\nv 1 0 0\nv 0 1 0\nv 1 1 0\nusemtl Named\nf 1 2 3\nusemtl\nf 2 4 3\n"; + let scene = read(src, &Limits::default(), &mut no_external).unwrap(); + assert_eq!(scene.meshes[0].parts.iter().map(|p| p.material).collect::>(), [Some(0), None]); + + let text = write(&scene, None, &Limits::default()).unwrap(); + assert!(text.contains("usemtl Named\n") && text.contains("usemtl\nf 2 4 3\n"), "{text}"); + let roundtrip = read(text.as_bytes(), &Limits::default(), &mut no_external).unwrap(); + assert_eq!(roundtrip.meshes[0].parts.iter().map(|p| p.material).collect::>(), [Some(0), None]); +} + #[test] fn corner_normals_and_smoothing_groups() { let vertices = "v 0 0 0\nv 1 0 0\nv 0 1 0\nv 0 -0.8660254037844386 0.5\n";