summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--engine/src/file_format/wavefront.rs1
-rw-r--r--engine/src/file_format/wavefront/mtl.rs299
2 files changed, 300 insertions, 0 deletions
diff --git a/engine/src/file_format/wavefront.rs b/engine/src/file_format/wavefront.rs
index b23706a..0d1e867 100644
--- a/engine/src/file_format/wavefront.rs
+++ b/engine/src/file_format/wavefront.rs
@@ -1,3 +1,4 @@
+pub mod mtl;
pub mod obj;
mod common;
diff --git a/engine/src/file_format/wavefront/mtl.rs b/engine/src/file_format/wavefront/mtl.rs
new file mode 100644
index 0000000..9c6c136
--- /dev/null
+++ b/engine/src/file_format/wavefront/mtl.rs
@@ -0,0 +1,299 @@
+//! MTL file format parsing.
+//!
+//! File format documentation: <https://paulbourke.net/dataformats/mtl>
+
+use std::path::Path;
+
+use crate::color::Color;
+use crate::file_format::wavefront::common::{
+ keyword,
+ parse_statement_line,
+ ParsingError,
+ Statement,
+};
+use crate::material::{Builder as MaterialBuilder, Material};
+use crate::texture::{Error as TextureError, Texture};
+
+/// Parses the content of a Wavefront `.mtl`.
+///
+/// # Errors
+/// Will return `Err` if the `.mtl` content is formatted incorrectly.
+pub fn parse(obj_content: &str) -> Result<Vec<NamedMaterial>, 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 material_cnt = statements
+ .iter()
+ .filter(|(_, statement)| matches!(statement.keyword, Keyword::Newmtl))
+ .count();
+
+ #[cfg(feature = "debug")]
+ tracing::debug!("Material count: {material_cnt}");
+
+ statements_to_materials(statements, material_cnt)
+}
+
+#[derive(Debug, Clone)]
+pub struct NamedMaterial
+{
+ pub name: String,
+ pub material: Material,
+}
+
+#[derive(Debug, Clone)]
+pub struct UnfinishedNamedMaterial
+{
+ name: String,
+ material_builder: MaterialBuilder,
+ ready: bool,
+}
+
+#[derive(Debug, thiserror::Error)]
+pub enum Error
+{
+ #[error(transparent)]
+ ParsingError(#[from] ParsingError),
+
+ #[error("Failed to open texture")]
+ TextureError(#[from] TextureError),
+
+ #[error(
+ "Unsupported number of arguments ({arg_count}) to {keyword} at line {line_no}"
+ )]
+ UnsupportedArgumentCount
+ {
+ keyword: String,
+ arg_count: usize,
+ line_no: usize,
+ },
+
+ #[error("Invalid number of arguments ({arg_count}) to {keyword} at line {line_no}")]
+ InvalidArgumentCount
+ {
+ keyword: String,
+ arg_count: usize,
+ line_no: usize,
+ },
+}
+
+#[cfg_attr(feature = "debug", tracing::instrument(skip_all))]
+fn statements_to_materials(
+ statements: impl IntoIterator<Item = (usize, Statement<Keyword>)>,
+ material_cnt: usize,
+) -> Result<Vec<NamedMaterial>, Error>
+{
+ let mut materials = Vec::<NamedMaterial>::with_capacity(material_cnt);
+
+ let mut curr_material = UnfinishedNamedMaterial {
+ name: String::new(),
+ material_builder: MaterialBuilder::new(),
+ ready: false,
+ };
+
+ for (line_no, statement) in statements {
+ if statement.keyword == Keyword::Newmtl {
+ if curr_material.ready {
+ #[cfg(feature = "debug")]
+ tracing::debug!("Building material");
+
+ let material = curr_material.material_builder.clone().build();
+
+ materials.push(NamedMaterial { name: curr_material.name, material });
+ }
+
+ let name = statement.get_text_arg(0, line_no)?;
+
+ curr_material.name = name.to_string();
+ curr_material.ready = true;
+
+ continue;
+ }
+
+ if !curr_material.ready {
+ // Discard statements not belonging to a material
+ continue;
+ };
+
+ match statement.keyword {
+ Keyword::Ka => {
+ if statement.arguments.len() > 3 {
+ return Err(Error::InvalidArgumentCount {
+ keyword: statement.keyword.to_string(),
+ arg_count: statement.arguments.len(),
+ line_no,
+ });
+ }
+
+ let red = statement.get_float_arg(0, line_no)?;
+ let green = statement.get_float_arg(1, line_no)?;
+ let blue = statement.get_float_arg(2, line_no)?;
+
+ #[cfg(feature = "debug")]
+ tracing::debug!("Adding ambient color");
+
+ curr_material.material_builder = curr_material
+ .material_builder
+ .ambient(Color { red, green, blue });
+ }
+ Keyword::Kd => {
+ if statement.arguments.len() > 3 {
+ return Err(Error::InvalidArgumentCount {
+ keyword: statement.keyword.to_string(),
+ arg_count: statement.arguments.len(),
+ line_no,
+ });
+ }
+
+ let red = statement.get_float_arg(0, line_no)?;
+ let green = statement.get_float_arg(1, line_no)?;
+ let blue = statement.get_float_arg(2, line_no)?;
+
+ #[cfg(feature = "debug")]
+ tracing::debug!("Adding diffuse color");
+
+ curr_material.material_builder = curr_material
+ .material_builder
+ .diffuse(Color { red, green, blue });
+ }
+ Keyword::Ks => {
+ if statement.arguments.len() > 3 {
+ return Err(Error::InvalidArgumentCount {
+ keyword: statement.keyword.to_string(),
+ arg_count: statement.arguments.len(),
+ line_no,
+ });
+ }
+
+ let red = statement.get_float_arg(0, line_no)?;
+ let green = statement.get_float_arg(1, line_no)?;
+ let blue = statement.get_float_arg(2, line_no)?;
+
+ #[cfg(feature = "debug")]
+ tracing::debug!("Adding specular color");
+
+ curr_material.material_builder = curr_material
+ .material_builder
+ .specular(Color { red, green, blue });
+ }
+ Keyword::MapKa => {
+ if statement.arguments.len() > 1 {
+ return Err(Error::UnsupportedArgumentCount {
+ keyword: statement.keyword.to_string(),
+ arg_count: statement.arguments.len(),
+ line_no,
+ });
+ }
+
+ let texture_file_path = statement.get_text_arg(0, line_no)?;
+
+ let texture = Texture::open(Path::new(texture_file_path))?;
+
+ #[cfg(feature = "debug")]
+ tracing::debug!("Adding ambient map");
+
+ let texture_id = texture.id();
+
+ curr_material.material_builder = curr_material
+ .material_builder
+ .texture(texture)
+ .ambient_map(texture_id);
+ }
+ Keyword::MapKd => {
+ if statement.arguments.len() > 1 {
+ return Err(Error::UnsupportedArgumentCount {
+ keyword: statement.keyword.to_string(),
+ arg_count: statement.arguments.len(),
+ line_no,
+ });
+ }
+
+ let texture_file_path = statement.get_text_arg(0, line_no)?;
+
+ let texture = Texture::open(Path::new(texture_file_path))?;
+
+ #[cfg(feature = "debug")]
+ tracing::debug!("Adding diffuse map");
+
+ let texture_id = texture.id();
+
+ curr_material.material_builder = curr_material
+ .material_builder
+ .texture(texture)
+ .diffuse_map(texture_id);
+ }
+ Keyword::MapKs => {
+ if statement.arguments.len() > 1 {
+ return Err(Error::UnsupportedArgumentCount {
+ keyword: statement.keyword.to_string(),
+ arg_count: statement.arguments.len(),
+ line_no,
+ });
+ }
+
+ let texture_file_path = statement.get_text_arg(0, line_no)?;
+
+ let texture = Texture::open(Path::new(texture_file_path))?;
+
+ #[cfg(feature = "debug")]
+ tracing::debug!("Adding specular map");
+
+ let texture_id = texture.id();
+
+ curr_material.material_builder = curr_material
+ .material_builder
+ .texture(texture)
+ .specular_map(texture_id);
+ }
+ _ => {}
+ }
+ }
+
+ if curr_material.ready {
+ #[cfg(feature = "debug")]
+ tracing::debug!("Building last material");
+
+ let material = curr_material.material_builder.build();
+
+ materials.push(NamedMaterial { name: curr_material.name, material });
+ }
+
+ Ok(materials)
+}
+
+keyword! {
+ #[derive(Debug, PartialEq, Eq, Clone, Copy)]
+ enum Keyword {
+ #[keyword(rename = "newmtl")]
+ Newmtl,
+
+ Ka,
+ Kd,
+ Ks,
+
+ #[keyword(rename = "map_Ka")]
+ MapKa,
+
+ #[keyword(rename = "map_Kd")]
+ MapKd,
+
+ #[keyword(rename = "map_Ks")]
+ MapKs,
+ }
+}