319 lines
11 KiB
Rust
319 lines
11 KiB
Rust
//! Module that contains all the logic for parsing 3D files.
|
|
|
|
pub mod obj;
|
|
pub mod mtl;
|
|
|
|
use std::fmt;
|
|
use std::path::{Path, PathBuf};
|
|
use std::fs::File;
|
|
use std::io::{BufReader, Error, Read};
|
|
use math::vector::{Vector2, Vector3};
|
|
use model::face::Face;
|
|
use model::material::Material;
|
|
use model::{Model, ModelError};
|
|
|
|
/// A 3D element.
|
|
///
|
|
/// All elements that exists in 3D and that can appear in a 3D format file should have its variant
|
|
/// in this enum.
|
|
#[derive(PartialEq, Debug)]
|
|
pub enum Element {
|
|
/// An empty element (correspond to an empty line for example).
|
|
None,
|
|
|
|
/// Declares the use of a material file.
|
|
MaterialLib(String),
|
|
|
|
/// Changes the material used for the next faces.
|
|
UseMaterial(String),
|
|
|
|
/// Declares a new vertex.
|
|
Vertex(Vector3<f64>),
|
|
|
|
/// Declares a new texture coordinate.
|
|
TextureCoordinate(Vector2<f64>),
|
|
|
|
/// Declares a new normal.
|
|
Normal(Vector3<f64>),
|
|
|
|
/// Declares a new face.
|
|
Face(Face),
|
|
|
|
/// Declares a new material.
|
|
NewMaterial(String),
|
|
|
|
/// Changes the texture of the current material.
|
|
///
|
|
/// First string is the name of the map.
|
|
/// Second string is the path to the image file.
|
|
Texture(String, String),
|
|
|
|
/// Change the main color of the current material.
|
|
Color(Vector3<f64>),
|
|
|
|
/// An unknown material instruction that will be copied into the mtl file.
|
|
UnknownMaterialInstruction(String),
|
|
|
|
/// Declares multiple faces.
|
|
Faces(Vec<Face>),
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
/// An error while parsing a file.
|
|
pub enum ParserError {
|
|
|
|
/// An std::io::Error when opening the file.
|
|
IoError(String, Error),
|
|
|
|
/// The model format is unknown.
|
|
UnknownFormat(String),
|
|
|
|
/// A keyword was expected but something else arrived.
|
|
UnexpectedKeyword(String, usize, String),
|
|
|
|
/// Failed to parse a number.
|
|
ParseNumberError(String, usize, String),
|
|
|
|
/// Failed to parse a line.
|
|
///
|
|
/// First usize is line.
|
|
/// Second usize is expected.
|
|
/// Thirs usize is got.
|
|
IncorrectNumberOfParameters(String, usize, usize, usize),
|
|
|
|
/// A face contains too many vertices.
|
|
///
|
|
/// Only 3 vertices face are supported.
|
|
///
|
|
/// First usize is line.
|
|
/// Second usize is the number of vertices in the face.
|
|
TooManyVertices(String, usize, usize),
|
|
|
|
/// A face references a vertex / tex coord / normal that does not exist.
|
|
///
|
|
/// First usize is line.
|
|
/// Second usize is the id of the vertex.
|
|
/// Third usize is the size of the array.
|
|
IndexOutOfBound(String, usize, usize, usize),
|
|
|
|
/// Material already exists.
|
|
///
|
|
/// usize is the line.
|
|
/// String is the name of the material that is already defined.
|
|
MaterialAlreadyExists(String, usize, String),
|
|
|
|
/// Texture arrived before creating a material.
|
|
///
|
|
/// usize is the line.
|
|
/// String is the path to the texture.
|
|
NoMaterialExist(String, usize, String),
|
|
|
|
/// Something weird happened
|
|
OtherError(String),
|
|
}
|
|
|
|
/// Parses a content from a reader, guess the file type from path, and return the model.
|
|
pub fn parse<R: Read>(reader: R, path: &str) -> Result<Model, ParserError> {
|
|
let mut model = Model::new();
|
|
parse_into_model(reader, path, &mut model)?;
|
|
Ok(model)
|
|
}
|
|
|
|
/// Parses a file and append the result to the model passed as parameter.
|
|
pub fn parse_file_into_model(path: &str, model: &mut Model) -> Result<(), ParserError> {
|
|
let file = match File::open(path) {
|
|
Ok(file) => file,
|
|
Err(e) => return Err(ParserError::IoError(path.to_owned(), e)),
|
|
};
|
|
|
|
let bufreader = BufReader::new(file);
|
|
parse_into_model(bufreader, path, model)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Detects the format from path name and parses it into the model.
|
|
pub fn parse_into_model<R: Read>(reader: R, path: &str, model: &mut Model) -> Result<(), ParserError> {
|
|
let extension = Path::new(path).extension().unwrap().to_str().unwrap();
|
|
|
|
use self::obj::ObjParser;
|
|
use self::mtl::MtlParser;
|
|
|
|
match extension {
|
|
"obj" => ObjParser::new().parse_into_model(model, reader, path)?,
|
|
"mtl" => MtlParser::new().parse_into_model(model, reader, path)?,
|
|
_ => return Err(ParserError::UnknownFormat(path.to_owned())),
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Detects the format from path name and parses it into a new model.
|
|
pub fn parse_file(path: &str) -> Result<Model, ParserError> {
|
|
let mut model = Model::new();
|
|
parse_file_into_model(path, &mut model)?;
|
|
Ok(model)
|
|
}
|
|
|
|
impl fmt::Display for ParserError {
|
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
match *self {
|
|
ParserError::IoError(ref s, ref e) =>
|
|
write!(f, "Error opening file {}: {}", s, e),
|
|
ParserError::UnknownFormat(ref s) =>
|
|
write!(f, "Unknown format for file {}", s),
|
|
ParserError::UnexpectedKeyword(ref p, n, ref s) =>
|
|
write!(f, "Unexpected keyword in {}:{}:\n\t{}", p, n, s),
|
|
ParserError::ParseNumberError(ref p, n, ref s) =>
|
|
write!(f, "Could not parse number in {}:{}:\n\t{}", p, n, s),
|
|
ParserError::IncorrectNumberOfParameters(ref p, l, e, r) =>
|
|
write!(f, "Wrong number of parameters in {}:{}:\n\texpected {}, got {}", p, l, e, r),
|
|
ParserError::TooManyVertices(ref p, l, s) =>
|
|
write!(f, "Too many faces in {}:{}:\n\t{}", p, l, s),
|
|
ParserError::IndexOutOfBound(ref p, l, i, s) =>
|
|
write!(f, "Index out of bound in {}:{}:\n\tsize is {} but got {}", p, l, i, s),
|
|
ParserError::MaterialAlreadyExists(ref p, l, ref n) =>
|
|
write!(f, "Redecralation of material in {}:{}:\n\t{} already exists", p, n, l),
|
|
ParserError::NoMaterialExist(ref p, l, ref n) =>
|
|
write!(f, "Missing material in {}:{}\n\tNo material were defined, can't set {} as textuure", p, l, n),
|
|
ParserError::OtherError(ref p) =>
|
|
write!(f, "Something weird happened in {}...", p),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Anything that can parse a 3D file and create a 3D model from it.
|
|
pub trait Parser {
|
|
|
|
/// Parses a file and adds its content into an empty model and returns the model.
|
|
fn parse<R: Read>(&mut self, reader: R, path: &str) -> Result<Model, ParserError> {
|
|
let mut model = Model::new();
|
|
self.parse_into_model(&mut model, reader, path)?;
|
|
Ok(model)
|
|
}
|
|
|
|
/// Parses a file and adds its content to an already existing model.
|
|
fn parse_into_model<R: Read>(&mut self, model: &mut Model, reader: R, path: &str) -> Result<(), ParserError> {
|
|
logln!("Parsing {}...", path);
|
|
self.priv_parse_into_model(model, reader, path)
|
|
}
|
|
|
|
/// Parses a file and adds its content to an already existing model.
|
|
///
|
|
/// This is the method you should implement for you format parser.
|
|
fn priv_parse_into_model<R: Read>(&mut self, model: &mut Model, reader: R, path: &str) -> Result<(), ParserError>;
|
|
}
|
|
|
|
/// Anything that can parse a 3D file line by line.
|
|
///
|
|
/// This should not be used for binary files for instance.
|
|
pub trait LineParser {
|
|
/// Parses a single line of a file
|
|
fn parse_line(&mut self, line_number: usize, line: &str, filename: &str) ->
|
|
Result<Element, ParserError>;
|
|
}
|
|
|
|
impl<LP: LineParser> Parser for LP {
|
|
|
|
fn priv_parse_into_model<R: Read>(&mut self, model: &mut Model, reader: R, path: &str) -> Result<(), ParserError> {
|
|
|
|
let file = BufReader::new(reader);
|
|
|
|
let mut current_material_name = None;
|
|
|
|
use std::io::BufRead;
|
|
for (num, line) in file.lines().enumerate() {
|
|
|
|
let num = num + 1;
|
|
|
|
let line = match line {
|
|
Ok(x) => x,
|
|
Err(e) => return Err(ParserError::IoError(path.to_owned(), e)),
|
|
};
|
|
|
|
let element = self.parse_line(num, &line, &path)?;
|
|
|
|
match element {
|
|
Element::None => (),
|
|
Element::UseMaterial(m) => model.change_part(Some(m)),
|
|
Element::Vertex(v) => model.vertices.push(v),
|
|
Element::TextureCoordinate(v) => model.texture_coordinates.push(v),
|
|
Element::Normal(v) => model.normals.push(v),
|
|
|
|
Element::MaterialLib(ref material_path) => {
|
|
// Append material_path to path
|
|
let mut path = PathBuf::from(path);
|
|
path.pop();
|
|
path.push(material_path);
|
|
if let Err(err) = parse_file_into_model(path.to_str().unwrap(), model) {
|
|
return Err(err);
|
|
}
|
|
},
|
|
|
|
Element::Face(f) => {
|
|
if let Err(ModelError::IndexError(index, size)) = model.add_face(f) {
|
|
return Err(ParserError::IndexOutOfBound(path.to_owned(), num, index, size));
|
|
}
|
|
},
|
|
|
|
Element::NewMaterial(m) => {
|
|
if model.materials.contains_key(&m) {
|
|
return Err(ParserError::MaterialAlreadyExists(path.to_owned(), num, m));
|
|
} else {
|
|
let new_material = Material::new(&m);
|
|
current_material_name = Some(m.clone());
|
|
model.materials.insert(m, new_material);
|
|
}
|
|
},
|
|
|
|
Element::Texture(texture_name, texture_path) => {
|
|
if current_material_name.is_none() {
|
|
return Err(ParserError::NoMaterialExist(path.to_owned(), num, texture_path));
|
|
}
|
|
|
|
let material_name = current_material_name.as_ref().unwrap().clone();
|
|
|
|
let current_material = model.materials.get_mut(&material_name);
|
|
|
|
if current_material.is_none() {
|
|
return Err(ParserError::NoMaterialExist(path.to_owned(), num, texture_path))
|
|
}
|
|
|
|
let current_material = current_material.unwrap();
|
|
current_material.textures.insert(texture_name, texture_path);
|
|
},
|
|
|
|
Element::Color(_) => (),
|
|
|
|
Element::UnknownMaterialInstruction(s) => {
|
|
if current_material_name.is_none() {
|
|
return Err(ParserError::NoMaterialExist(path.to_owned(), num, s));
|
|
}
|
|
|
|
let material_name = current_material_name.as_ref().unwrap().clone();
|
|
|
|
let current_material = model.materials.get_mut(&material_name);
|
|
|
|
if current_material.is_none() {
|
|
return Err(ParserError::NoMaterialExist(path.to_owned(), num, s))
|
|
}
|
|
|
|
current_material.unwrap().add_unknown_instruction(s);
|
|
},
|
|
|
|
Element::Faces(faces) => {
|
|
for f in faces {
|
|
if let Err(ModelError::IndexError(index, size)) = model.add_face(f) {
|
|
return Err(ParserError::IndexOutOfBound(path.to_owned(), num, index, size));
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
}
|