summaryrefslogtreecommitdiff
path: root/engine/src
diff options
context:
space:
mode:
Diffstat (limited to 'engine/src')
-rw-r--r--engine/src/file_format.rs1
-rw-r--r--engine/src/file_format/wavefront.rs3
-rw-r--r--engine/src/file_format/wavefront/common.rs391
-rw-r--r--engine/src/file_format/wavefront/obj.rs270
-rw-r--r--engine/src/lib.rs2
-rw-r--r--engine/src/util.rs24
6 files changed, 691 insertions, 0 deletions
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;