diff options
Diffstat (limited to 'engine/src/file_format/wavefront/obj.rs')
| -rw-r--r-- | engine/src/file_format/wavefront/obj.rs | 733 |
1 files changed, 418 insertions, 315 deletions
diff --git a/engine/src/file_format/wavefront/obj.rs b/engine/src/file_format/wavefront/obj.rs index 88d566c..d1a1bf3 100644 --- a/engine/src/file_format/wavefront/obj.rs +++ b/engine/src/file_format/wavefront/obj.rs @@ -6,15 +6,23 @@ use std::collections::HashMap; use std::fs::read_to_string; use std::path::PathBuf; +use engine_ecs::util::Either; + use crate::file_format::wavefront::common::{ keyword, parse_statement_line, + Keyword as _, ParsingError, Statement, Triplet, }; -use crate::mesh::{Mesh, Vertex}; -use crate::util::try_option; +use crate::mesh::vertex_buffer::{ + NamedVertexAttr, + VertexAttrInfo, + VertexBuffer as MeshVertexBuffer, + VertexLabel, +}; +use crate::mesh::{Mesh, VertexAttrType}; use crate::vector::{Vec2, Vec3}; /// Parses the content of a Wavefront `.obj`. @@ -28,34 +36,37 @@ pub fn parse(obj_content: &str) -> Result<Obj, Error> .enumerate() .map(|(line_index, line)| (line_index + 1, line)); - let statements = lines - .map(|(line_no, line)| (line_no, parse_statement_line::<Keyword>(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::<Result<Vec<_>, _>>()?; - - let vertex_positions = get_vertex_positions_from_statements(&statements)?; - let texture_positions = get_texture_positions_from_statements(&statements)?; - let vertex_normals = get_vertex_normals_from_statements(&statements)?; - let material_specs = get_material_specs_from_statements(&statements)?; - let faces = get_faces_from_statements(&statements, &material_specs)?; - let mtl_libs = get_mtl_libs_from_statements(&statements)?; - - Ok(Obj { - vertex_positions, - vertex_normals, - texture_positions, - faces, - mtl_libs, - }) + let mut item_counts = ItemCounts::default(); + + for (line_index, line) in obj_content.lines().enumerate() { + if line.is_empty() || line.starts_with('#') { + continue; + } + + let Some((keyword, _)) = line.split_once(' ') else { + continue; + }; + + let Ok(keyword) = Keyword::from_str(keyword, line_index + 1) else { + continue; + }; + + item_counts.increment_item_cnt_by_keyword(keyword); + } + + let mut obj = Obj::with_capacities_from_item_cnts(item_counts); + + let mut parsing_state = ParsingState::default(); + + for (line_no, line) in lines { + let Some(statement) = parse_statement_line::<Keyword>(line, line_no)? else { + continue; + }; + + obj.handle_statement(line_no, statement, &mut parsing_state)?; + } + + Ok(obj) } /// The data of a Wavefront object file. @@ -67,7 +78,8 @@ pub struct Obj pub vertex_normals: Vec<Vec3<f32>>, pub texture_positions: Vec<Vec2<f32>>, pub faces: Vec<Face>, - pub mtl_libs: Vec<Vec<PathBuf>>, + pub mtl_libs: Vec<PathBuf>, + pub unique_used_material_names: Vec<Box<str>>, } impl Obj @@ -80,9 +92,26 @@ impl Obj /// - A face's texture position cannot be found /// - A face's vertex normal cannot be found /// - A face index does not fit in a [`u32`] - pub fn to_mesh(&self) -> Result<Mesh, Error> + pub fn to_mesh(&self, options: ToMeshOptions) -> Result<Mesh, Error> { - let mut vertices = Vec::<Vertex>::with_capacity(self.faces.len() * 3); + let mut vertex_buf = MeshVertexBuffer::with_capacity( + &[ + VertexAttrInfo { + label: VertexLabel::Position, + ty: VertexAttrType::Float32Array { length: 3 }, + }, + VertexAttrInfo { + label: VertexLabel::UvFromTopLeft, + ty: VertexAttrType::Float32Array { length: 2 }, + }, + VertexAttrInfo { + label: VertexLabel::Normal, + ty: VertexAttrType::Float32Array { length: 3 }, + }, + ], + self.faces.len() * 3, + ); + let mut indices = Vec::<u32>::with_capacity(self.faces.len() * 3); let mut added_face_vertices = @@ -90,16 +119,86 @@ impl Obj for face in &self.faces { for face_vertex in &face.vertices { - if let Some(index) = added_face_vertices.get(&face_vertex) { + if let Some(index) = added_face_vertices.get(face_vertex) { indices.push(*index); continue; } - vertices.push(face_vertex.to_vertex(self)?); + let pos = *self + .vertex_positions + .get(face_vertex.position as usize - 1) + .ok_or(Error::FaceVertexPositionNotFound { + vertex_pos_index: face_vertex.position, + })?; + + let texture_pos = face_vertex.texture.map_or_else( + || { + if !self.texture_positions.is_empty() { + tracing::warn!(concat!( + "Wavefront OBJ has texture coordinates ", + "but face vertex does not specify one" + )); + } + + Ok(Vec2::default()) + }, + |face_vertex_texture| { + self.texture_positions + .get(face_vertex_texture as usize - 1) + .ok_or(Error::FaceTexturePositionNotFound { + texture_pos_index: face_vertex_texture, + }) + .cloned() + }, + )?; + + let texture_pos = if options.y_flip_uvs { + Vec2 { x: texture_pos.x, y: -texture_pos.y } + } else { + texture_pos + }; + + let normal = face_vertex.normal.map_or_else( + || { + if !self.vertex_normals.is_empty() { + tracing::warn!(concat!( + "Wavefront OBJ has normals ", + "but face vertex does not specify one" + )); + } + + Ok(Vec3::default()) + }, + |face_vertex_normal| { + self.vertex_normals + .get(face_vertex_normal as usize - 1) + .ok_or(Error::FaceVertexNormalNotFound { + vertex_normal_index: face_vertex_normal, + }) + .cloned() + }, + )?; + + vertex_buf.push(( + NamedVertexAttr { + label: VertexLabel::Position, + value: pos.into_array(), + }, + NamedVertexAttr { + label: VertexLabel::UvFromTopLeft, + value: texture_pos.into_array(), + }, + NamedVertexAttr { + label: VertexLabel::Normal, + value: normal.into_array(), + }, + )); + + let vertex_index = vertex_buf.len() - 1; - let vertex_index = u32::try_from(vertices.len() - 1) - .map_err(|_| Error::FaceIndexTooBig(vertices.len() - 1))?; + let vertex_index = u32::try_from(vertex_index) + .map_err(|_| Error::FaceIndexTooBig(vertex_index))?; indices.push(vertex_index); @@ -107,7 +206,10 @@ impl Obj } } - Ok(Mesh::new(vertices, Some(indices))) + Ok(Mesh::builder() + .vertices(vertex_buf) + .indices(indices) + .build()) } /// Reads and parses the material libraries of this `Obj`. @@ -123,7 +225,6 @@ impl Obj { self.mtl_libs .iter() - .flatten() .map(|mtl_lib| { Ok(parse_material_lib(&read_to_string(mtl_lib).map_err( |err| MaterialLibsError::ReadingMaterialLibFailed { @@ -138,6 +239,238 @@ impl Obj }) .collect::<Result<Vec<_>, _>>() } + + fn with_capacities_from_item_cnts(item_counts: ItemCounts) -> Self + { + Self { + vertex_positions: Vec::with_capacity(item_counts.pos), + vertex_normals: Vec::with_capacity(item_counts.normal), + texture_positions: Vec::with_capacity(item_counts.uv), + faces: Vec::with_capacity(item_counts.face), + mtl_libs: Vec::with_capacity(item_counts.mtl_lib), + unique_used_material_names: Vec::with_capacity(item_counts.material_usage), + } + } + + fn handle_statement( + &mut self, + line_no: usize, + statement: Statement<Keyword>, + parsing_state: &mut ParsingState, + ) -> Result<(), Error> + { + match statement.keyword { + Keyword::V => self.handle_position_statement(line_no, statement), + Keyword::Vn => self.handle_normal_statement(line_no, statement), + Keyword::Vt => self.handle_uv_statement(line_no, statement), + Keyword::F => self.handle_face_statement(line_no, statement, parsing_state), + Keyword::Mtllib => self.handle_material_lib_statement(line_no, statement), + Keyword::Usemtl => { + self.handle_material_usage_statement(line_no, statement, parsing_state) + } + Keyword::O | Keyword::S => Ok(()), + } + } + + fn handle_position_statement( + &mut self, + line_no: usize, + statement: Statement<Keyword>, + ) -> Result<(), Error> + { + debug_assert_eq!(statement.keyword, Keyword::V); + + if statement.arguments.len() == 4 { + return Err(Error::UnsupportedArgumentCount { + keyword: statement.keyword.to_string(), + arg_count: statement.arguments.len(), + line_no, + }); + } + + if statement.arguments.len() > 4 { + return Err(Error::InvalidArgumentCount { + keyword: statement.keyword.to_string(), + arg_count: statement.arguments.len(), + line_no, + }); + } + + let x = statement.get_float_arg(0, line_no)?; + let y = statement.get_float_arg(1, line_no)?; + let z = statement.get_float_arg(2, line_no)?; + + self.vertex_positions.push(Vec3 { x, y, z }); + + Ok(()) + } + + fn handle_uv_statement( + &mut self, + line_no: usize, + statement: Statement<Keyword>, + ) -> Result<(), Error> + { + debug_assert_eq!(statement.keyword, Keyword::Vt); + + if statement.arguments.len() == 3 { + return Err(Error::UnsupportedArgumentCount { + keyword: statement.keyword.to_string(), + arg_count: statement.arguments.len(), + line_no, + }); + } + + if statement.arguments.len() > 3 { + return Err(Error::InvalidArgumentCount { + keyword: statement.keyword.to_string(), + arg_count: statement.arguments.len(), + line_no, + }); + } + + let u = statement.get_float_arg(0, line_no)?; + let v = statement.get_float_arg(1, line_no)?; + + self.texture_positions.push(Vec2 { x: u, y: v }); + + Ok(()) + } + + fn handle_normal_statement( + &mut self, + line_no: usize, + statement: Statement<Keyword>, + ) -> Result<(), Error> + { + debug_assert_eq!(statement.keyword, Keyword::Vn); + + if statement.arguments.len() > 3 { + return Err(Error::InvalidArgumentCount { + keyword: statement.keyword.to_string(), + arg_count: statement.arguments.len(), + line_no, + }); + } + + let i = statement.get_float_arg(0, line_no)?; + let j = statement.get_float_arg(1, line_no)?; + let k = statement.get_float_arg(2, line_no)?; + + self.vertex_normals.push(Vec3 { x: i, y: j, z: k }); + + Ok(()) + } + + fn handle_material_usage_statement( + &mut self, + line_no: usize, + statement: Statement<Keyword>, + parsing_state: &mut ParsingState, + ) -> Result<(), Error> + { + debug_assert_eq!(statement.keyword, Keyword::Usemtl); + + if statement.arguments.len() > 1 { + return Err(Error::InvalidArgumentCount { + keyword: statement.keyword.to_string(), + arg_count: statement.arguments.len(), + line_no, + }); + } + + let material_name = statement + .get_text_arg(0, line_no)? + .to_owned() + .into_boxed_str(); + + if !self.unique_used_material_names.contains(&material_name) { + self.unique_used_material_names.push(material_name.clone()); + } + + parsing_state.current_material_name = Some(material_name); + + Ok(()) + } + + fn handle_face_statement( + &mut self, + line_no: usize, + statement: Statement<Keyword>, + parsing_state: &mut ParsingState, + ) -> Result<(), Error> + { + debug_assert_eq!(statement.keyword, Keyword::F); + + if statement.arguments.len() > 3 { + return Err(Error::UnsupportedArgumentCount { + keyword: statement.keyword.to_string(), + arg_count: statement.arguments.len(), + line_no, + }); + } + + let vertex_a = statement.get_triplet_arg(0, line_no)?; + let vertex_b = statement.get_triplet_arg(1, line_no)?; + let vertex_c = statement.get_triplet_arg(2, line_no)?; + + self.faces.push(Face { + vertices: [vertex_a.into(), vertex_b.into(), vertex_c.into()], + material_name: parsing_state.current_material_name.clone(), + }); + + Ok(()) + } + + fn handle_material_lib_statement( + &mut self, + line_no: usize, + statement: Statement<Keyword>, + ) -> Result<(), Error> + { + debug_assert_eq!(statement.keyword, Keyword::Mtllib); + + for (index, value) in statement.arguments.iter().enumerate() { + let mtl_lib = PathBuf::from(value.to_text(index, line_no)?); + + self.mtl_libs.push(mtl_lib); + } + + Ok(()) + } +} + +#[derive(Debug, Clone)] +#[non_exhaustive] +pub struct ToMeshOptions +{ + /// Whether all UVs should be flipped on the Y axis. This is necessary if the UVs + /// have their origin at the bottom left corner of the image + /// + /// The default is `true`. + pub y_flip_uvs: bool, +} + +impl ToMeshOptions +{ + /// Whether all UVs should be flipped on the Y axis. This is necessary if the UVs have + /// their origin at the bottom left corner of the image + /// + /// The default is `true`. + #[must_use] + pub fn y_flip_uvs(mut self, y_flip_uvs: bool) -> Self + { + self.y_flip_uvs = y_flip_uvs; + self + } +} + +impl Default for ToMeshOptions +{ + fn default() -> Self + { + Self { y_flip_uvs: true } + } } #[derive(Debug)] @@ -145,7 +478,7 @@ impl Obj pub struct Face { pub vertices: [FaceVertex; 3], - pub material_name: Option<String>, + pub material_name: Option<Box<str>>, } #[derive(Debug, Clone, PartialEq, Eq, Hash)] @@ -156,51 +489,6 @@ pub struct FaceVertex pub normal: Option<u32>, } -impl FaceVertex -{ - /// Tries to convert this face vertex into a [`Vertex`]. - /// - /// # Errors - /// Returns `Err` if: - /// - The face's vertex position cannot be found in the given [`Obj`] - /// - The face's texture position cannot be found in the given [`Obj`] - /// - The face's vertex normal cannot be found in the given [`Obj`] - pub fn to_vertex(&self, obj: &Obj) -> Result<Vertex, Error> - { - let mut vertex_builder = Vertex::builder(); - - let vertex_pos = *obj.vertex_positions.get(self.position as usize - 1).ok_or( - Error::FaceVertexPositionNotFound { vertex_pos_index: self.position }, - )?; - - vertex_builder = vertex_builder.pos(vertex_pos); - - if let Some(face_vertex_texture) = self.texture { - let texture_pos = obj - .texture_positions - .get(face_vertex_texture as usize - 1) - .ok_or(Error::FaceTexturePositionNotFound { - texture_pos_index: face_vertex_texture, - })?; - - vertex_builder = vertex_builder.texture_coords(*texture_pos); - } - - if let Some(face_vertex_normal) = self.normal { - let vertex_normal = *obj - .vertex_normals - .get(face_vertex_normal as usize - 1) - .ok_or(Error::FaceVertexNormalNotFound { - vertex_normal_index: face_vertex_normal, - })?; - - vertex_builder = vertex_builder.normal(vertex_normal); - } - - Ok(vertex_builder.build()) - } -} - impl From<Triplet> for FaceVertex { fn from(triplet: Triplet) -> Self @@ -280,6 +568,39 @@ pub enum MaterialLibsError<MaterialLibParseError> }, } +#[derive(Debug, Default)] +struct ParsingState +{ + current_material_name: Option<Box<str>>, +} + +#[derive(Debug, Default)] +struct ItemCounts +{ + pos: usize, + uv: usize, + normal: usize, + face: usize, + mtl_lib: usize, + material_usage: usize, +} + +impl ItemCounts +{ + fn increment_item_cnt_by_keyword(&mut self, keyword: Keyword) + { + match keyword { + Keyword::V => self.pos += 1, + Keyword::Vn => self.normal += 1, + Keyword::Vt => self.uv += 1, + Keyword::F => self.face += 1, + Keyword::Mtllib => self.mtl_lib += 1, + Keyword::Usemtl => self.material_usage += 1, + Keyword::O | Keyword::S => {} + } + } +} + keyword! { #[derive(Debug, PartialEq, Eq, Clone, Copy)] enum Keyword { @@ -309,233 +630,6 @@ keyword! { } } -#[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) -} - -enum Either<ValA, ValB> -{ - A(ValA), - B(ValB), -} - -impl<ValA, ValB, Item> Iterator for Either<ValA, ValB> -where - ValA: Iterator<Item = Item>, - ValB: Iterator<Item = Item>, -{ - type Item = Item; - - fn next(&mut self) -> Option<Self::Item> - { - match self { - Self::A(iter_a) => iter_a.next(), - Self::B(iter_b) => iter_b.next(), - } - } -} - -fn get_vertex_positions_from_statements( - statements: &[(usize, Statement<Keyword>)], -) -> Result<Vec<Vec3<f32>>, Error> -{ - statements - .iter() - .filter_map(|(line_no, statement)| { - if statement.keyword != Keyword::V { - return None; - } - - if statement.arguments.len() == 4 { - return Some(Err(Error::UnsupportedArgumentCount { - keyword: statement.keyword.to_string(), - arg_count: statement.arguments.len(), - line_no: *line_no, - })); - } - - if statement.arguments.len() > 4 { - return Some(Err(Error::InvalidArgumentCount { - keyword: statement.keyword.to_string(), - arg_count: statement.arguments.len(), - line_no: *line_no, - })); - } - - 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::<Result<Vec<_>, Error>>() -} - -fn get_texture_positions_from_statements( - statements: &[(usize, Statement<Keyword>)], -) -> Result<Vec<Vec2<f32>>, Error> -{ - statements - .iter() - .filter_map(|(line_no, statement)| { - if statement.keyword != Keyword::Vt { - return None; - } - - if statement.arguments.len() == 3 { - return Some(Err(Error::UnsupportedArgumentCount { - keyword: statement.keyword.to_string(), - arg_count: statement.arguments.len(), - line_no: *line_no, - })); - } - - if statement.arguments.len() > 3 { - return Some(Err(Error::InvalidArgumentCount { - keyword: statement.keyword.to_string(), - arg_count: statement.arguments.len(), - line_no: *line_no, - })); - } - - let u = try_option!(statement.get_float_arg(0, *line_no)); - let v = try_option!(statement.get_float_arg(1, *line_no)); - - Some(Ok(Vec2 { x: u, y: v })) - }) - .collect::<Result<Vec<_>, Error>>() -} - -fn get_vertex_normals_from_statements( - statements: &[(usize, Statement<Keyword>)], -) -> Result<Vec<Vec3<f32>>, Error> -{ - statements - .iter() - .filter_map(|(line_no, statement)| { - if statement.keyword != Keyword::Vn { - return None; - } - - if statement.arguments.len() > 3 { - return Some(Err(Error::InvalidArgumentCount { - keyword: statement.keyword.to_string(), - arg_count: statement.arguments.len(), - line_no: *line_no, - })); - } - - 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::<Result<Vec<_>, Error>>() -} - -fn get_material_specs_from_statements( - statements: &[(usize, Statement<Keyword>)], -) -> Result<Vec<MaterialSpecifier>, Error> -{ - statements - .iter() - .filter_map(|(line_no, statement)| { - if statement.keyword != Keyword::Usemtl { - return None; - } - - if statement.arguments.len() > 1 { - return Some(Err(Error::InvalidArgumentCount { - keyword: statement.keyword.to_string(), - arg_count: statement.arguments.len(), - line_no: *line_no, - })); - } - - 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::<Result<Vec<_>, Error>>() -} - -fn get_faces_from_statements( - statements: &[(usize, Statement<Keyword>)], - material_specifiers: &[MaterialSpecifier], -) -> Result<Vec<Face>, Error> -{ - statements - .iter() - .filter_map(|(line_no, statement)| { - if statement.keyword != Keyword::F { - return None; - } - - if statement.arguments.len() > 3 { - return Some(Err(Error::UnsupportedArgumentCount { - keyword: statement.keyword.to_string(), - arg_count: statement.arguments.len(), - line_no: *line_no, - })); - } - - 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::<Result<Vec<Face>, Error>>() -} - -fn get_mtl_libs_from_statements( - statements: &[(usize, Statement<Keyword>)], -) -> Result<Vec<Vec<PathBuf>>, Error> -{ - 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::<Result<Vec<_>, ParsingError>>()); - - Some(Ok(mtl_lib_paths)) - }) - .collect::<Result<Vec<_>, Error>>() -} - #[cfg(test)] mod tests { @@ -556,17 +650,26 @@ mod tests assert_eq!(obj.faces.len(), 3); assert_eq!( - obj.faces[0].material_name.as_ref().expect("Expected Some"), + obj.faces[0] + .material_name + .as_deref() + .expect("Expected Some"), "dark-green" ); assert_eq!( - obj.faces[1].material_name.as_ref().expect("Expected Some"), + obj.faces[1] + .material_name + .as_deref() + .expect("Expected Some"), "dark-green" ); assert_eq!( - obj.faces[2].material_name.as_ref().expect("Expected Some"), + obj.faces[2] + .material_name + .as_deref() + .expect("Expected Some"), "light-pink" ); } |
