//! OBJ file format parsing. //! //! File format documentation: use std::path::PathBuf; use crate::file_format::wavefront::common::{ keyword, parse_statement_line, ParsingError, Triplet, }; use crate::mesh::Mesh; use crate::util::try_option; use crate::vector::{Vec2, Vec3}; use crate::vertex::{Builder as VertexBuilder, Vertex}; /// Parses the content of a Wavefront `.obj`. /// /// # Errors /// Will return `Err` if the `.obj` content is formatted incorrectly. pub fn parse(obj_content: &str) -> Result { let lines = obj_content .lines() .enumerate() .map(|(line_index, line)| (line_index + 1, line)); let statements = lines .map(|(line_no, line)| (line_no, parse_statement_line::(line, line_no))) .filter_map(|(line_no, result)| { let opt_statement = match result { Ok(opt_statement) => opt_statement, Err(err) => { return Some(Err(err)); } }; Some(Ok((line_no, opt_statement?))) }) .collect::, _>>()?; let vertex_positions = statements .iter() .filter_map(|(line_no, statement)| { if statement.keyword != Keyword::V { return None; } let x = try_option!(statement.get_float_arg(0, *line_no)); let y = try_option!(statement.get_float_arg(1, *line_no)); let z = try_option!(statement.get_float_arg(2, *line_no)); Some(Ok(Vec3 { x, y, z })) }) .collect::, _>>()?; let texture_positions = statements .iter() .filter_map(|(line_no, statement)| { if statement.keyword != Keyword::Vt { return None; } let u = try_option!(statement.get_float_arg(0, *line_no)); let v = try_option!(statement.get_float_arg(1, *line_no)); // let w = try_option!(statement.get_float_arg(2, *line_no)); Some(Ok(Vec2 { x: u, y: v })) }) .collect::, _>>()?; let vertex_normals = statements .iter() .filter_map(|(line_no, statement)| { if statement.keyword != Keyword::Vn { return None; } let i = try_option!(statement.get_float_arg(0, *line_no)); let j = try_option!(statement.get_float_arg(1, *line_no)); let k = try_option!(statement.get_float_arg(2, *line_no)); Some(Ok(Vec3 { x: i, y: j, z: k })) }) .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)| { if statement.keyword != Keyword::F { return None; } let vertex_a = try_option!(statement.get_triplet_arg(0, *line_no)).into(); 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(); 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::, _>>()?; 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, }) } /// The output from parsing the content of a Wavefront `.obj` using [`parse`]. #[derive(Debug)] #[non_exhaustive] pub struct Obj { pub vertex_positions: Vec>, pub vertex_normals: Vec>, pub texture_positions: Vec>, pub faces: Vec, pub mtl_libs: Vec>, } impl Obj { pub fn to_mesh(&self) -> Result { let vertices = self .faces .iter() .map(|face| face.vertices.clone()) .flatten() .map(|face_vertex| face_vertex.to_vertex(self)) .collect::, Error>>()?; Ok(Mesh::new( vertices, Some( self.faces .iter() .map(|face| face.vertices.clone()) .flatten() .enumerate() .map(|(index, _)| { u32::try_from(index).map_err(|_| Error::FaceIndexTooBig(index)) }) .collect::, _>>()?, ), )) } } #[derive(Debug)] #[non_exhaustive] pub struct Face { pub vertices: [FaceVertex; 3], pub material_name: Option, } #[derive(Debug, Clone)] pub struct FaceVertex { pub position: u32, pub texture: Option, pub normal: Option, } impl FaceVertex { /// Tries to convert this face vertex into a [`Vertex`]. pub fn to_vertex(&self, obj: &Obj) -> Result { let vertex_pos = *obj.vertex_positions.get(self.position as usize - 1).ok_or( Error::FaceVertexPositionNotFound { vertex_pos_index: self.position }, )?; let face_vertex_texture = self.texture.ok_or(Error::NoFaceVertexTextureNotSupported)?; let texture_pos = *obj .texture_positions .get(face_vertex_texture as usize - 1) .ok_or(Error::FaceTexturePositionNotFound { texture_pos_index: face_vertex_texture, })?; let face_vertex_normal = self.normal.ok_or(Error::NoFaceVertexNormalNotSupported)?; let vertex_normal = *obj .vertex_normals .get(face_vertex_normal as usize - 1) .ok_or(Error::FaceVertexNormalNotFound { vertex_normal_index: face_vertex_normal, })?; Ok(VertexBuilder::new() .pos(vertex_pos) .texture_coords(texture_pos) .normal(vertex_normal) .build() .unwrap()) } } impl From for FaceVertex { fn from(triplet: Triplet) -> Self { Self { position: triplet.0, texture: triplet.1, normal: triplet.2, } } } #[derive(Debug, thiserror::Error)] pub enum Error { #[error(transparent)] ParsingError(#[from] ParsingError), #[error( "Face vertex position with index {vertex_pos_index} (1-based) was not found" )] FaceVertexPositionNotFound { vertex_pos_index: u32 }, #[error( "Face texture position with index {texture_pos_index} (1-based) was not found" )] FaceTexturePositionNotFound { texture_pos_index: u32 }, #[error( "Face vertex normal with index {vertex_normal_index} (1-based) was not found" )] FaceVertexNormalNotFound { vertex_normal_index: u32 }, #[error("Face index {0} is too big to fit into a 32-bit integer")] FaceIndexTooBig(usize), #[error("Face vertices without textures are not yet supported")] NoFaceVertexTextureNotSupported, #[error("Face vertices without normals are not yet supported")] NoFaceVertexNormalNotSupported, } keyword! { #[derive(Debug, PartialEq, Eq, Clone, Copy)] enum Keyword { #[keyword(rename = "v")] V, #[keyword(rename = "vn")] Vn, #[keyword(rename = "vt")] Vt, #[keyword(rename = "o")] O, #[keyword(rename = "s")] S, #[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" ); } }