From 9530d22cf5369ceba369487fff1b85376da64657 Mon Sep 17 00:00:00 2001
From: HampusM <hampus@hampusmat.com>
Date: Thu, 25 Apr 2024 20:48:39 +0200
Subject: feat(engine): add basic Wavefront obj file parsing

---
 engine/src/file_format.rs                  |   1 +
 engine/src/file_format/wavefront.rs        |   3 +
 engine/src/file_format/wavefront/common.rs | 391 +++++++++++++++++++++++++++++
 engine/src/file_format/wavefront/obj.rs    | 270 ++++++++++++++++++++
 engine/src/lib.rs                          |   2 +
 engine/src/util.rs                         |  24 ++
 6 files changed, 691 insertions(+)
 create mode 100644 engine/src/file_format.rs
 create mode 100644 engine/src/file_format/wavefront.rs
 create mode 100644 engine/src/file_format/wavefront/common.rs
 create mode 100644 engine/src/file_format/wavefront/obj.rs
 create mode 100644 engine/src/util.rs

(limited to 'engine/src')

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;
-- 
cgit v1.2.3-18-g5258