use crate::mesh3d::{Mesh3D, Vec3, Vertex};
use crate::texture3d::Material3D;
use std::collections::HashMap;
use std::path::Path;
#[derive(Debug)]
pub enum ModelError {
FileNotFound(String),
ParseError(String),
UnsupportedFormat(String),
IoError(String),
}
impl std::fmt::Display for ModelError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
ModelError::FileNotFound(path) => write!(f, "File not found: {}", path),
ModelError::ParseError(msg) => write!(f, "Parse error: {}", msg),
ModelError::UnsupportedFormat(format) => write!(f, "Unsupported format: {}", format),
ModelError::IoError(msg) => write!(f, "IO error: {}", msg),
}
}
}
impl std::error::Error for ModelError {}
pub struct ModelLoader {
cache: HashMap<String, Vec<Mesh3D>>,
materials: HashMap<String, Material3D>,
}
impl ModelLoader {
pub fn new() -> Self {
Self {
cache: HashMap::new(),
materials: HashMap::new(),
}
}
pub fn load(&mut self, path: &str) -> Result<Vec<Mesh3D>, ModelError> {
if let Some(meshes) = self.cache.get(path) {
return Ok(meshes.clone());
}
let extension = Path::new(path)
.extension()
.and_then(|s| s.to_str())
.ok_or_else(|| ModelError::UnsupportedFormat("No extension".to_string()))?;
let meshes = match extension.to_lowercase().as_str() {
"obj" => self.load_obj(path)?,
"fbx" => self.load_fbx(path)?,
_ => return Err(ModelError::UnsupportedFormat(extension.to_string())),
};
self.cache.insert(path.to_string(), meshes.clone());
Ok(meshes)
}
fn load_obj(&mut self, path: &str) -> Result<Vec<Mesh3D>, ModelError> {
let (models, materials) = tobj::load_obj(
path,
&tobj::LoadOptions {
single_index: true,
triangulate: true,
ignore_points: true,
ignore_lines: true,
},
)
.map_err(|e| ModelError::ParseError(format!("OBJ parse error: {:?}", e)))?;
if let Ok(mats) = materials {
for mat in mats {
let material = self.convert_material(&mat);
self.materials.insert(mat.name.clone(), material);
}
}
let mut meshes = Vec::new();
for model in models {
let mesh_data = &model.mesh;
let mut vertices = Vec::new();
for i in 0..mesh_data.positions.len() / 3 {
let pos_idx = i * 3;
let position = Vec3::new(
mesh_data.positions[pos_idx],
mesh_data.positions[pos_idx + 1],
mesh_data.positions[pos_idx + 2],
);
let normal = if !mesh_data.normals.is_empty() {
Vec3::new(
mesh_data.normals[pos_idx],
mesh_data.normals[pos_idx + 1],
mesh_data.normals[pos_idx + 2],
)
} else {
Vec3::new(0.0, 1.0, 0.0)
};
let uv = if !mesh_data.texcoords.is_empty() && i * 2 + 1 < mesh_data.texcoords.len() {
(mesh_data.texcoords[i * 2], mesh_data.texcoords[i * 2 + 1])
} else {
(0.0, 0.0)
};
vertices.push(Vertex {
position,
normal,
uv,
color: [255, 255, 255, 255],
});
}
let mesh = Mesh3D::finalize(vertices, mesh_data.indices.clone());
meshes.push(mesh);
}
Ok(meshes)
}
fn load_fbx(&mut self, _path: &str) -> Result<Vec<Mesh3D>, ModelError> {
Err(ModelError::UnsupportedFormat(
"FBX support coming soon. Use OBJ format for now.".to_string()
))
}
fn convert_material(&self, mat: &tobj::Material) -> Material3D {
let diffuse = mat.diffuse.unwrap_or([1.0, 1.0, 1.0]);
let mut material = Material3D::new([
(diffuse[0] * 255.0) as u8,
(diffuse[1] * 255.0) as u8,
(diffuse[2] * 255.0) as u8,
255,
]);
if let Some(shininess) = mat.shininess {
material = material.with_roughness(1.0 - (shininess / 1000.0).min(1.0));
}
if let Some(ambient) = mat.ambient {
if ambient[0] > 0.0 || ambient[1] > 0.0 || ambient[2] > 0.0 {
material = material.with_emission(
[
(ambient[0] * 255.0) as u8,
(ambient[1] * 255.0) as u8,
(ambient[2] * 255.0) as u8,
255,
],
1.0,
);
}
}
material
}
pub fn clear_cache(&mut self) {
self.cache.clear();
}
pub fn remove_from_cache(&mut self, path: &str) {
self.cache.remove(path);
}
pub fn cached_models(&self) -> Vec<String> {
self.cache.keys().cloned().collect()
}
pub fn load_with_transform(
&mut self,
path: &str,
position: Vec3,
rotation: Vec3,
scale: Vec3,
) -> Result<Vec<Mesh3D>, ModelError> {
let mut meshes = self.load(path)?;
for mesh in &mut meshes {
mesh.position = position;
mesh.rotation = rotation;
mesh.scale = scale;
}
Ok(meshes)
}
pub fn load_with_material(
&mut self,
path: &str,
material: Material3D,
) -> Result<Vec<Mesh3D>, ModelError> {
let mut meshes = self.load(path)?;
for mesh in &mut meshes {
mesh.material = Some(material.clone());
}
Ok(meshes)
}
}
pub struct ModelBuilder {
path: String,
position: Vec3,
rotation: Vec3,
scale: Vec3,
material: Option<Material3D>,
}
impl ModelBuilder {
pub fn new(path: &str) -> Self {
Self {
path: path.to_string(),
position: Vec3::zero(),
rotation: Vec3::zero(),
scale: Vec3::new(1.0, 1.0, 1.0),
material: None,
}
}
pub fn position(mut self, x: f32, y: f32, z: f32) -> Self {
self.position = Vec3::new(x, y, z);
self
}
pub fn rotation(mut self, x: f32, y: f32, z: f32) -> Self {
self.rotation = Vec3::new(x, y, z);
self
}
pub fn scale(mut self, x: f32, y: f32, z: f32) -> Self {
self.scale = Vec3::new(x, y, z);
self
}
pub fn uniform_scale(mut self, scale: f32) -> Self {
self.scale = Vec3::new(scale, scale, scale);
self
}
pub fn material(mut self, material: Material3D) -> Self {
self.material = Some(material);
self
}
pub fn load(self, loader: &mut ModelLoader) -> Result<Vec<Mesh3D>, ModelError> {
let mut meshes = loader.load(&self.path)?;
for mesh in &mut meshes {
mesh.position = self.position;
mesh.rotation = self.rotation;
mesh.scale = self.scale;
if let Some(ref material) = self.material {
mesh.material = Some(material.clone());
}
}
Ok(meshes)
}
}
impl ModelLoader {
pub fn export_cube_obj(path: &str, size: f32) -> Result<(), ModelError> {
let s = size / 2.0;
let obj_content = format!(
"# Cube exported from SevenX Engine\n\
v -{} -{} {}\n\
v {} -{} {}\n\
v {} {} {}\n\
v -{} {} {}\n\
v {} -{} -{}\n\
v -{} -{} -{}\n\
v -{} {} -{}\n\
v {} {} -{}\n\
\n\
vn 0.0 0.0 1.0\n\
vn 0.0 0.0 -1.0\n\
vn 0.0 1.0 0.0\n\
vn 0.0 -1.0 0.0\n\
vn 1.0 0.0 0.0\n\
vn -1.0 0.0 0.0\n\
\n\
f 1//1 2//1 3//1\n\
f 1//1 3//1 4//1\n\
f 5//2 6//2 7//2\n\
f 5//2 7//2 8//2\n\
f 4//3 3//3 8//3\n\
f 4//3 8//3 7//3\n\
f 6//4 5//4 2//4\n\
f 6//4 2//4 1//4\n\
f 2//5 5//5 8//5\n\
f 2//5 8//5 3//5\n\
f 6//6 1//6 4//6\n\
f 6//6 4//6 7//6\n",
s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s
);
std::fs::write(path, obj_content)
.map_err(|e| ModelError::IoError(e.to_string()))
}
pub fn export_mesh_obj(mesh: &Mesh3D, path: &str) -> Result<(), ModelError> {
let mut obj_content = String::from("# Mesh exported from SevenX Engine\n");
for vertex in &mesh.vertices {
obj_content.push_str(&format!(
"v {} {} {}\n",
vertex.position.x, vertex.position.y, vertex.position.z
));
}
obj_content.push('\n');
for vertex in &mesh.vertices {
obj_content.push_str(&format!(
"vn {} {} {}\n",
vertex.normal.x, vertex.normal.y, vertex.normal.z
));
}
obj_content.push('\n');
for vertex in &mesh.vertices {
obj_content.push_str(&format!("vt {} {}\n", vertex.uv.0, vertex.uv.1));
}
obj_content.push('\n');
for i in (0..mesh.indices.len()).step_by(3) {
let i0 = mesh.indices[i] + 1;
let i1 = mesh.indices[i + 1] + 1;
let i2 = mesh.indices[i + 2] + 1;
obj_content.push_str(&format!(
"f {}/{}/{} {}/{}/{} {}/{}/{}\n",
i0, i0, i0, i1, i1, i1, i2, i2, i2
));
}
std::fs::write(path, obj_content)
.map_err(|e| ModelError::IoError(e.to_string()))
}
}