#[cfg(any(feature = "gltf", feature = "obj"))]
use super::ModelMesh;
use thiserror::Error;
#[derive(Debug, Clone, Error)]
pub enum ModelLoadError {
#[error("parse error: {0}")]
Parse(String),
#[error("no mesh found: {0}")]
NoMesh(String),
}
#[cfg(feature = "gltf")]
impl ModelMesh {
pub fn from_gltf(bytes: &[u8]) -> Result<Self, ModelLoadError> {
let (document, buffers, _images) =
gltf::import_slice(bytes).map_err(|e| ModelLoadError::Parse(e.to_string()))?;
let mesh = document
.meshes()
.next()
.ok_or_else(|| ModelLoadError::NoMesh("glTF contains no meshes".into()))?;
let primitive = mesh
.primitives()
.next()
.ok_or_else(|| ModelLoadError::NoMesh("mesh contains no primitives".into()))?;
let reader = primitive.reader(|buffer| Some(&buffers[buffer.index()]));
let positions: Vec<[f32; 3]> = reader
.read_positions()
.ok_or_else(|| ModelLoadError::NoMesh("primitive has no POSITION attribute".into()))?
.collect();
let normals: Vec<[f32; 3]> = reader
.read_normals()
.map(|iter| iter.collect())
.unwrap_or_else(|| vec![[0.0, 0.0, 1.0]; positions.len()]);
let uvs: Vec<[f32; 2]> = reader
.read_tex_coords(0)
.map(|tc| tc.into_f32().collect())
.unwrap_or_else(|| vec![[0.0, 0.0]; positions.len()]);
let indices: Vec<u32> = reader
.read_indices()
.map(|idx| idx.into_u32().collect())
.unwrap_or_else(|| (0..positions.len() as u32).collect());
Ok(ModelMesh {
positions,
normals,
uvs,
indices,
})
}
}
#[cfg(feature = "obj")]
impl ModelMesh {
pub fn from_obj(bytes: &[u8]) -> Result<Self, ModelLoadError> {
let mut cursor = std::io::Cursor::new(bytes);
let (models, _materials) = tobj::load_obj_buf(
&mut cursor,
&tobj::LoadOptions {
triangulate: true,
single_index: true,
..Default::default()
},
|_path| Err(tobj::LoadError::OpenFileFailed),
)
.map_err(|e| ModelLoadError::Parse(e.to_string()))?;
let model = models
.first()
.ok_or_else(|| ModelLoadError::NoMesh("OBJ contains no models".into()))?;
let mesh = &model.mesh;
let vertex_count = mesh.positions.len() / 3;
let positions: Vec<[f32; 3]> = mesh
.positions
.chunks_exact(3)
.map(|c| [c[0], c[1], c[2]])
.collect();
let normals: Vec<[f32; 3]> = if mesh.normals.len() == mesh.positions.len() {
mesh.normals
.chunks_exact(3)
.map(|c| [c[0], c[1], c[2]])
.collect()
} else {
vec![[0.0, 0.0, 1.0]; vertex_count]
};
let uvs: Vec<[f32; 2]> = if mesh.texcoords.len() >= vertex_count * 2 {
mesh.texcoords
.chunks_exact(2)
.map(|c| [c[0], c[1]])
.collect()
} else {
vec![[0.0, 0.0]; vertex_count]
};
let indices: Vec<u32> = mesh.indices.clone();
Ok(ModelMesh {
positions,
normals,
uvs,
indices,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn model_load_error_display() {
let e = ModelLoadError::Parse("bad data".into());
assert!(e.to_string().contains("bad data"));
let e = ModelLoadError::NoMesh("empty".into());
assert!(e.to_string().contains("empty"));
}
#[cfg(feature = "gltf")]
#[test]
fn from_gltf_invalid_bytes() {
let result = ModelMesh::from_gltf(b"not a gltf file");
assert!(result.is_err());
}
#[cfg(feature = "obj")]
#[test]
fn from_obj_invalid_is_error() {
let result = ModelMesh::from_obj(&[0xFF, 0xFE, 0x00, 0x01]);
let _ = result;
}
#[cfg(feature = "obj")]
#[test]
fn from_obj_triangle() {
let obj = b"\
v 0.0 0.0 0.0
v 1.0 0.0 0.0
v 0.0 1.0 0.0
f 1 2 3
";
let mesh = ModelMesh::from_obj(obj).expect("parse triangle");
assert_eq!(mesh.positions.len(), 3);
assert_eq!(mesh.indices.len(), 3);
assert_eq!(mesh.normals.len(), 3);
assert_eq!(mesh.uvs.len(), 3);
}
}