1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
//! Parses `.mtl` format which stores material data

use std::collections::HashMap;
use std::io::BufRead;
use std::mem::take;

use crate::error::ObjResult;
use crate::raw::lexer::lex;
use crate::raw::util::parse_args;

/// Parses a wavefront `.mtl` format *(incomplete)*
pub fn parse_mtl<T: BufRead>(input: T) -> ObjResult<RawMtl> {
    let mut materials = HashMap::new();

    // Properties of the material being currently parsed
    let mut name: Option<String> = None;
    let mut mat: Material = Material::default();

    lex(input, |stmt, args| {
        match stmt {
            // Material name statement
            "newmtl" => {
                // Finish whatever material we were parsing
                if let Some(name) = name.take() {
                    materials.insert(name, take(&mut mat));
                }

                match args {
                    [arg] => name = Some((*arg).to_string()),
                    _ => make_error!(WrongNumberOfArguments, "Expected exactly 1 argument"),
                }
            }

            // Material color and illumination statements
            "Ka" => mat.ambient = Some(parse_color(args)?),
            "Kd" => mat.diffuse = Some(parse_color(args)?),
            "Ks" => mat.specular = Some(parse_color(args)?),
            "Ke" => mat.emissive = Some(parse_color(args)?),
            "Km" => unimplemented!(),
            "Tf" => mat.transmission_filter = Some(parse_color(args)?),
            "Ns" => match args {
                [arg] => mat.specular_exponent = Some(arg.parse()?),
                _ => make_error!(WrongNumberOfArguments, "Expected exactly 1 argument"),
            },
            "Ni" => match args {
                [arg] => mat.optical_density = Some(arg.parse()?),
                _ => make_error!(WrongNumberOfArguments, "Expected exactly 1 argument"),
            },
            "illum" => match args {
                [arg] => mat.illumination_model = Some(arg.parse()?),
                _ => make_error!(WrongNumberOfArguments, "Expected exactly 1 argument"),
            },
            "d" => match args {
                [arg] => mat.dissolve = Some(arg.parse()?),
                _ => make_error!(WrongNumberOfArguments, "Expected exactly 1 argument"),
            },
            "Tr" => match args {
                [arg] => mat.dissolve = Some(1.0 - arg.parse::<f32>()?),
                _ => make_error!(WrongNumberOfArguments, "Expected exactly 1 argument"),
            },

            // Texture map statements
            "map_Ka" => mat.ambient_map = Some(parse_texture_map(args)?),
            "map_Kd" => mat.diffuse_map = Some(parse_texture_map(args)?),
            "map_Ks" => mat.specular_map = Some(parse_texture_map(args)?),
            "map_Ke" => mat.emissive_map = Some(parse_texture_map(args)?),
            "map_d" => mat.dissolve_map = Some(parse_texture_map(args)?),
            "map_aat" => unimplemented!(),
            "map_refl" => unimplemented!(),
            "map_bump" | "map_Bump" | "bump" => mat.bump_map = Some(parse_texture_map(args)?),
            "disp" => unimplemented!(),

            // Reflection map statement
            "refl" => unimplemented!(),

            // Unexpected statement
            _ => make_error!(UnexpectedStatement, "Received unknown statement"),
        }

        Ok(())
    })?;

    // Insert the final material
    if let Some(name) = name {
        materials.insert(name, mat);
    }

    Ok(RawMtl { materials })
}

/// Parses a color from the arguments of a statement
fn parse_color(args: &[&str]) -> ObjResult<MtlColor> {
    if args.is_empty() {
        make_error!(WrongNumberOfArguments, "Expected at least 1 argument");
    }

    Ok(match args[0] {
        "xyz" => match parse_args(&args[1..])?[..] {
            [x] => MtlColor::Xyz(x, x, x),
            [x, y, z] => MtlColor::Xyz(x, y, z),
            _ => make_error!(WrongNumberOfArguments, "Expected 1 or 3 color values"),
        },

        "spectral" => match args[1..] {
            [name] => MtlColor::Spectral(name.to_string(), 1.0),
            [name, multiplier] => MtlColor::Spectral(name.to_string(), multiplier.parse()?),
            _ => make_error!(WrongNumberOfArguments, "Expected 1 or 2 arguments"),
        },

        _ => match parse_args(args)?[..] {
            [r] => MtlColor::Rgb(r, r, r),
            [r, g, b] => MtlColor::Rgb(r, g, b),
            _ => make_error!(WrongNumberOfArguments, "Expected 1 or 3 color values"),
        },
    })
}

/// Parses a texture map specification from the arguments of a statement
fn parse_texture_map(args: &[&str]) -> ObjResult<MtlTextureMap> {
    match args {
        [file] => Ok(MtlTextureMap {
            file: (*file).to_string(),
        }),
        _ => make_error!(WrongNumberOfArguments, "Expected exactly 1 argument"),
    }
}

/// Low-level Rust binding for `.mtl` format *(incomplete)*.
#[derive(Clone, Debug)]
pub struct RawMtl {
    /// Map from the material name to its properties
    pub materials: HashMap<String, Material>,
}

/// A single material from a `.mtl` file
#[derive(Clone, PartialEq, Debug, Default)]
pub struct Material {
    /// The ambient color, specified by `Ka`
    pub ambient: Option<MtlColor>,
    /// The diffuse color, specified by `Kd`
    pub diffuse: Option<MtlColor>,
    /// The specular color, specified by `Ks`
    pub specular: Option<MtlColor>,
    /// The emissive color, specified by `Ke`
    pub emissive: Option<MtlColor>,
    /// The transmission filter, specified by `Tf`
    pub transmission_filter: Option<MtlColor>,
    /// The illumination model to use for this material; see the `.mtl` spec for more details.
    pub illumination_model: Option<u32>,
    /// The dissolve (opacity) of the material, specified by `d`
    pub dissolve: Option<f32>,
    /// The specular exponent, specified by `Ne`
    pub specular_exponent: Option<f32>,
    /// The optical density, i.e. index of refraction, specified by `Ni`
    pub optical_density: Option<f32>,
    /// The ambient color map, specified by `map_Ka`
    pub ambient_map: Option<MtlTextureMap>,
    /// The diffuse color map, specified by `map_Kd`
    pub diffuse_map: Option<MtlTextureMap>,
    /// The specular color map, specified by `map_Ks`
    pub specular_map: Option<MtlTextureMap>,
    /// The emissive color map, specified by `map_Ke`
    pub emissive_map: Option<MtlTextureMap>,
    /// The dissolve map, specified by `map_d`
    pub dissolve_map: Option<MtlTextureMap>,
    /// The bump map (normal map), specified by `bump`
    pub bump_map: Option<MtlTextureMap>,
}

/// A color specified in a `.mtl` file
#[derive(Clone, PartialEq, Debug)]
pub enum MtlColor {
    /// Color in the RGB color space
    Rgb(f32, f32, f32),
    /// Color in the CIEXYZ color space
    Xyz(f32, f32, f32),
    /// Color using a spectral curve
    ///
    /// The first argument is the name of a `.rfl` file specifying the curve.
    /// The second argument is a multiplier for the values in the `.rfl` file.
    Spectral(String, f32),
}

/// A texture map specified in a `.mtl` file
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct MtlTextureMap {
    /// The name of the texture file
    pub file: String,
    // TODO: support arguments to the texture map
}