obj/raw/
material.rs

1//! Parses `.mtl` format which stores material data
2
3use std::collections::HashMap;
4use std::io::BufRead;
5use std::mem::take;
6
7use crate::error::{make_error, ObjResult};
8use crate::raw::lexer::lex;
9use crate::raw::util::parse_args;
10
11/// Parses a wavefront `.mtl` format *(incomplete)*
12pub fn parse_mtl<T: BufRead>(input: T) -> ObjResult<RawMtl> {
13    let mut materials = HashMap::new();
14
15    // Properties of the material being currently parsed
16    let mut name: Option<String> = None;
17    let mut mat: Material = Material::default();
18
19    lex(input, |stmt, args| {
20        match stmt {
21            // Material name statement
22            "newmtl" => {
23                // Finish whatever material we were parsing
24                if let Some(name) = name.take() {
25                    materials.insert(name, take(&mut mat));
26                }
27
28                match args {
29                    [arg] => name = Some((*arg).to_string()),
30                    _ => make_error!(WrongNumberOfArguments, "Expected exactly 1 argument"),
31                }
32            }
33
34            // Material color and illumination statements
35            "Ka" => mat.ambient = Some(parse_color(args)?),
36            "Kd" => mat.diffuse = Some(parse_color(args)?),
37            "Ks" => mat.specular = Some(parse_color(args)?),
38            "Ke" => mat.emissive = Some(parse_color(args)?),
39            "Km" => unimplemented!(),
40            "Tf" => mat.transmission_filter = Some(parse_color(args)?),
41            "Ns" => match args {
42                [arg] => mat.specular_exponent = Some(arg.parse()?),
43                _ => make_error!(WrongNumberOfArguments, "Expected exactly 1 argument"),
44            },
45            "Ni" => match args {
46                [arg] => mat.optical_density = Some(arg.parse()?),
47                _ => make_error!(WrongNumberOfArguments, "Expected exactly 1 argument"),
48            },
49            "illum" => match args {
50                [arg] => mat.illumination_model = Some(arg.parse()?),
51                _ => make_error!(WrongNumberOfArguments, "Expected exactly 1 argument"),
52            },
53            "d" => match args {
54                [arg] => mat.dissolve = Some(arg.parse()?),
55                _ => make_error!(WrongNumberOfArguments, "Expected exactly 1 argument"),
56            },
57            "Tr" => match args {
58                [arg] => mat.dissolve = Some(1.0 - arg.parse::<f32>()?),
59                _ => make_error!(WrongNumberOfArguments, "Expected exactly 1 argument"),
60            },
61
62            // Texture map statements
63            "map_Ka" => mat.ambient_map = Some(parse_texture_map(args)?),
64            "map_Kd" => mat.diffuse_map = Some(parse_texture_map(args)?),
65            "map_Ks" => mat.specular_map = Some(parse_texture_map(args)?),
66            "map_Ke" => mat.emissive_map = Some(parse_texture_map(args)?),
67            "map_d" => mat.dissolve_map = Some(parse_texture_map(args)?),
68            "map_aat" => unimplemented!(),
69            "map_refl" => unimplemented!(),
70            "map_bump" | "map_Bump" | "bump" => mat.bump_map = Some(parse_texture_map(args)?),
71            "disp" => unimplemented!(),
72
73            // Reflection map statement
74            "refl" => unimplemented!(),
75
76            // Unexpected statement
77            _ => make_error!(UnexpectedStatement, "Received unknown statement"),
78        }
79
80        Ok(())
81    })?;
82
83    // Insert the final material
84    if let Some(name) = name {
85        materials.insert(name, mat);
86    }
87
88    Ok(RawMtl { materials })
89}
90
91/// Parses a color from the arguments of a statement
92fn parse_color(args: &[&str]) -> ObjResult<MtlColor> {
93    if args.is_empty() {
94        make_error!(WrongNumberOfArguments, "Expected at least 1 argument");
95    }
96
97    Ok(match args[0] {
98        "xyz" => match parse_args(&args[1..])?[..] {
99            [x] => MtlColor::Xyz(x, x, x),
100            [x, y, z] => MtlColor::Xyz(x, y, z),
101            _ => make_error!(WrongNumberOfArguments, "Expected 1 or 3 color values"),
102        },
103
104        "spectral" => match args[1..] {
105            [name] => MtlColor::Spectral(name.to_string(), 1.0),
106            [name, multiplier] => MtlColor::Spectral(name.to_string(), multiplier.parse()?),
107            _ => make_error!(WrongNumberOfArguments, "Expected 1 or 2 arguments"),
108        },
109
110        _ => match parse_args(args)?[..] {
111            [r] => MtlColor::Rgb(r, r, r),
112            [r, g, b] => MtlColor::Rgb(r, g, b),
113            _ => make_error!(WrongNumberOfArguments, "Expected 1 or 3 color values"),
114        },
115    })
116}
117
118/// Parses a texture map specification from the arguments of a statement
119fn parse_texture_map(args: &[&str]) -> ObjResult<MtlTextureMap> {
120    match args {
121        [file] => Ok(MtlTextureMap {
122            file: (*file).to_string(),
123        }),
124        _ => make_error!(WrongNumberOfArguments, "Expected exactly 1 argument"),
125    }
126}
127
128/// Low-level Rust binding for `.mtl` format *(incomplete)*.
129#[derive(Clone, PartialEq, Debug, Default)]
130pub struct RawMtl {
131    /// Map from the material name to its properties
132    pub materials: HashMap<String, Material>,
133}
134
135/// A single material from a `.mtl` file
136#[derive(Clone, PartialEq, Debug, Default)]
137pub struct Material {
138    /// The ambient color, specified by `Ka`
139    pub ambient: Option<MtlColor>,
140    /// The diffuse color, specified by `Kd`
141    pub diffuse: Option<MtlColor>,
142    /// The specular color, specified by `Ks`
143    pub specular: Option<MtlColor>,
144    /// The emissive color, specified by `Ke`
145    pub emissive: Option<MtlColor>,
146    /// The transmission filter, specified by `Tf`
147    pub transmission_filter: Option<MtlColor>,
148    /// The illumination model to use for this material; see the `.mtl` spec for more details.
149    pub illumination_model: Option<u32>,
150    /// The dissolve (opacity) of the material, specified by `d`
151    pub dissolve: Option<f32>,
152    /// The specular exponent, specified by `Ne`
153    pub specular_exponent: Option<f32>,
154    /// The optical density, i.e. index of refraction, specified by `Ni`
155    pub optical_density: Option<f32>,
156    /// The ambient color map, specified by `map_Ka`
157    pub ambient_map: Option<MtlTextureMap>,
158    /// The diffuse color map, specified by `map_Kd`
159    pub diffuse_map: Option<MtlTextureMap>,
160    /// The specular color map, specified by `map_Ks`
161    pub specular_map: Option<MtlTextureMap>,
162    /// The emissive color map, specified by `map_Ke`
163    pub emissive_map: Option<MtlTextureMap>,
164    /// The dissolve map, specified by `map_d`
165    pub dissolve_map: Option<MtlTextureMap>,
166    /// The bump map (normal map), specified by `bump`
167    pub bump_map: Option<MtlTextureMap>,
168}
169
170/// A color specified in a `.mtl` file
171#[derive(Clone, PartialEq, Debug)]
172pub enum MtlColor {
173    /// Color in the RGB color space
174    Rgb(f32, f32, f32),
175    /// Color in the CIEXYZ color space
176    Xyz(f32, f32, f32),
177    /// Color using a spectral curve
178    ///
179    /// The first argument is the name of a `.rfl` file specifying the curve.
180    /// The second argument is a multiplier for the values in the `.rfl` file.
181    Spectral(String, f32),
182}
183
184/// A texture map specified in a `.mtl` file
185#[derive(Clone, PartialEq, Eq, Debug)]
186pub struct MtlTextureMap {
187    /// The name of the texture file
188    pub file: String,
189    // TODO: support arguments to the texture map
190}