From 4d9ce83a8823e457f28f7d8d1377eca2db667d4c Mon Sep 17 00:00:00 2001 From: HampusM Date: Tue, 7 May 2024 21:48:12 +0200 Subject: feat(engine): add support for Wavefront obj usemtl & mtllib --- engine/src/file_format/wavefront/common.rs | 40 +++++++++- engine/src/file_format/wavefront/obj.rs | 121 ++++++++++++++++++++++++++++- 2 files changed, 156 insertions(+), 5 deletions(-) diff --git a/engine/src/file_format/wavefront/common.rs b/engine/src/file_format/wavefront/common.rs index 0c5ac33..2e27bcb 100644 --- a/engine/src/file_format/wavefront/common.rs +++ b/engine/src/file_format/wavefront/common.rs @@ -218,11 +218,19 @@ impl Value })?)); } - if value.contains('/') { + if value.contains('/') + && value + .chars() + .all(|character| character.is_ascii_digit() || character == '/') + { return Ok(Self::Triplet(Triplet::parse::(value, line_no)?)); } - if value.contains(|character: char| character.is_ascii_digit()) { + if !value.is_empty() + && value + .chars() + .all(|character: char| character.is_ascii_digit()) + { return Ok(Self::Integer(value.parse().map_err(|err| { ParsingError::InvalidIntegerValue { err, @@ -238,6 +246,18 @@ impl Value Err(ParsingError::UnknownValueType(value.to_string())) } + + pub fn to_text( + &self, + argument_index: usize, + line_no: usize, + ) -> Result<&str, ParsingError> + { + match self { + Value::Text(value) => Ok(value), + _ => Err(ParsingError::UnexpectedArgumentType { argument_index, line_no }), + } + } } #[derive(Debug)] @@ -265,6 +285,22 @@ impl Statement } } + pub fn get_text_arg(&self, index: usize, line_no: usize) + -> Result<&str, ParsingError> + { + match self.arguments.get(index) { + Some(Value::Text(value)) => Ok(value), + Some(_) => Err(ParsingError::UnexpectedArgumentType { + argument_index: index, + line_no, + }), + None => Err(ParsingError::MissingArgument { + keyword: self.keyword.to_string(), + line_no, + }), + } + } + pub fn get_triplet_arg( &self, index: usize, diff --git a/engine/src/file_format/wavefront/obj.rs b/engine/src/file_format/wavefront/obj.rs index 884229c..7e1629a 100644 --- a/engine/src/file_format/wavefront/obj.rs +++ b/engine/src/file_format/wavefront/obj.rs @@ -2,6 +2,8 @@ //! //! File format documentation: +use std::path::PathBuf; + use crate::color::Color; use crate::file_format::wavefront::common::{ keyword, @@ -84,6 +86,22 @@ pub fn parse(obj_content: &str) -> Result }) .collect::, _>>()?; + let material_specifiers = statements + .iter() + .filter_map(|(line_no, statement)| { + if statement.keyword != Keyword::Usemtl { + return None; + } + + let material_name = try_option!(statement.get_text_arg(0, *line_no)); + + Some(Ok(MaterialSpecifier { + material_name: material_name.to_string(), + line_no: *line_no, + })) + }) + .collect::, _>>()?; + let faces = statements .iter() .filter_map(|(line_no, statement)| { @@ -95,15 +113,41 @@ pub fn parse(obj_content: &str) -> Result let vertex_b = try_option!(statement.get_triplet_arg(1, *line_no)).into(); let vertex_c = try_option!(statement.get_triplet_arg(2, *line_no)).into(); - Some(Ok([vertex_a, vertex_b, vertex_c])) + let material_name = + find_material_specifier_for_line_no(&material_specifiers, *line_no) + .map(|material_specifier| material_specifier.material_name.clone()); + + Some(Ok(Face { + vertices: [vertex_a, vertex_b, vertex_c], + material_name, + })) }) - .collect::, _>>()?; + .collect::, _>>()?; + + let mtl_libs = statements + .iter() + .filter_map(|(line_no, statement)| { + if statement.keyword != Keyword::Mtllib { + return None; + } + + let mtl_lib_paths = try_option!(statement + .arguments + .iter() + .enumerate() + .map(|(index, value)| Ok(PathBuf::from(value.to_text(index, *line_no)?))) + .collect::, ParsingError>>()); + + Some(Ok(mtl_lib_paths)) + }) + .collect::, _>>()?; Ok(Obj { vertex_positions, vertex_normals, texture_positions, faces, + mtl_libs, }) } @@ -115,7 +159,8 @@ pub struct Obj pub vertex_positions: Vec>, pub vertex_normals: Vec>, pub texture_positions: Vec>, - pub faces: Vec<[FaceVertex; 3]>, + pub faces: Vec, + pub mtl_libs: Vec>, } impl Obj @@ -125,6 +170,7 @@ impl Obj let vertices = self .faces .iter() + .map(|face| face.vertices.clone()) .flatten() .map(|face_vertex| face_vertex.to_vertex(self)) .collect::, Error>>()?; @@ -134,6 +180,7 @@ impl Obj Some( self.faces .iter() + .map(|face| face.vertices.clone()) .flatten() .enumerate() .map(|(index, _)| { @@ -146,6 +193,14 @@ impl Obj } #[derive(Debug)] +#[non_exhaustive] +pub struct Face +{ + pub vertices: [FaceVertex; 3], + pub material_name: Option, +} + +#[derive(Debug, Clone)] pub struct FaceVertex { pub position: u32, @@ -264,5 +319,65 @@ keyword! { #[keyword(rename = "f")] F, + + #[keyword(rename = "mtllib")] + Mtllib, + + #[keyword(rename = "usemtl")] + Usemtl, + } +} + +#[derive(Debug)] +struct MaterialSpecifier +{ + material_name: String, + line_no: usize, +} + +fn find_material_specifier_for_line_no( + material_specifiers: &[MaterialSpecifier], + line_no: usize, +) -> Option<&MaterialSpecifier> +{ + material_specifiers + .iter() + .rev() + .find(|material_specifier| material_specifier.line_no < line_no) +} + +#[cfg(test)] +mod tests +{ + use super::parse; + + #[test] + fn parse_containing_usemtl_works() + { + let obj = parse(concat!( + "usemtl dark-green\n", + "f 6/8/1 7/5/3 1/3/2\n", + "f 1/2/4 4/1/8 3/4/2\n", + "usemtl light-pink\n", + "f 9/1/7 5/1/2 3/2/7" + )) + .expect("Expected Ok"); + + assert_eq!(obj.faces.len(), 3); + + assert_eq!( + obj.faces[0].material_name.as_ref().expect("Expected Some"), + "dark-green" + ); + + assert_eq!( + obj.faces[1].material_name.as_ref().expect("Expected Some"), + "dark-green" + ); + + assert_eq!( + obj.faces[2].material_name.as_ref().expect("Expected Some"), + "light-pink" + ); } } -- cgit v1.2.3-18-g5258