#![allow(dead_code)]
#![cfg_attr(all(test, feature = "unstable"), feature(test))]
#![cfg_attr(feature = "unstable", feature(plugin))]
#![cfg_attr(feature = "unstable", plugin(clippy))]
#[cfg(all(test, feature = "unstable"))]
extern crate test;
use std::collections::HashMap;
use std::error::Error;
use std::fmt;
use std::fs::File;
use std::io::prelude::*;
use std::io::BufReader;
use std::path::Path;
use std::str::{FromStr, SplitWhitespace};
#[derive(Debug, Clone)]
pub struct Mesh {
pub positions: Vec<f32>,
pub normals: Vec<f32>,
pub texcoords: Vec<f32>,
pub indices: Vec<u32>,
pub num_face_indices: Vec<u32>,
pub material_id: Option<usize>,
}
impl Mesh {
pub fn empty() -> Mesh {
Mesh {
positions: Vec::new(),
normals: Vec::new(),
texcoords: Vec::new(),
indices: Vec::new(),
num_face_indices: Vec::new(),
material_id: None,
}
}
}
#[derive(Clone, Debug)]
pub struct Model {
pub mesh: Mesh,
pub name: String,
}
impl Model {
pub fn new(mesh: Mesh, name: String) -> Model {
Model { mesh, name }
}
}
#[derive(Clone, Debug)]
pub struct Material {
pub name: String,
pub ambient: [f32; 3],
pub diffuse: [f32; 3],
pub specular: [f32; 3],
pub shininess: f32,
pub dissolve: f32,
pub optical_density: f32,
pub ambient_texture: String,
pub diffuse_texture: String,
pub specular_texture: String,
pub normal_texture: String,
pub shininess_texture: String,
pub dissolve_texture: String,
pub illumination_model: Option<u8>,
pub unknown_param: HashMap<String, String>,
}
impl Material {
pub fn empty() -> Material {
Material {
name: String::new(),
ambient: [0.0; 3],
diffuse: [0.0; 3],
specular: [0.0; 3],
shininess: 0.0,
dissolve: 1.0,
optical_density: 1.0,
ambient_texture: String::new(),
diffuse_texture: String::new(),
specular_texture: String::new(),
normal_texture: String::new(),
shininess_texture: String::new(),
dissolve_texture: String::new(),
illumination_model: None,
unknown_param: HashMap::new(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum LoadError {
OpenFileFailed,
ReadError,
UnrecognizedCharacter,
PositionParseError,
NormalParseError,
TexcoordParseError,
FaceParseError,
MaterialParseError,
InvalidObjectName,
FaceVertexOutOfBounds,
FaceTexCoordOutOfBounds,
FaceNormalOutOfBounds,
GenericFailure,
}
impl fmt::Display for LoadError {
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
let msg = match *self {
LoadError::OpenFileFailed => "open file failed",
LoadError::ReadError => "read error",
LoadError::UnrecognizedCharacter => "unrecognized character",
LoadError::PositionParseError => "position parse error",
LoadError::NormalParseError => "normal parse error",
LoadError::TexcoordParseError => "texcoord parse error",
LoadError::FaceParseError => "face parse error",
LoadError::MaterialParseError => "material parse error",
LoadError::InvalidObjectName => "invalid object name",
LoadError::FaceVertexOutOfBounds => "face vertex index out of bounds",
LoadError::FaceTexCoordOutOfBounds => "face texcoord index out of bounds",
LoadError::FaceNormalOutOfBounds => "face normal index out of bounds",
LoadError::GenericFailure => "generic failure",
};
f.write_str(msg)
}
}
impl Error for LoadError {}
pub type LoadResult = Result<(Vec<Model>, Vec<Material>), LoadError>;
pub type MTLLoadResult = Result<(Vec<Material>, HashMap<String, usize>), LoadError>;
#[derive(Hash, Eq, PartialEq, PartialOrd, Ord, Debug, Copy, Clone)]
struct VertexIndices {
pub v: isize,
pub vt: isize,
pub vn: isize,
}
impl VertexIndices {
fn parse(
face_str: &str,
pos_sz: usize,
tex_sz: usize,
norm_sz: usize,
) -> Option<VertexIndices> {
let mut indices = [-1; 3];
for i in face_str.split('/').enumerate() {
if !i.1.is_empty() {
match isize::from_str(i.1) {
Ok(x) => {
indices[i.0] = if x < 0 {
match i.0 {
0 => x + pos_sz as isize,
1 => x + tex_sz as isize,
2 => x + norm_sz as isize,
_ => panic!("Invalid number of elements for a face (> 3)!"),
}
} else {
x - 1
};
}
Err(_) => return None,
}
}
}
Some(VertexIndices {
v: indices[0],
vt: indices[1],
vn: indices[2],
})
}
}
#[derive(Debug)]
enum Face {
Line(VertexIndices, VertexIndices),
Triangle(VertexIndices, VertexIndices, VertexIndices),
Quad(VertexIndices, VertexIndices, VertexIndices, VertexIndices),
Polygon(Vec<VertexIndices>),
}
fn parse_floatn(val_str: SplitWhitespace, vals: &mut Vec<f32>, n: usize) -> bool {
let sz = vals.len();
for p in val_str {
if sz + n == vals.len() {
return true;
}
match FromStr::from_str(p) {
Ok(x) => vals.push(x),
Err(_) => return false,
}
}
sz + n == vals.len()
}
fn parse_float3(val_str: SplitWhitespace, vals: &mut [f32; 3]) -> bool {
for (i, p) in val_str.enumerate().take(3) {
match FromStr::from_str(p) {
Ok(x) => vals[i] = x,
Err(_) => return false,
}
}
true
}
fn parse_face(
face_str: SplitWhitespace,
faces: &mut Vec<Face>,
pos_sz: usize,
tex_sz: usize,
norm_sz: usize,
) -> bool {
let mut indices = Vec::new();
for f in face_str {
match VertexIndices::parse(f, pos_sz, tex_sz, norm_sz) {
Some(v) => indices.push(v),
None => return false,
}
}
match indices.len() {
2 => faces.push(Face::Line(indices[0], indices[1])),
3 => faces.push(Face::Triangle(indices[0], indices[1], indices[2])),
4 => faces.push(Face::Quad(indices[0], indices[1], indices[2], indices[3])),
_ => faces.push(Face::Polygon(indices)),
}
true
}
fn add_vertex(
mesh: &mut Mesh,
index_map: &mut HashMap<VertexIndices, u32>,
vert: &VertexIndices,
pos: &[f32],
texcoord: &[f32],
normal: &[f32],
) -> Result<(), LoadError> {
match index_map.get(vert) {
Some(&i) => mesh.indices.push(i),
None => {
let v = vert.v as usize;
if v * 3 + 2 >= pos.len() {
return Err(LoadError::FaceVertexOutOfBounds);
}
mesh.positions.push(pos[v * 3]);
mesh.positions.push(pos[v * 3 + 1]);
mesh.positions.push(pos[v * 3 + 2]);
if !texcoord.is_empty() && vert.vt > -1 {
let vt = vert.vt as usize;
if vt * 2 + 1 >= texcoord.len() {
return Err(LoadError::FaceTexCoordOutOfBounds);
}
mesh.texcoords.push(texcoord[vt * 2]);
mesh.texcoords.push(texcoord[vt * 2 + 1]);
}
if !normal.is_empty() && vert.vn > -1 {
let vn = vert.vn as usize;
if vn * 3 + 2 >= normal.len() {
return Err(LoadError::FaceNormalOutOfBounds);
}
mesh.normals.push(normal[vn * 3]);
mesh.normals.push(normal[vn * 3 + 1]);
mesh.normals.push(normal[vn * 3 + 2]);
}
let next = index_map.len() as u32;
mesh.indices.push(next);
index_map.insert(*vert, next);
}
}
Ok(())
}
fn export_faces(
pos: &[f32],
texcoord: &[f32],
normal: &[f32],
faces: &[Face],
mat_id: Option<usize>,
triangulate_faces: bool,
) -> Result<Mesh, LoadError> {
let mut index_map = HashMap::new();
let mut mesh = Mesh::empty();
mesh.material_id = mat_id;
for f in faces {
match *f {
Face::Line(ref a, ref b) => {
add_vertex(&mut mesh, &mut index_map, a, pos, texcoord, normal)?;
add_vertex(&mut mesh, &mut index_map, b, pos, texcoord, normal)?;
mesh.num_face_indices.push(2);
}
Face::Triangle(ref a, ref b, ref c) => {
add_vertex(&mut mesh, &mut index_map, a, pos, texcoord, normal)?;
add_vertex(&mut mesh, &mut index_map, b, pos, texcoord, normal)?;
add_vertex(&mut mesh, &mut index_map, c, pos, texcoord, normal)?;
mesh.num_face_indices.push(3);
}
Face::Quad(ref a, ref b, ref c, ref d) => {
if triangulate_faces {
add_vertex(&mut mesh, &mut index_map, a, pos, texcoord, normal)?;
add_vertex(&mut mesh, &mut index_map, b, pos, texcoord, normal)?;
add_vertex(&mut mesh, &mut index_map, c, pos, texcoord, normal)?;
mesh.num_face_indices.push(3);
add_vertex(&mut mesh, &mut index_map, a, pos, texcoord, normal)?;
add_vertex(&mut mesh, &mut index_map, c, pos, texcoord, normal)?;
add_vertex(&mut mesh, &mut index_map, d, pos, texcoord, normal)?;
mesh.num_face_indices.push(3);
} else {
add_vertex(&mut mesh, &mut index_map, a, pos, texcoord, normal)?;
add_vertex(&mut mesh, &mut index_map, b, pos, texcoord, normal)?;
add_vertex(&mut mesh, &mut index_map, c, pos, texcoord, normal)?;
add_vertex(&mut mesh, &mut index_map, d, pos, texcoord, normal)?;
mesh.num_face_indices.push(4);
}
}
Face::Polygon(ref indices) => {
if triangulate_faces {
let a = &indices[0];
let mut b = &indices[1];
for c in indices.iter().skip(2) {
add_vertex(&mut mesh, &mut index_map, a, pos, texcoord, normal)?;
add_vertex(&mut mesh, &mut index_map, b, pos, texcoord, normal)?;
add_vertex(&mut mesh, &mut index_map, c, pos, texcoord, normal)?;
mesh.num_face_indices.push(3);
b = c;
}
} else {
for i in indices.iter() {
add_vertex(&mut mesh, &mut index_map, i, pos, texcoord, normal)?;
}
mesh.num_face_indices.push(indices.len() as u32);
}
}
}
}
Ok(mesh)
}
pub fn load_obj<P>(file_name: P, triangulate_faces: bool) -> LoadResult
where
P: AsRef<Path> + fmt::Debug,
{
let file = match File::open(file_name.as_ref()) {
Ok(f) => f,
Err(_e) => {
#[cfg(feature = "log")]
log::error!("load_obj - failed to open {:?} due to {}", file_name, _e);
return Err(LoadError::OpenFileFailed);
}
};
let mut reader = BufReader::new(file);
load_obj_buf(&mut reader, triangulate_faces, |mat_path| {
let full_path = if let Some(parent) = file_name.as_ref().parent() {
parent.join(mat_path)
} else {
mat_path.to_owned()
};
self::load_mtl(&full_path)
})
}
pub fn load_mtl<P>(file_name: P) -> MTLLoadResult
where
P: AsRef<Path> + fmt::Debug,
{
let file = match File::open(file_name.as_ref()) {
Ok(f) => f,
Err(_e) => {
#[cfg(feature = "log")]
log::error!("load_mtl - failed to open {:?} due to {}", file_name, _e);
return Err(LoadError::OpenFileFailed);
}
};
let mut reader = BufReader::new(file);
load_mtl_buf(&mut reader)
}
pub fn load_obj_buf<B, ML>(
reader: &mut B,
triangulate_faces: bool,
material_loader: ML,
) -> LoadResult
where
B: BufRead,
ML: Fn(&Path) -> MTLLoadResult,
{
let mut models = Vec::new();
let mut materials = Vec::new();
let mut mat_map = HashMap::new();
let mut tmp_pos = Vec::new();
let mut tmp_texcoord = Vec::new();
let mut tmp_normal = Vec::new();
let mut tmp_faces: Vec<Face> = Vec::new();
let mut name = "unnamed_object".to_owned();
let mut mat_id = None;
for line in reader.lines() {
let (line, mut words) = match line {
Ok(ref line) => (&line[..], line[..].split_whitespace()),
Err(_e) => {
#[cfg(feature = "log")]
log::error!("load_obj - failed to read line due to {}", _e);
return Err(LoadError::ReadError);
}
};
match words.next() {
Some("#") | None => continue,
Some("v") => {
if !parse_floatn(words, &mut tmp_pos, 3) {
return Err(LoadError::PositionParseError);
}
}
Some("vt") => {
if !parse_floatn(words, &mut tmp_texcoord, 2) {
return Err(LoadError::TexcoordParseError);
}
}
Some("vn") => {
if !parse_floatn(words, &mut tmp_normal, 3) {
return Err(LoadError::NormalParseError);
}
}
Some("f") | Some("l") => {
if !parse_face(
words,
&mut tmp_faces,
tmp_pos.len() / 3,
tmp_texcoord.len() / 2,
tmp_normal.len() / 3,
) {
return Err(LoadError::FaceParseError);
}
}
Some("o") | Some("g") => {
if !tmp_faces.is_empty() {
models.push(Model::new(
export_faces(
&tmp_pos,
&tmp_texcoord,
&tmp_normal,
&tmp_faces,
mat_id,
triangulate_faces,
)?,
name,
));
tmp_faces.clear();
}
name = line[1..].trim().to_owned();
if name.is_empty() {
name = "unnamed_object".to_owned();
}
}
Some("mtllib") => {
if let Some(mtllib) = words.next() {
let mat_file = Path::new(mtllib).to_path_buf();
match material_loader(mat_file.as_path()) {
Ok((mut mats, map)) => {
let mat_offset = materials.len();
materials.append(&mut mats);
for m in map {
mat_map.insert(m.0, m.1 + mat_offset);
}
}
Err(e) => return Err(e),
}
} else {
return Err(LoadError::MaterialParseError);
}
}
Some("usemtl") => {
let mat_name = line[7..].trim().to_owned();
if !mat_name.is_empty() {
let new_mat = mat_map.get(&mat_name).cloned();
if mat_id != new_mat && !tmp_faces.is_empty() {
models.push(Model::new(
export_faces(
&tmp_pos,
&tmp_texcoord,
&tmp_normal,
&tmp_faces,
mat_id,
triangulate_faces,
)?,
name.clone(),
));
tmp_faces.clear();
}
if new_mat.is_none() {
#[cfg(feature = "log")]
log::warn!("Object {} refers to unfound material: {}", name, mat_name);
}
mat_id = new_mat;
} else {
return Err(LoadError::MaterialParseError);
}
}
Some(_) => {}
}
}
models.push(Model::new(
export_faces(
&tmp_pos,
&tmp_texcoord,
&tmp_normal,
&tmp_faces,
mat_id,
triangulate_faces,
)?,
name,
));
Ok((models, materials))
}
pub fn load_mtl_buf<B: BufRead>(reader: &mut B) -> MTLLoadResult {
let mut materials = Vec::new();
let mut mat_map = HashMap::new();
let mut cur_mat = Material::empty();
for line in reader.lines() {
let (line, mut words) = match line {
Ok(ref line) => (line.trim(), line[..].split_whitespace()),
Err(_e) => {
#[cfg(feature = "log")]
log::error!("load_obj - failed to read line due to {}", _e);
return Err(LoadError::ReadError);
}
};
match words.next() {
Some("#") | None => continue,
Some("newmtl") => {
if !cur_mat.name.is_empty() {
mat_map.insert(cur_mat.name.clone(), materials.len());
materials.push(cur_mat);
}
cur_mat = Material::empty();
cur_mat.name = line[6..].trim().to_owned();
if cur_mat.name.is_empty() {
return Err(LoadError::InvalidObjectName);
}
}
Some("Ka") => {
if !parse_float3(words, &mut cur_mat.ambient) {
return Err(LoadError::MaterialParseError);
}
}
Some("Kd") => {
if !parse_float3(words, &mut cur_mat.diffuse) {
return Err(LoadError::MaterialParseError);
}
}
Some("Ks") => {
if !parse_float3(words, &mut cur_mat.specular) {
return Err(LoadError::MaterialParseError);
}
}
Some("Ns") => {
if let Some(p) = words.next() {
match FromStr::from_str(p) {
Ok(x) => cur_mat.shininess = x,
Err(_) => return Err(LoadError::MaterialParseError),
}
} else {
return Err(LoadError::MaterialParseError);
}
}
Some("Ni") => {
if let Some(p) = words.next() {
match FromStr::from_str(p) {
Ok(x) => cur_mat.optical_density = x,
Err(_) => return Err(LoadError::MaterialParseError),
}
} else {
return Err(LoadError::MaterialParseError);
}
}
Some("d") => {
if let Some(p) = words.next() {
match FromStr::from_str(p) {
Ok(x) => cur_mat.dissolve = x,
Err(_) => return Err(LoadError::MaterialParseError),
}
} else {
return Err(LoadError::MaterialParseError);
}
}
Some("map_Ka") => match line.get(6..).map(str::trim) {
Some("") | None => return Err(LoadError::MaterialParseError),
Some(tex) => cur_mat.ambient_texture = tex.to_owned(),
},
Some("map_Kd") => match line.get(6..).map(str::trim) {
Some("") | None => return Err(LoadError::MaterialParseError),
Some(tex) => cur_mat.diffuse_texture = tex.to_owned(),
},
Some("map_Ks") => match line.get(6..).map(str::trim) {
Some("") | None => return Err(LoadError::MaterialParseError),
Some(tex) => cur_mat.specular_texture = tex.to_owned(),
},
Some("map_Bump") | Some("map_bump") => match line.get(8..).map(str::trim) {
Some("") | None => return Err(LoadError::MaterialParseError),
Some(tex) => cur_mat.normal_texture = tex.to_owned(),
},
Some("map_Ns") | Some("map_ns") | Some("map_NS") => {
match line.get(6..).map(str::trim) {
Some("") | None => return Err(LoadError::MaterialParseError),
Some(tex) => cur_mat.shininess_texture = tex.to_owned(),
}
}
Some("bump") => match line.get(4..).map(str::trim) {
Some("") | None => return Err(LoadError::MaterialParseError),
Some(tex) => cur_mat.normal_texture = tex.to_owned(),
},
Some("map_d") => match line.get(5..).map(str::trim) {
Some("") | None => return Err(LoadError::MaterialParseError),
Some(tex) => cur_mat.dissolve_texture = tex.to_owned(),
},
Some("illum") => {
if let Some(p) = words.next() {
match FromStr::from_str(p) {
Ok(x) => cur_mat.illumination_model = Some(x),
Err(_) => return Err(LoadError::MaterialParseError),
}
} else {
return Err(LoadError::MaterialParseError);
}
}
Some(unknown) => {
if !unknown.is_empty() {
let param = line[unknown.len()..].trim().to_owned();
cur_mat.unknown_param.insert(unknown.to_owned(), param);
}
}
}
}
if !cur_mat.name.is_empty() {
mat_map.insert(cur_mat.name.clone(), materials.len());
materials.push(cur_mat);
}
Ok((materials, mat_map))
}