From 4c745eca7762eb5d7f42a9997e9bb0eb85509920 Mon Sep 17 00:00:00 2001 From: HampusM Date: Sat, 18 May 2024 14:27:12 +0200 Subject: feat(engine): add basic Wavefront MTL parsing --- engine/src/file_format/wavefront/mtl.rs | 299 ++++++++++++++++++++++++++++++++ 1 file changed, 299 insertions(+) create mode 100644 engine/src/file_format/wavefront/mtl.rs (limited to 'engine/src/file_format/wavefront') 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: + +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, 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::(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::, _>>()?; + + 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)>, + material_cnt: usize, +) -> Result, Error> +{ + let mut materials = Vec::::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, + } +} -- cgit v1.2.3-18-g5258