#![cfg_attr(feature = "merging", allow(incomplete_features))]
#![cfg_attr(feature = "merging", feature(generic_const_exprs))]
#[cfg(test)]
mod tests;
use std::{
error::Error,
fmt,
fs::File,
io::{prelude::*, BufReader},
path::{Path, PathBuf},
str::{FromStr, SplitWhitespace},
};
#[cfg(feature = "use_f64")]
type Float = f64;
#[cfg(not(feature = "use_f64"))]
type Float = f32;
#[cfg(feature = "async")]
use std::future::Future;
#[cfg(feature = "merging")]
use std::mem::size_of;
#[cfg(feature = "ahash")]
type HashMap<K, V> = ahash::AHashMap<K, V>;
#[cfg(not(feature = "ahash"))]
type HashMap<K, V> = std::collections::HashMap<K, V>;
pub const GPU_LOAD_OPTIONS: LoadOptions = LoadOptions {
#[cfg(feature = "merging")]
merge_identical_points: false,
#[cfg(feature = "reordering")]
reorder_data: false,
single_index: true,
triangulate: true,
ignore_points: true,
ignore_lines: true,
};
pub const OFFLINE_RENDERING_LOAD_OPTIONS: LoadOptions = LoadOptions {
#[cfg(feature = "merging")]
merge_identical_points: true,
#[cfg(feature = "reordering")]
reorder_data: true,
single_index: false,
triangulate: false,
ignore_points: true,
ignore_lines: true,
};
#[derive(Debug, Clone, Default)]
pub struct Mesh {
pub positions: Vec<Float>,
pub vertex_color: Vec<Float>,
pub normals: Vec<Float>,
pub texcoords: Vec<Float>,
pub indices: Vec<u32>,
pub face_arities: Vec<u32>,
#[cfg(feature = "merging")]
pub vertex_color_indices: Vec<u32>,
pub texcoord_indices: Vec<u32>,
pub normal_indices: Vec<u32>,
pub material_id: Option<usize>,
}
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[derive(Debug, Default, Clone, Copy)]
pub struct LoadOptions {
#[cfg(feature = "merging")]
pub merge_identical_points: bool,
#[cfg(feature = "reordering")]
pub reorder_data: bool,
pub single_index: bool,
pub triangulate: bool,
pub ignore_points: bool,
pub ignore_lines: bool,
}
impl LoadOptions {
pub fn is_valid(&self) -> bool {
#[allow(unused_mut)]
let mut other_flags = false;
#[cfg(feature = "merging")]
{
other_flags = other_flags || self.merge_identical_points;
}
#[cfg(feature = "reordering")]
{
other_flags = other_flags || self.reorder_data;
}
(self.single_index != other_flags) || (!self.single_index && !other_flags)
}
}
#[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, Default)]
pub struct Material {
pub name: String,
pub ambient: Option<[Float; 3]>,
pub diffuse: Option<[Float; 3]>,
pub specular: Option<[Float; 3]>,
pub shininess: Option<Float>,
pub dissolve: Option<Float>,
pub optical_density: Option<Float>,
pub ambient_texture: Option<String>,
pub diffuse_texture: Option<String>,
pub specular_texture: Option<String>,
pub normal_texture: Option<String>,
pub shininess_texture: Option<String>,
pub dissolve_texture: Option<String>,
pub illumination_model: Option<u8>,
pub unknown_param: HashMap<String, String>,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum LoadError {
OpenFileFailed,
ReadError,
UnrecognizedCharacter,
PositionParseError,
NormalParseError,
TexcoordParseError,
FaceParseError,
MaterialParseError,
InvalidObjectName,
InvalidPolygon,
FaceVertexOutOfBounds,
FaceTexCoordOutOfBounds,
FaceNormalOutOfBounds,
FaceColorOutOfBounds,
InvalidLoadOptionConfig,
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::InvalidPolygon => "invalid polygon",
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::FaceColorOutOfBounds => "face vertex color index out of bounds",
LoadError::InvalidLoadOptionConfig => "mutually exclusive load options",
LoadError::GenericFailure => "generic failure",
};
f.write_str(msg)
}
}
impl Error for LoadError {}
pub type LoadResult = Result<(Vec<Model>, Result<Vec<Material>, LoadError>), LoadError>;
pub type MTLLoadResult = Result<(Vec<Material>, HashMap<String, usize>), LoadError>;
#[derive(Hash, Eq, PartialEq, PartialOrd, Ord, Debug, Copy, Clone)]
struct VertexIndices {
pub v: usize,
pub vt: usize,
pub vn: usize,
}
static MISSING_INDEX: usize = usize::MAX;
impl VertexIndices {
fn parse(
face_str: &str,
pos_sz: usize,
tex_sz: usize,
norm_sz: usize,
) -> Option<VertexIndices> {
let mut indices = [MISSING_INDEX; 3];
for i in face_str.split('/').enumerate() {
if !i.1.is_empty() {
match isize::from_str(i.1) {
Ok(x) => {
*indices.get_mut(i.0)? = if x < 0 {
match i.0 {
0 => (pos_sz as isize + x) as _,
1 => (tex_sz as isize + x) as _,
2 => (norm_sz as isize + x) as _,
_ => return None, }
} else {
(x - 1) as _
};
}
Err(_) => return None,
}
}
}
Some(VertexIndices {
v: indices[0],
vt: indices[1],
vn: indices[2],
})
}
}
#[derive(Debug)]
enum Face {
Point(VertexIndices),
Line(VertexIndices, VertexIndices),
Triangle(VertexIndices, VertexIndices, VertexIndices),
Quad(VertexIndices, VertexIndices, VertexIndices, VertexIndices),
Polygon(Vec<VertexIndices>),
}
fn parse_floatn(val_str: &mut SplitWhitespace, vals: &mut Vec<Float>, n: usize) -> bool {
let sz = vals.len();
for p in val_str.take(n) {
match FromStr::from_str(p) {
Ok(x) => vals.push(x),
Err(_) => return false,
}
}
sz + n == vals.len()
}
fn parse_float3(val_str: SplitWhitespace) -> Result<[Float; 3], LoadError> {
let arr: [Float; 3] = val_str
.take(3)
.map(FromStr::from_str)
.collect::<Result<Vec<_>, _>>()
.map_err(|_| LoadError::MaterialParseError)?
.try_into()
.unwrap();
Ok(arr)
}
fn parse_float(val_str: Option<&str>) -> Result<Float, LoadError> {
val_str
.map(FromStr::from_str)
.map_or(Err(LoadError::MaterialParseError), |v| {
v.map_err(|_| LoadError::MaterialParseError)
})
}
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() {
1 => faces.push(Face::Point(indices[0])),
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: &[Float],
v_color: &[Float],
texcoord: &[Float],
normal: &[Float],
) -> Result<(), LoadError> {
match index_map.get(vert) {
Some(&i) => mesh.indices.push(i),
None => {
let v = vert.v;
if v.saturating_mul(3).saturating_add(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 != MISSING_INDEX {
let vt = vert.vt;
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 != MISSING_INDEX {
let vn = vert.vn;
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]);
}
if !v_color.is_empty() {
if v * 3 + 2 >= v_color.len() {
return Err(LoadError::FaceColorOutOfBounds);
}
mesh.vertex_color.push(v_color[v * 3]);
mesh.vertex_color.push(v_color[v * 3 + 1]);
mesh.vertex_color.push(v_color[v * 3 + 2]);
}
let next = index_map.len() as u32;
mesh.indices.push(next);
index_map.insert(*vert, next);
}
}
Ok(())
}
fn export_faces(
pos: &[Float],
v_color: &[Float],
texcoord: &[Float],
normal: &[Float],
faces: &[Face],
mat_id: Option<usize>,
load_options: &LoadOptions,
) -> Result<Mesh, LoadError> {
let mut index_map = HashMap::new();
let mut mesh = Mesh {
material_id: mat_id,
..Default::default()
};
let mut is_all_triangles = true;
for f in faces {
match *f {
Face::Point(ref a) => {
if !load_options.ignore_points {
add_vertex(&mut mesh, &mut index_map, a, pos, v_color, texcoord, normal)?;
if load_options.triangulate {
add_vertex(&mut mesh, &mut index_map, a, pos, v_color, texcoord, normal)?;
add_vertex(&mut mesh, &mut index_map, a, pos, v_color, texcoord, normal)?;
} else {
is_all_triangles = false;
mesh.face_arities.push(1);
}
}
}
Face::Line(ref a, ref b) => {
if !load_options.ignore_lines {
add_vertex(&mut mesh, &mut index_map, a, pos, v_color, texcoord, normal)?;
add_vertex(&mut mesh, &mut index_map, b, pos, v_color, texcoord, normal)?;
if load_options.triangulate {
add_vertex(&mut mesh, &mut index_map, b, pos, v_color, texcoord, normal)?;
} else {
is_all_triangles = false;
mesh.face_arities.push(2);
}
}
}
Face::Triangle(ref a, ref b, ref c) => {
add_vertex(&mut mesh, &mut index_map, a, pos, v_color, texcoord, normal)?;
add_vertex(&mut mesh, &mut index_map, b, pos, v_color, texcoord, normal)?;
add_vertex(&mut mesh, &mut index_map, c, pos, v_color, texcoord, normal)?;
if !load_options.triangulate {
mesh.face_arities.push(3);
}
}
Face::Quad(ref a, ref b, ref c, ref d) => {
add_vertex(&mut mesh, &mut index_map, a, pos, v_color, texcoord, normal)?;
add_vertex(&mut mesh, &mut index_map, b, pos, v_color, texcoord, normal)?;
add_vertex(&mut mesh, &mut index_map, c, pos, v_color, texcoord, normal)?;
if load_options.triangulate {
add_vertex(&mut mesh, &mut index_map, a, pos, v_color, texcoord, normal)?;
add_vertex(&mut mesh, &mut index_map, c, pos, v_color, texcoord, normal)?;
add_vertex(&mut mesh, &mut index_map, d, pos, v_color, texcoord, normal)?;
} else {
add_vertex(&mut mesh, &mut index_map, d, pos, v_color, texcoord, normal)?;
is_all_triangles = false;
mesh.face_arities.push(4);
}
}
Face::Polygon(ref indices) => {
if load_options.triangulate {
let a = indices.first().ok_or(LoadError::InvalidPolygon)?;
let mut b = indices.get(1).ok_or(LoadError::InvalidPolygon)?;
for c in indices.iter().skip(2) {
add_vertex(&mut mesh, &mut index_map, a, pos, v_color, texcoord, normal)?;
add_vertex(&mut mesh, &mut index_map, b, pos, v_color, texcoord, normal)?;
add_vertex(&mut mesh, &mut index_map, c, pos, v_color, texcoord, normal)?;
b = c;
}
} else {
for i in indices.iter() {
add_vertex(&mut mesh, &mut index_map, i, pos, v_color, texcoord, normal)?;
}
is_all_triangles = false;
mesh.face_arities.push(indices.len() as u32);
}
}
}
}
if is_all_triangles {
mesh.face_arities = Vec::new();
}
Ok(mesh)
}
#[allow(clippy::too_many_arguments)]
#[inline]
fn add_vertex_multi_index(
mesh: &mut Mesh,
index_map: &mut HashMap<usize, u32>,
normal_index_map: &mut HashMap<usize, u32>,
texcoord_index_map: &mut HashMap<usize, u32>,
vert: &VertexIndices,
pos: &[Float],
v_color: &[Float],
texcoord: &[Float],
normal: &[Float],
) -> Result<(), LoadError> {
match index_map.get(&vert.v) {
Some(&i) => mesh.indices.push(i),
None => {
let vertex = vert.v;
if vertex.saturating_mul(3).saturating_add(2) >= pos.len() {
return Err(LoadError::FaceVertexOutOfBounds);
}
mesh.positions.push(pos[vertex * 3]);
mesh.positions.push(pos[vertex * 3 + 1]);
mesh.positions.push(pos[vertex * 3 + 2]);
let next = index_map.len() as u32;
mesh.indices.push(next);
index_map.insert(vertex, next);
if !v_color.is_empty() {
let vertex = vert.v;
if vertex * 3 + 2 >= v_color.len() {
return Err(LoadError::FaceColorOutOfBounds);
}
mesh.vertex_color.push(v_color[vertex * 3]);
mesh.vertex_color.push(v_color[vertex * 3 + 1]);
mesh.vertex_color.push(v_color[vertex * 3 + 2]);
}
}
}
if !texcoord.is_empty() {
let texcoord_indices = &mut mesh.texcoord_indices;
if MISSING_INDEX == vert.vt {
if texcoord_indices.is_empty() {
mesh.texcoords.push(texcoord[0]);
mesh.texcoords.push(texcoord[1]);
texcoord_indices.push(0);
texcoord_index_map.insert(0, 0);
} else {
texcoord_indices.push(*texcoord_indices.last().unwrap());
}
} else {
match texcoord_index_map.get(&vert.vt) {
Some(&index) => mesh.texcoord_indices.push(index as _),
None => {
let vt = vert.vt;
if vt * 2 + 1 >= texcoord.len() {
return Err(LoadError::FaceTexCoordOutOfBounds);
}
mesh.texcoords.push(texcoord[vt * 2]);
mesh.texcoords.push(texcoord[vt * 2 + 1]);
let next = texcoord_index_map.len() as u32;
mesh.texcoord_indices.push(next);
texcoord_index_map.insert(vt, next);
}
}
}
}
if !normal.is_empty() {
let normal_indices = &mut mesh.normal_indices;
if MISSING_INDEX == vert.vn {
if normal_indices.is_empty() {
mesh.normals.push(normal[0]);
mesh.normals.push(normal[1]);
mesh.normals.push(normal[2]);
normal_indices.push(0);
normal_index_map.insert(0, 0);
} else {
normal_indices.push(*normal_indices.last().unwrap());
}
} else {
match normal_index_map.get(&vert.vn) {
Some(&index) => normal_indices.push(index as _),
None => {
let vn = vert.vn;
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 = normal_index_map.len() as u32;
normal_indices.push(next);
normal_index_map.insert(vn, next);
}
}
}
}
Ok(())
}
fn export_faces_multi_index(
pos: &[Float],
v_color: &[Float],
texcoord: &[Float],
normal: &[Float],
faces: &[Face],
mat_id: Option<usize>,
load_options: &LoadOptions,
) -> Result<Mesh, LoadError> {
let mut index_map = HashMap::new();
let mut normal_index_map = HashMap::new();
let mut texcoord_index_map = HashMap::new();
let mut mesh = Mesh {
material_id: mat_id,
..Default::default()
};
let mut is_all_triangles = true;
for f in faces {
match *f {
Face::Point(ref a) => {
if !load_options.ignore_points {
add_vertex_multi_index(
&mut mesh,
&mut index_map,
&mut normal_index_map,
&mut texcoord_index_map,
a,
pos,
v_color,
texcoord,
normal,
)?;
if load_options.triangulate {
add_vertex_multi_index(
&mut mesh,
&mut index_map,
&mut normal_index_map,
&mut texcoord_index_map,
a,
pos,
v_color,
texcoord,
normal,
)?;
add_vertex_multi_index(
&mut mesh,
&mut index_map,
&mut normal_index_map,
&mut texcoord_index_map,
a,
pos,
v_color,
texcoord,
normal,
)?;
} else {
is_all_triangles = false;
mesh.face_arities.push(1);
}
}
}
Face::Line(ref a, ref b) => {
if !load_options.ignore_lines {
add_vertex_multi_index(
&mut mesh,
&mut index_map,
&mut normal_index_map,
&mut texcoord_index_map,
a,
pos,
v_color,
texcoord,
normal,
)?;
add_vertex_multi_index(
&mut mesh,
&mut index_map,
&mut normal_index_map,
&mut texcoord_index_map,
b,
pos,
v_color,
texcoord,
normal,
)?;
if load_options.triangulate {
add_vertex_multi_index(
&mut mesh,
&mut index_map,
&mut normal_index_map,
&mut texcoord_index_map,
b,
pos,
v_color,
texcoord,
normal,
)?;
} else {
is_all_triangles = false;
mesh.face_arities.push(2);
}
}
}
Face::Triangle(ref a, ref b, ref c) => {
add_vertex_multi_index(
&mut mesh,
&mut index_map,
&mut normal_index_map,
&mut texcoord_index_map,
a,
pos,
v_color,
texcoord,
normal,
)?;
add_vertex_multi_index(
&mut mesh,
&mut index_map,
&mut normal_index_map,
&mut texcoord_index_map,
b,
pos,
v_color,
texcoord,
normal,
)?;
add_vertex_multi_index(
&mut mesh,
&mut index_map,
&mut normal_index_map,
&mut texcoord_index_map,
c,
pos,
v_color,
texcoord,
normal,
)?;
if !load_options.triangulate {
mesh.face_arities.push(3);
}
}
Face::Quad(ref a, ref b, ref c, ref d) => {
add_vertex_multi_index(
&mut mesh,
&mut index_map,
&mut normal_index_map,
&mut texcoord_index_map,
a,
pos,
v_color,
texcoord,
normal,
)?;
add_vertex_multi_index(
&mut mesh,
&mut index_map,
&mut normal_index_map,
&mut texcoord_index_map,
b,
pos,
v_color,
texcoord,
normal,
)?;
add_vertex_multi_index(
&mut mesh,
&mut index_map,
&mut normal_index_map,
&mut texcoord_index_map,
c,
pos,
v_color,
texcoord,
normal,
)?;
if load_options.triangulate {
add_vertex_multi_index(
&mut mesh,
&mut index_map,
&mut normal_index_map,
&mut texcoord_index_map,
a,
pos,
v_color,
texcoord,
normal,
)?;
add_vertex_multi_index(
&mut mesh,
&mut index_map,
&mut normal_index_map,
&mut texcoord_index_map,
c,
pos,
v_color,
texcoord,
normal,
)?;
add_vertex_multi_index(
&mut mesh,
&mut index_map,
&mut normal_index_map,
&mut texcoord_index_map,
d,
pos,
v_color,
texcoord,
normal,
)?;
} else {
add_vertex_multi_index(
&mut mesh,
&mut index_map,
&mut normal_index_map,
&mut texcoord_index_map,
d,
pos,
v_color,
texcoord,
normal,
)?;
is_all_triangles = false;
mesh.face_arities.push(4);
}
}
Face::Polygon(ref indices) => {
if load_options.triangulate {
let a = indices.first().ok_or(LoadError::InvalidPolygon)?;
let mut b = indices.get(1).ok_or(LoadError::InvalidPolygon)?;
for c in indices.iter().skip(2) {
add_vertex_multi_index(
&mut mesh,
&mut index_map,
&mut normal_index_map,
&mut texcoord_index_map,
a,
pos,
v_color,
texcoord,
normal,
)?;
add_vertex_multi_index(
&mut mesh,
&mut index_map,
&mut normal_index_map,
&mut texcoord_index_map,
b,
pos,
v_color,
texcoord,
normal,
)?;
add_vertex_multi_index(
&mut mesh,
&mut index_map,
&mut normal_index_map,
&mut texcoord_index_map,
c,
pos,
v_color,
texcoord,
normal,
)?;
b = c;
}
} else {
for i in indices.iter() {
add_vertex_multi_index(
&mut mesh,
&mut index_map,
&mut normal_index_map,
&mut texcoord_index_map,
i,
pos,
v_color,
texcoord,
normal,
)?;
}
is_all_triangles = false;
mesh.face_arities.push(indices.len() as u32);
}
}
}
}
if is_all_triangles {
mesh.face_arities = Vec::new();
}
#[cfg(feature = "merging")]
if load_options.merge_identical_points {
if !mesh.vertex_color.is_empty() {
mesh.vertex_color_indices = mesh.indices.clone();
merge_identical_points::<3>(&mut mesh.vertex_color, &mut mesh.vertex_color_indices);
}
merge_identical_points::<3>(&mut mesh.positions, &mut mesh.indices);
merge_identical_points::<3>(&mut mesh.normals, &mut mesh.normal_indices);
merge_identical_points::<2>(&mut mesh.texcoords, &mut mesh.texcoord_indices);
}
#[cfg(feature = "reordering")]
if load_options.reorder_data {
reorder_data(&mut mesh);
}
Ok(mesh)
}
#[cfg(feature = "reordering")]
#[inline]
fn reorder_data(mesh: &mut Mesh) {
if mesh.positions.len() < mesh.texcoords.len() {
mesh.texcoords = mesh
.texcoord_indices
.iter()
.flat_map(|&index| {
let index = index as usize * 2;
IntoIterator::into_iter([mesh.texcoords[index], mesh.texcoords[index + 1]])
})
.collect::<Vec<_>>();
} else {
assert!(mesh.texcoords.len() == mesh.positions.len());
let mut new_texcoords = vec![0.0; mesh.positions.len()];
mesh.texcoord_indices
.iter()
.zip(&mesh.indices)
.for_each(|(&texcoord_index, &index)| {
let texcoord_index = texcoord_index as usize * 2;
let index = index as usize * 2;
new_texcoords[index] = mesh.texcoords[texcoord_index];
new_texcoords[index + 1] = mesh.texcoords[texcoord_index + 1];
});
mesh.texcoords = new_texcoords;
}
mesh.texcoord_indices = Vec::new();
if mesh.positions.len() < mesh.normals.len() {
mesh.normals = mesh
.normal_indices
.iter()
.flat_map(|&index| {
let index = index as usize * 2;
IntoIterator::into_iter([
mesh.normals[index],
mesh.normals[index + 1],
mesh.normals[index + 2],
])
})
.collect::<Vec<_>>();
} else {
assert!(mesh.normals.len() == mesh.positions.len());
let mut new_normals = vec![0.0; mesh.positions.len()];
mesh.normal_indices
.iter()
.zip(&mesh.indices)
.for_each(|(&normal_index, &index)| {
let normal_index = normal_index as usize * 3;
let index = index as usize * 3;
new_normals[index] = mesh.normals[normal_index];
new_normals[index + 1] = mesh.normals[normal_index + 1];
new_normals[index + 2] = mesh.normals[normal_index + 2];
});
mesh.normals = new_normals;
}
mesh.normal_indices = Vec::new();
}
#[cfg(feature = "merging")]
#[inline]
fn merge_identical_points<const N: usize>(points: &mut Vec<Float>, indices: &mut Vec<u32>)
where
[(); size_of::<[Float; N]>()]:,
{
if indices.is_empty() {
return;
}
let mut compressed_indices = Vec::new();
let mut canonical_indices = HashMap::<[u8; size_of::<[Float; N]>()], u32>::new();
let mut index = 0;
*points = points
.chunks(N)
.filter_map(|position| {
let position: &[Float; N] = &unsafe { *(position.as_ptr() as *const [Float; N]) };
let bitpattern = unsafe {
std::mem::transmute::<&[Float; N], &[u8; size_of::<[Float; N]>()]>(position)
};
match canonical_indices.get(bitpattern) {
Some(&other_index) => {
compressed_indices.push(other_index);
None
}
None => {
canonical_indices.insert(*bitpattern, index);
compressed_indices.push(index);
index += 1;
Some(IntoIterator::into_iter(*position))
}
}
})
.flatten()
.collect();
indices
.iter_mut()
.for_each(|vertex| *vertex = compressed_indices[*vertex as usize]);
}
#[derive(Debug)]
struct TmpModels {
models: Vec<Model>,
pos: Vec<Float>,
v_color: Vec<Float>,
texcoord: Vec<Float>,
normal: Vec<Float>,
faces: Vec<Face>,
name: String,
mat_id: Option<usize>,
}
impl Default for TmpModels {
#[inline]
fn default() -> Self {
Self {
models: Vec::new(),
pos: Vec::new(),
v_color: Vec::new(),
texcoord: Vec::new(),
normal: Vec::new(),
faces: Vec::new(),
name: "unnamed_object".to_owned(),
mat_id: None,
}
}
}
impl TmpModels {
#[inline]
fn new() -> Self {
Self::default()
}
#[inline]
fn pop_model(&mut self, load_options: &LoadOptions) -> Result<(), LoadError> {
self.models.push(Model::new(
if load_options.single_index {
export_faces(
&self.pos,
&self.v_color,
&self.texcoord,
&self.normal,
&self.faces,
self.mat_id,
load_options,
)?
} else {
export_faces_multi_index(
&self.pos,
&self.v_color,
&self.texcoord,
&self.normal,
&self.faces,
self.mat_id,
load_options,
)?
},
self.name.clone(),
));
self.faces.clear();
Ok(())
}
#[inline]
fn into_models(self) -> Vec<Model> {
self.models
}
}
#[derive(Debug)]
struct TmpMaterials {
materials: Vec<Material>,
mat_map: HashMap<String, usize>,
mtlerr: Option<LoadError>,
}
impl Default for TmpMaterials {
#[inline]
fn default() -> Self {
Self {
materials: Vec::new(),
mat_map: HashMap::new(),
mtlerr: None,
}
}
}
impl TmpMaterials {
#[inline]
fn new() -> Self {
Self::default()
}
#[inline]
fn push(&mut self, material: Material) {
self.mat_map
.insert(material.name.clone(), self.materials.len());
self.materials.push(material);
}
#[inline]
fn merge(&mut self, mtl_load_result: MTLLoadResult) {
match mtl_load_result {
Ok((mut mats, map)) => {
let mat_offset = self.materials.len();
self.materials.append(&mut mats);
for m in map {
self.mat_map.insert(m.0, m.1 + mat_offset);
}
}
Err(e) => {
self.mtlerr = Some(e);
}
}
}
#[inline]
fn into_mtl_load_result(self) -> MTLLoadResult {
Ok((self.materials, self.mat_map))
}
#[inline]
fn into_materials(self) -> Result<Vec<Material>, LoadError> {
if !self.materials.is_empty() {
Ok(self.materials)
} else if let Some(mtlerr) = self.mtlerr {
Err(mtlerr)
} else {
Ok(Vec::new())
}
}
}
enum ParseReturnType {
LoadMaterial(PathBuf),
None,
}
#[inline]
fn parse_obj_line(
line: std::io::Result<String>,
load_options: &LoadOptions,
models: &mut TmpModels,
materials: &TmpMaterials,
) -> Result<ParseReturnType, LoadError> {
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 => Ok(ParseReturnType::None),
Some("v") => {
if !parse_floatn(&mut words, &mut models.pos, 3) {
return Err(LoadError::PositionParseError);
}
parse_floatn(&mut words, &mut models.v_color, 3);
Ok(ParseReturnType::None)
}
Some("vt") => {
if !parse_floatn(&mut words, &mut models.texcoord, 2) {
Err(LoadError::TexcoordParseError)
} else {
Ok(ParseReturnType::None)
}
}
Some("vn") => {
if !parse_floatn(&mut words, &mut models.normal, 3) {
Err(LoadError::NormalParseError)
} else {
Ok(ParseReturnType::None)
}
}
Some("f") | Some("l") => {
if !parse_face(
words,
&mut models.faces,
models.pos.len() / 3,
models.texcoord.len() / 2,
models.normal.len() / 3,
) {
Err(LoadError::FaceParseError)
} else {
Ok(ParseReturnType::None)
}
}
Some("o") | Some("g") => {
if !models.faces.is_empty() {
models.pop_model(load_options)?;
}
let size = line.chars().next().unwrap().len_utf8();
models.name = line[size..].trim().to_owned();
if models.name.is_empty() {
models.name = "unnamed_object".to_owned();
}
Ok(ParseReturnType::None)
}
Some("mtllib") => {
let mtllib = line.split_once(' ').unwrap_or_default().1.trim();
let mat_file = Path::new(mtllib).to_path_buf();
Ok(ParseReturnType::LoadMaterial(mat_file))
}
Some("usemtl") => {
let mat_name = line.split_once(' ').unwrap_or_default().1.trim().to_owned();
if !mat_name.is_empty() {
let new_mat = materials.mat_map.get(&mat_name).cloned();
if models.mat_id != new_mat && !models.faces.is_empty() {
models.pop_model(load_options)?;
}
if new_mat.is_none() {
#[cfg(feature = "log")]
log::warn!(
"Object {} refers to unfound material: {}",
models.name,
mat_name
);
}
models.mat_id = new_mat;
Ok(ParseReturnType::None)
} else {
Err(LoadError::MaterialParseError)
}
}
Some(_) => Ok(ParseReturnType::None),
}
}
#[inline]
fn parse_mtl_line(
line: std::io::Result<String>,
materials: &mut TmpMaterials,
mut cur_mat: Material,
) -> Result<Material, LoadError> {
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 => {}
Some("newmtl") => {
if !cur_mat.name.is_empty() {
materials.push(cur_mat);
}
cur_mat = Material::default();
cur_mat.name = line[6..].trim().to_owned();
if cur_mat.name.is_empty() {
return Err(LoadError::InvalidObjectName);
}
}
Some("Ka") => cur_mat.ambient = Some(parse_float3(words)?),
Some("Kd") => cur_mat.diffuse = Some(parse_float3(words)?),
Some("Ks") => cur_mat.specular = Some(parse_float3(words)?),
Some("Ns") => cur_mat.shininess = Some(parse_float(words.next())?),
Some("Ni") => cur_mat.optical_density = Some(parse_float(words.next())?),
Some("d") => cur_mat.dissolve = Some(parse_float(words.next())?),
Some("map_Ka") => match line.get(6..).map(str::trim) {
Some("") | None => return Err(LoadError::MaterialParseError),
Some(tex) => cur_mat.ambient_texture = Some(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 = Some(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 = Some(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 = Some(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 = Some(tex.to_owned()),
},
Some("bump") => match line.get(4..).map(str::trim) {
Some("") | None => return Err(LoadError::MaterialParseError),
Some(tex) => cur_mat.normal_texture = Some(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 = Some(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);
}
}
}
Ok(cur_mat)
}
pub fn load_obj<P>(file_name: P, load_options: &LoadOptions) -> 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, load_options, |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,
load_options: &LoadOptions,
material_loader: ML,
) -> LoadResult
where
B: BufRead,
ML: Fn(&Path) -> MTLLoadResult,
{
if !load_options.is_valid() {
return Err(LoadError::InvalidLoadOptionConfig);
}
let mut models = TmpModels::new();
let mut materials = TmpMaterials::new();
for line in reader.lines() {
let parse_return = parse_obj_line(line, load_options, &mut models, &materials)?;
match parse_return {
ParseReturnType::LoadMaterial(mat_file) => {
materials.merge(material_loader(mat_file.as_path()));
}
ParseReturnType::None => {}
}
}
models.pop_model(load_options)?;
Ok((models.into_models(), materials.into_materials()))
}
pub fn load_mtl_buf<B: BufRead>(reader: &mut B) -> MTLLoadResult {
let mut materials = TmpMaterials::new();
let mut cur_mat = Material::default();
for line in reader.lines() {
cur_mat = parse_mtl_line(line, &mut materials, cur_mat)?;
}
if !cur_mat.name.is_empty() {
materials.push(cur_mat);
}
materials.into_mtl_load_result()
}
#[cfg(feature = "async")]
#[deprecated(
since = "4.0.3",
note = "load_obj_buf_async is not fully async. Use futures/tokio feature flags instead"
)]
pub async fn load_obj_buf_async<B, ML, MLFut>(
reader: &mut B,
load_options: &LoadOptions,
material_loader: ML,
) -> LoadResult
where
B: BufRead,
ML: Fn(String) -> MLFut,
MLFut: Future<Output = MTLLoadResult>,
{
if !load_options.is_valid() {
return Err(LoadError::InvalidLoadOptionConfig);
}
let mut models = TmpModels::new();
let mut materials = TmpMaterials::new();
for line in reader.lines() {
let parse_return = parse_obj_line(line, load_options, &mut models, &materials)?;
match parse_return {
ParseReturnType::LoadMaterial(mat_file) => {
match mat_file.into_os_string().into_string() {
Ok(mat_file) => materials.merge(material_loader(mat_file).await),
Err(_mat_file) => {
#[cfg(feature = "log")]
log::error!(
"load_obj - material path contains invalid Unicode: {_mat_file:?}"
);
return Err(LoadError::ReadError);
}
}
}
ParseReturnType::None => {}
}
}
models.pop_model(load_options)?;
Ok((models.into_models(), materials.into_materials()))
}
#[cfg(feature = "futures")]
pub mod futures {
use super::*;
use futures_lite::{pin, AsyncBufRead, AsyncBufReadExt, StreamExt};
pub async fn load_obj_buf<B, ML, MLFut>(
reader: B,
load_options: &LoadOptions,
material_loader: ML,
) -> LoadResult
where
B: AsyncBufRead,
ML: Fn(PathBuf) -> MLFut,
MLFut: Future<Output = MTLLoadResult>,
{
if !load_options.is_valid() {
return Err(LoadError::InvalidLoadOptionConfig);
}
let mut models = TmpModels::new();
let mut materials = TmpMaterials::new();
pin!(reader);
let mut lines = reader.lines();
while let Some(line) = lines.next().await {
let parse_return = parse_obj_line(line, load_options, &mut models, &materials)?;
match parse_return {
ParseReturnType::LoadMaterial(mat_file) => {
materials.merge(material_loader(mat_file).await);
}
ParseReturnType::None => {}
}
}
models.pop_model(load_options)?;
Ok((models.into_models(), materials.into_materials()))
}
pub async fn load_mtl_buf<B: AsyncBufRead>(reader: B) -> MTLLoadResult {
let mut materials = TmpMaterials::new();
let mut cur_mat = Material::default();
pin!(reader);
let mut lines = reader.lines();
while let Some(line) = lines.next().await {
cur_mat = parse_mtl_line(line, &mut materials, cur_mat)?;
}
if !cur_mat.name.is_empty() {
materials.push(cur_mat);
}
materials.into_mtl_load_result()
}
}
#[cfg(feature = "tokio")]
pub mod tokio {
use super::*;
use ::tokio::fs::File;
use ::tokio::io::{AsyncBufRead, AsyncBufReadExt, BufReader};
use ::tokio::pin;
pub async fn load_obj<P>(file_name: P, load_options: &LoadOptions) -> LoadResult
where
P: AsRef<Path> + fmt::Debug,
{
let file = match File::open(file_name.as_ref()).await {
Ok(f) => f,
Err(_e) => {
#[cfg(feature = "log")]
log::error!("load_obj - failed to open {:?} due to {}", file_name, _e);
return Err(LoadError::OpenFileFailed);
}
};
load_obj_buf(BufReader::new(file), load_options, |mat_path| {
let file_name: &Path = file_name.as_ref();
let file_name = file_name.to_path_buf();
async move {
let full_path = if let Some(parent) = file_name.parent() {
parent.join(mat_path)
} else {
mat_path
};
load_mtl(full_path).await
}
})
.await
}
pub async fn load_mtl<P>(file_name: P) -> MTLLoadResult
where
P: AsRef<Path> + fmt::Debug,
{
let file = match File::open(file_name.as_ref()).await {
Ok(f) => f,
Err(_e) => {
#[cfg(feature = "log")]
log::error!("load_mtl - failed to open {:?} due to {}", file_name, _e);
return Err(LoadError::OpenFileFailed);
}
};
load_mtl_buf(BufReader::new(file)).await
}
pub async fn load_obj_buf<B, ML, MLFut>(
reader: B,
load_options: &LoadOptions,
material_loader: ML,
) -> LoadResult
where
B: AsyncBufRead,
ML: Fn(PathBuf) -> MLFut,
MLFut: Future<Output = MTLLoadResult>,
{
if !load_options.is_valid() {
return Err(LoadError::InvalidLoadOptionConfig);
}
let mut models = TmpModels::new();
let mut materials = TmpMaterials::new();
pin!(reader);
let mut lines = reader.lines();
while let Some(line) = lines.next_line().await.transpose() {
let parse_return = parse_obj_line(line, load_options, &mut models, &materials)?;
match parse_return {
ParseReturnType::LoadMaterial(mat_file) => {
materials.merge(material_loader(mat_file).await);
}
ParseReturnType::None => {}
}
}
models.pop_model(load_options)?;
Ok((models.into_models(), materials.into_materials()))
}
pub async fn load_mtl_buf<B: AsyncBufRead>(reader: B) -> MTLLoadResult {
let mut materials = TmpMaterials::new();
let mut cur_mat = Material::default();
pin!(reader);
let mut lines = reader.lines();
while let Some(line) = lines.next_line().await.transpose() {
cur_mat = parse_mtl_line(line, &mut materials, cur_mat)?;
}
if !cur_mat.name.is_empty() {
materials.push(cur_mat);
}
materials.into_mtl_load_result()
}
}