diff options
author | HampusM <hampus@hampusmat.com> | 2024-04-25 20:48:39 +0200 |
---|---|---|
committer | HampusM <hampus@hampusmat.com> | 2024-05-01 19:18:03 +0200 |
commit | 9530d22cf5369ceba369487fff1b85376da64657 (patch) | |
tree | 5beeaa594e77a32877336e119035197e8cd5ac9d | |
parent | 33f7772ddddf2a1c2bfefc50ef39f123df8af3e4 (diff) |
feat(engine): add basic Wavefront obj file parsing
-rw-r--r-- | Cargo.lock | 1 | ||||
-rw-r--r-- | engine/Cargo.toml | 1 | ||||
-rw-r--r-- | engine/src/file_format.rs | 1 | ||||
-rw-r--r-- | engine/src/file_format/wavefront.rs | 3 | ||||
-rw-r--r-- | engine/src/file_format/wavefront/common.rs | 391 | ||||
-rw-r--r-- | engine/src/file_format/wavefront/obj.rs | 270 | ||||
-rw-r--r-- | engine/src/lib.rs | 2 | ||||
-rw-r--r-- | engine/src/util.rs | 24 |
8 files changed, 693 insertions, 0 deletions
@@ -150,6 +150,7 @@ dependencies = [ "gl", "glfw", "image", + "paste", "seq-macro", "thiserror", "tracing", diff --git a/engine/Cargo.toml b/engine/Cargo.toml index 3691996..6d5effe 100644 --- a/engine/Cargo.toml +++ b/engine/Cargo.toml @@ -14,6 +14,7 @@ bitflags = "2.4.0" cstr = "0.2.11" tracing = { version = "0.1.39", optional = true } seq-macro = "0.3.5" +paste = "1.0.14" ecs = { path = "../ecs" } [dependencies.image] diff --git a/engine/src/file_format.rs b/engine/src/file_format.rs new file mode 100644 index 0000000..06c713d --- /dev/null +++ b/engine/src/file_format.rs @@ -0,0 +1 @@ +pub mod wavefront; diff --git a/engine/src/file_format/wavefront.rs b/engine/src/file_format/wavefront.rs new file mode 100644 index 0000000..b23706a --- /dev/null +++ b/engine/src/file_format/wavefront.rs @@ -0,0 +1,3 @@ +pub mod obj; + +mod common; diff --git a/engine/src/file_format/wavefront/common.rs b/engine/src/file_format/wavefront/common.rs new file mode 100644 index 0000000..0c5ac33 --- /dev/null +++ b/engine/src/file_format/wavefront/common.rs @@ -0,0 +1,391 @@ +use std::num::{ParseFloatError, ParseIntError}; + +macro_rules! keyword { + ( + $(#[$attr: meta])* + $visibility: vis enum $name: ident + { + $( + $(#[keyword(rename = $variant_rename: literal)])? + $variant: ident, + )* + } + ) => { + $(#[$attr])* + $visibility enum $name + { + $($variant,)* + } + + impl $crate::file_format::wavefront::common::Keyword for $name + { + fn from_str( + keyword: &str, + line_no: usize + ) -> Result<Keyword, $crate::file_format::wavefront::common::ParsingError> + { + use $crate::file_format::wavefront::common::ParsingError; + + match keyword { + $( + $crate::util::or!( + ($($variant_rename)?) + else (stringify!($variant)) + ) => Ok(Keyword::$variant),)* + _ => Err(ParsingError::UnknownKeyword { + keyword: keyword.to_string(), + line_no, + }), + } + } + } + + impl std::fmt::Display for $name + { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result + { + match self { + $(Self::$variant => write!(formatter, stringify!($variant)),)* + } + } + } + }; +} + +pub(crate) use keyword; + +pub fn parse_statement_line<KeywordT>( + line: &str, + line_no: usize, +) -> Result<Option<Statement<KeywordT>>, ParsingError> +where + KeywordT: Keyword, +{ + if line.is_empty() || line.starts_with('#') { + return Ok(None); + } + + let mut parts = line.split(' '); + let keyword = KeywordT::from_str(parts.next().unwrap(), line_no)?; + + let arguments = parts + .map(|part| Value::parse::<KeywordT>(part, line_no)) + .collect::<Result<Vec<_>, _>>()?; + + Ok(Some(Statement { keyword, arguments })) +} + +#[derive(Debug, thiserror::Error)] +pub enum ParsingError +{ + #[error("Unknown keyword '{keyword}' at line {line_no}")] + UnknownKeyword + { + keyword: String, line_no: usize + }, + + #[error("Invalid float value '{value}'")] + InvalidFloatValue + { + err: ParseFloatError, value: String + }, + + #[error("Invalid integer value '{value}' at line {line_no}")] + InvalidIntegerValue + { + err: ParseIntError, + value: String, + line_no: usize, + }, + + #[error("Invalid type of value '{0}'")] + UnknownValueType(String), + + #[error("Missing triplet value at line {line_no}")] + MissingTripletValue + { + triplet_index: usize, + line_no: usize, + }, + + #[error("Missing argument to {keyword} at {line_no}")] + MissingArgument + { + keyword: String, line_no: usize + }, + + #[error("Unexpected type of argument {argument_index} at line {line_no}")] + UnexpectedArgumentType + { + argument_index: usize, + line_no: usize, + }, +} + +pub trait Keyword: ToString + Sized +{ + fn from_str(keyword: &str, line_no: usize) -> Result<Self, ParsingError>; +} + +type TripletNumber = u32; + +#[derive(Debug, Clone)] +pub struct Triplet( + pub TripletNumber, + pub Option<TripletNumber>, + pub Option<TripletNumber>, +); + +impl Triplet +{ + fn parse<KeywordT: Keyword>( + triplet: &str, + line_no: usize, + ) -> Result<Self, ParsingError> + { + let mut parts = triplet.splitn(3, '/'); + + let first = parts + .next() + .ok_or(ParsingError::MissingTripletValue { triplet_index: 0, line_no }) + .and_then(|value| { + value.parse::<TripletNumber>().map_err(|err| { + ParsingError::InvalidIntegerValue { + err, + value: value.to_string(), + line_no, + } + }) + })?; + + let second = parts + .next() + .ok_or(ParsingError::MissingTripletValue { triplet_index: 0, line_no }) + .and_then(|value| { + if value.is_empty() { + return Ok(None); + } + + Ok(Some(value.parse::<TripletNumber>().map_err(|err| { + ParsingError::InvalidIntegerValue { + err, + value: value.to_string(), + line_no, + } + })?)) + })?; + + let third = parts + .next() + .ok_or(ParsingError::MissingTripletValue { triplet_index: 2, line_no }) + .and_then(|value| { + if value.is_empty() { + return Ok(None); + } + + Ok(Some(value.parse::<TripletNumber>().map_err(|err| { + ParsingError::InvalidIntegerValue { + err, + value: value.to_string(), + line_no, + } + })?)) + })?; + + Ok(Self(first, second, third)) + } +} + +#[derive(Debug)] +pub enum Value +{ + Integer(i32), + Float(f32), + Triplet(Triplet), + Text(String), +} + +impl Value +{ + fn parse<KeywordT: Keyword>(value: &str, line_no: usize) + -> Result<Self, ParsingError> + { + if value.contains(|character: char| character.is_ascii_digit()) + && value.contains('.') + { + return Ok(Self::Float(value.parse().map_err(|err| { + ParsingError::InvalidFloatValue { err, value: value.to_string() } + })?)); + } + + if value.contains('/') { + return Ok(Self::Triplet(Triplet::parse::<KeywordT>(value, line_no)?)); + } + + if value.contains(|character: char| character.is_ascii_digit()) { + return Ok(Self::Integer(value.parse().map_err(|err| { + ParsingError::InvalidIntegerValue { + err, + value: value.to_string(), + line_no, + } + })?)); + } + + if value.chars().all(|character| character.is_ascii()) { + return Ok(Self::Text(value.to_string())); + } + + Err(ParsingError::UnknownValueType(value.to_string())) + } +} + +#[derive(Debug)] +pub struct Statement<KeywordT: Keyword> +{ + pub keyword: KeywordT, + pub arguments: Vec<Value>, +} + +impl<KeywordT: Keyword> Statement<KeywordT> +{ + pub fn get_float_arg(&self, index: usize, line_no: usize) + -> Result<f32, ParsingError> + { + match self.arguments.get(index) { + Some(Value::Float(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, + line_no: usize, + ) -> Result<Triplet, ParsingError> + { + match self.arguments.get(index) { + Some(Value::Triplet(value)) => Ok(value.clone()), + Some(_) => Err(ParsingError::UnexpectedArgumentType { + argument_index: index, + line_no, + }), + None => Err(ParsingError::MissingArgument { + keyword: self.keyword.to_string(), + line_no, + }), + } + } +} + +#[cfg(test)] +mod tests +{ + use std::fmt::Display; + + use super::{parse_statement_line, Value}; + use crate::file_format::wavefront::common::ParsingError; + + #[derive(Debug, PartialEq, Eq)] + enum Keyword + { + V, + Vn, + F, + } + + impl super::Keyword for Keyword + { + fn from_str(keyword: &str, _line_no: usize) -> Result<Self, ParsingError> + { + Ok(match keyword { + "v" => Self::V, + "vn" => Self::Vn, + "f" => Self::F, + _ => { + panic!("Unknown keyword '{keyword}'"); + } + }) + } + } + + impl Display for Keyword + { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result + { + match self { + Self::V => write!(formatter, "v"), + Self::Vn => write!(formatter, "vn"), + Self::F => write!(formatter, "f"), + } + } + } + + #[test] + fn parse_statement_line_works() + { + assert!(parse_statement_line::<Keyword>("", 1) + .is_ok_and(|statement| statement.is_none())); + + let res = parse_statement_line::<Keyword>("v", 3); + + assert!(matches!(res, Ok(Some(statement)) if statement.keyword == Keyword::V)); + } + + #[test] + fn parse_statement_line_with_args_works() + { + let statement = parse_statement_line::<Keyword>("vn 45 86 11", 3) + .expect("Expected Ok") + .expect("Expected Some"); + + assert_eq!(statement.keyword, Keyword::Vn); + + assert!(matches!(statement.arguments[0], Value::Integer(45))); + assert!(matches!(statement.arguments[1], Value::Integer(86))); + assert!(matches!(statement.arguments[2], Value::Integer(11))); + } + + #[test] + fn parse_statement_line_with_triplet_arg() + { + let statement = parse_statement_line::<Keyword>("f 67/925/23", 1) + .expect("Expected Ok") + .expect("Expected Some"); + + assert_eq!(statement.keyword, Keyword::F); + + let Value::Triplet(triplet) = &statement.arguments[0] else { + panic!("Argument is not a triplet"); + }; + + assert_eq!(triplet.0, 67); + assert_eq!(triplet.1, Some(925)); + assert_eq!(triplet.2, Some(23)); + } + + #[test] + fn parse_statement_line_with_two_value_triplet_arg() + { + let statement = parse_statement_line::<Keyword>("v 102//336", 1) + .expect("Expected Ok") + .expect("Expected Some"); + + assert_eq!(statement.keyword, Keyword::V); + + let Value::Triplet(triplet) = &statement.arguments[0] else { + panic!("Argument is not a triplet"); + }; + + assert_eq!(triplet.0, 102); + assert!(triplet.1.is_none()); + assert_eq!(triplet.2, Some(336)); + } +} diff --git a/engine/src/file_format/wavefront/obj.rs b/engine/src/file_format/wavefront/obj.rs new file mode 100644 index 0000000..d60b41d --- /dev/null +++ b/engine/src/file_format/wavefront/obj.rs @@ -0,0 +1,270 @@ +//! OBJ file format parsing. +//! +//! File format documentation: <https://paulbourke.net/dataformats/obj> + +use crate::color::Color; +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}; + +/// The output from parsing the content of a Wavefront `.obj` using [`parse`]. +#[derive(Debug)] +#[non_exhaustive] +pub struct Obj +{ + pub mesh: Mesh, +} + +/// 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<Obj, Error> +{ + 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::<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 = 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::<Result<Vec<_>, _>>()?; + + 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::<Result<Vec<_>, _>>()?; + + 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::<Result<Vec<_>, _>>()?; + + 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(); + + Some(Ok([vertex_a, vertex_b, vertex_c])) + }) + .collect::<Result<Vec<[FaceVertex; 3]>, _>>()?; + + let vertices = faces + .iter() + .flatten() + .map(|face_vertex| { + face_vertex_to_vertex( + face_vertex, + Data { + vertex_positions: &vertex_positions, + texture_positions: &texture_positions, + vertex_normals: &vertex_normals, + }, + ) + }) + .collect::<Result<Vec<_>, Error>>()?; + + Ok(Obj { + mesh: Mesh::new( + vertices, + Some( + faces + .iter() + .flatten() + .enumerate() + .map(|(index, _)| { + u32::try_from(index).map_err(|_| Error::FaceIndexTooBig(index)) + }) + .collect::<Result<Vec<_>, _>>()?, + ), + ), + }) +} + +struct Data<'a> +{ + vertex_positions: &'a [Vec3<f32>], + texture_positions: &'a [Vec2<f32>], + vertex_normals: &'a [Vec3<f32>], +} + +fn face_vertex_to_vertex(face_vertex: &FaceVertex, data: Data) -> Result<Vertex, Error> +{ + let vertex_pos = *data + .vertex_positions + .get(face_vertex.position as usize - 1) + .ok_or(Error::FaceVertexPositionNotFound { + vertex_pos_index: face_vertex.position, + })?; + + let face_vertex_texture = face_vertex + .texture + .ok_or(Error::NoFaceVertexTextureNotSupported)?; + + let texture_pos = *data + .texture_positions + .get(face_vertex_texture as usize - 1) + .ok_or(Error::FaceTexturePositionNotFound { + texture_pos_index: face_vertex_texture, + })?; + + let face_vertex_normal = face_vertex + .normal + .ok_or(Error::NoFaceVertexNormalNotSupported)?; + + let vertex_normal = *data + .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) + .color(Color::WHITE_F32) + .texture_coords(texture_pos) + .normal(vertex_normal) + .build() + .unwrap()) +} + +#[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, + } +} + +#[derive(Debug)] +struct FaceVertex +{ + position: u32, + texture: Option<u32>, + normal: Option<u32>, +} + +impl From<Triplet> for FaceVertex +{ + fn from(triplet: Triplet) -> Self + { + Self { + position: triplet.0, + texture: triplet.1, + normal: triplet.2, + } + } +} diff --git a/engine/src/lib.rs b/engine/src/lib.rs index b3f4aa1..f9daac3 100644 --- a/engine/src/lib.rs +++ b/engine/src/lib.rs @@ -19,11 +19,13 @@ use crate::event::{ mod opengl; mod shader_preprocessor; +mod util; pub mod camera; pub mod data_types; pub mod delta_time; pub mod event; +pub mod file_format; pub mod input; pub mod lighting; pub mod material; diff --git a/engine/src/util.rs b/engine/src/util.rs new file mode 100644 index 0000000..021b2fe --- /dev/null +++ b/engine/src/util.rs @@ -0,0 +1,24 @@ +macro_rules! try_option { + ($expr: expr) => { + match $expr { + Ok(value) => value, + Err(err) => { + return Some(Err(err)); + } + } + }; +} + +pub(crate) use try_option; + +macro_rules! or { + (($($tt: tt)+) else ($($else_tt: tt)*)) => { + $($tt)+ + }; + + (() else ($($else_tt: tt)*)) => { + $($else_tt)* + }; +} + +pub(crate) use or; |