From 97033c087163e662edb8bdaccce7ab831beae67b Mon Sep 17 00:00:00 2001 From: IllusionMan1212 Date: Fri, 20 Oct 2023 00:34:54 +0200 Subject: [PATCH] importer/obj: basic material handling. dont panic when encountering v/vt components --- TODO.md | 1 + src/importer/mod.rs | 71 ++++++++++++- src/importer/obj.rs | 236 +++++++++++++++++++++++++++++++++++++------- src/mesh.rs | 40 ++------ src/model.rs | 8 +- 5 files changed, 281 insertions(+), 75 deletions(-) diff --git a/TODO.md b/TODO.md index 077e161..6ddd4b6 100644 --- a/TODO.md +++ b/TODO.md @@ -26,5 +26,6 @@ - only select the last one if we removed the selected model - [x] Use filename for the root node - [x] Ship shaders directly in the executable + - [ ] Have the program accept model files as CLI args - [ ] Bring back the regular camera movement (can maybe be a switch between them) (for later) diff --git a/src/importer/mod.rs b/src/importer/mod.rs index 9bd5f16..d17d863 100644 --- a/src/importer/mod.rs +++ b/src/importer/mod.rs @@ -1,14 +1,81 @@ mod obj; -use std::path::PathBuf; +use std::{path::PathBuf, collections::HashMap}; use crate::{mesh::Vertex, aabb::AABB}; +#[derive(Debug, Clone)] +pub enum TextureType { + Ambient, + Diffuse, + Specular, + SpecularHighlight, + Bump, + Displacement, + Decal, + Reflection, + Emissive +} + +#[derive(Debug, Clone)] +pub struct Material { + pub name: String, + pub ambient_color: glm::Vec3, + pub diffuse_color: glm::Vec3, + pub specular_color: glm::Vec3, + pub specular_exponent: f32, + pub opacity: f32, + pub textures: HashMap, +} + +impl Material { + fn new(name: String, ambient: glm::Vec3, diffuse: glm::Vec3, specular: glm::Vec3, shininess: f32, opacity: f32, textures: HashMap) -> Self { + Self { + name, + ambient_color: ambient, + diffuse_color: diffuse, + specular_color: specular, + specular_exponent: shininess, + opacity, + textures + } + } +} + +impl Default for Material { + fn default() -> Self { + Self { + name: "default_mat".to_string(), + ambient_color: glm::vec3(0.4, 0.4, 0.4), + diffuse_color: glm::vec3(0.7, 0.7, 0.7), + specular_color: glm::vec3(0.1, 0.1, 0.1), + specular_exponent: 32.0, + opacity: 1.0, + textures: HashMap::new() + } + } +} + +impl std::fmt::Display for Material { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!( + f, + "Ambient: {:?}\nDiffuse: {:?}\nSpecular: {:?}\nShininess: {}\nOpacity: {}", + self.ambient_color, + self.diffuse_color, + self.specular_color, + self.specular_exponent, + self.opacity + ) + } +} + #[derive(Debug)] pub struct ObjMesh { pub name: String, pub vertices: Vec, pub indices: Vec, + pub material: Option, } #[derive(Debug)] @@ -26,6 +93,6 @@ pub fn load_from_file(path: &PathBuf) -> Result(s: &'a str) -> Option { +impl ObjToken { + fn from_str<'a>(s: &'a str) -> Option { match s { - "o" => Some(Token::Object), - "g" => Some(Token::Group), - "v" => Some(Token::Vertex), - "vn" => Some(Token::Normal), - "vt" => Some(Token::TexCoord), - "f" => Some(Token::Face), - "p" => Some(Token::Point), - "l" => Some(Token::Line), - "s" => Some(Token::SmoothShading), - "mtllib" => Some(Token::MaterialLib), - "usemtl" => Some(Token::MaterialUsage), + "o" => Some(ObjToken::Object), + "g" => Some(ObjToken::Group), + "v" => Some(ObjToken::Vertex), + "vn" => Some(ObjToken::Normal), + "vt" => Some(ObjToken::TexCoord), + "f" => Some(ObjToken::Face), + "p" => Some(ObjToken::Point), + "l" => Some(ObjToken::Line), + "s" => Some(ObjToken::SmoothShading), + "mtllib" => Some(ObjToken::MaterialLib), + "usemtl" => Some(ObjToken::MaterialUsage), _ => None, } } } -pub fn load_obj(file: std::fs::File) -> Result> { +enum MtlToken { + NewMaterial, + AmbientColor, + DiffuseColor, + SpecularColor, + Emissive, + SpecularExponent, + Refraction, + Opacity, + AmbientTexture, + DiffuseTexture, + SpecularTexture, + SpecularHighlightTexture, + EmissiveTexture, + BumpTexture, + DisplacementTexture, + DecalTexture, + ReflectionTexture, +} + +impl MtlToken { + fn from_str(s: &str) -> Option { + match s { + "newmtl" => Some(MtlToken::NewMaterial), + "Ka" => Some(MtlToken::AmbientColor), + "Kd" => Some(MtlToken::DiffuseColor), + "Ks" => Some(MtlToken::SpecularColor), + "Ke" => Some(MtlToken::Emissive), + "Ns" => Some(MtlToken::SpecularExponent), + "Ni" => Some(MtlToken::Refraction), + "d" => Some(MtlToken::Opacity), + "map_Ka" => Some(MtlToken::AmbientTexture), + "map_Kd" => Some(MtlToken::DiffuseTexture), + "map_Ks" => Some(MtlToken::SpecularTexture), + "map_Ns" => Some(MtlToken::SpecularHighlightTexture), + "map_Ke" => Some(MtlToken::EmissiveTexture), + "map_bump" => Some(MtlToken::BumpTexture), + "map_d" => Some(MtlToken::DisplacementTexture), + "decal" => Some(MtlToken::DecalTexture), + "refl" => Some(MtlToken::ReflectionTexture), + _ => None, + } + } +} + +fn parse_mtl(path: &PathBuf) -> Result, Box> { + let file = std::fs::File::open(path)?; + let reader = BufReader::with_capacity(BUF_CAP, file); + let mut material_name = String::new(); + let mut materials: HashMap = HashMap::new(); + let mut material; + let mut ambient = glm::vec3(0.0, 0.0, 0.0); + let mut diffuse = glm::vec3(0.0, 0.0, 0.0); + let mut specular = glm::vec3(0.0, 0.0, 0.0); + let mut shininess = 32.0; + let mut opacity = 1.0; + + for line in reader.lines() { + let line = line?; + // skip empty lines and comments + if line.is_empty() || line.chars().nth(0).is_some_and(|c| c == '#') { + continue; + } + + let mut iter = line.split_ascii_whitespace(); + let first = iter.next(); + if let Some(token) = first { + match MtlToken::from_str(token) { + Some(MtlToken::NewMaterial) => { + if !material_name.is_empty() { + material = Material::new(material_name.clone(), ambient, diffuse, specular, shininess, opacity, HashMap::new()); + materials.insert(material_name, material); + } + + material_name = iter.next().unwrap().to_string(); + } + Some(MtlToken::AmbientColor) => { + let r = iter.next().unwrap().parse::().unwrap(); + let g = iter.next().unwrap().parse::().unwrap(); + let b = iter.next().unwrap().parse::().unwrap(); + ambient = glm::vec3(r, g, b); + } + Some(MtlToken::DiffuseColor) => { + let r = iter.next().unwrap().parse::().unwrap(); + let g = iter.next().unwrap().parse::().unwrap(); + let b = iter.next().unwrap().parse::().unwrap(); + diffuse = glm::vec3(r, g, b); + } + Some(MtlToken::SpecularColor) => { + let r = iter.next().unwrap().parse::().unwrap(); + let g = iter.next().unwrap().parse::().unwrap(); + let b = iter.next().unwrap().parse::().unwrap(); + specular = glm::vec3(r, g, b); + } + Some(MtlToken::SpecularExponent) => { + shininess = iter.next().unwrap().parse::().unwrap(); + } + Some(MtlToken::Opacity) => { + opacity = iter.next().unwrap().parse::().unwrap(); + } + Some(MtlToken::DiffuseTexture) => { + // TODO: textures + } + _ => { println!("Unhandled material token: {}", token) }, + } + } + } + + material = Material::new(material_name.clone(), ambient, diffuse, specular, shininess, opacity, HashMap::new()); + + materials.insert(material_name, material); + + Ok(materials) +} + +pub fn load_obj(obj_path: &PathBuf, file: std::fs::File) -> Result> { let now = std::time::Instant::now(); let reader = BufReader::with_capacity(BUF_CAP, file); let mut object_name = String::new(); @@ -49,6 +164,8 @@ pub fn load_obj(file: std::fs::File) -> Result = HashMap::new(); + let mut current_material: Option = None; let mut min_aabb = glm::vec3(std::f32::MAX, std::f32::MAX, std::f32::MAX); let mut max_aabb = glm::vec3(std::f32::MIN, std::f32::MIN, std::f32::MIN); @@ -62,8 +179,8 @@ pub fn load_obj(file: std::fs::File) -> Result { + match ObjToken::from_str(token) { + Some(ObjToken::Object) => { let name = { if current_mesh_name.is_empty() { object_name.clone() @@ -76,6 +193,7 @@ pub fn load_obj(file: std::fs::File) -> Result Result { + Some(ObjToken::Group) => { let name = { if current_mesh_name.is_empty() { object_name.clone() @@ -97,6 +215,7 @@ pub fn load_obj(file: std::fs::File) -> Result Result { + Some(ObjToken::Vertex) => { let mut iter = iter .take(3) .map(|i| i.parse::().unwrap()); @@ -138,7 +257,7 @@ pub fn load_obj(file: std::fs::File) -> Result { + Some(ObjToken::Normal) => { let mut iter = iter .take(3) .map(|i| i.parse::().unwrap()); @@ -147,7 +266,7 @@ pub fn load_obj(file: std::fs::File) -> Result { + Some(ObjToken::TexCoord) => { let mut iter = iter .take(2) .map(|i| i.parse::().unwrap()); @@ -155,16 +274,20 @@ pub fn load_obj(file: std::fs::File) -> Result { + Some(ObjToken::Face) => { // TODO: vertex indices can be negative let face = iter.collect::>(); let mut calculated_normal = glm::vec3(0.0, 0.0, 0.0); if normals.is_empty() { + let part0 = face[0].split("/").next().unwrap(); + let part1 = face[1].split("/").next().unwrap(); + let part2 = face[2].split("/").next().unwrap(); + calculated_normal = glm::normalize(glm::cross( - temp_vertices[face[1].parse::().unwrap() as usize - 1] - temp_vertices[face[0].parse::().unwrap() as usize - 1], - temp_vertices[face[2].parse::().unwrap() as usize - 1] - temp_vertices[face[0].parse::().unwrap() as usize - 1] + temp_vertices[part1.parse::().unwrap() as usize - 1] - temp_vertices[part0.parse::().unwrap() as usize - 1], + temp_vertices[part2.parse::().unwrap() as usize - 1] - temp_vertices[part0.parse::().unwrap() as usize - 1] )); } @@ -178,7 +301,7 @@ pub fn load_obj(file: std::fs::File) -> Result().unwrap() - 1; let t_coords = it.next().unwrap().parse::().unwrap() - 1; @@ -188,6 +311,15 @@ pub fn load_obj(file: std::fs::File) -> Result().unwrap() - 1; + let t_coords = it.next().unwrap().parse::().unwrap() - 1; + vertices.push(Vertex{ + position: *temp_vertices.get(vertex as usize).unwrap(), + normal: calculated_normal, + tex_coords: *tex_coords.get(t_coords as usize).unwrap() + }); } else { let vert = vert.parse::().unwrap() - 1; vertices.push(Vertex{ @@ -207,15 +339,48 @@ pub fn load_obj(file: std::fs::File) -> Result { - // TODO: handle material file + Some(ObjToken::MaterialLib) => { + let material_path = obj_path.parent().unwrap().join(iter.next().unwrap_or("")); + let new_materials = parse_mtl(&material_path); + match new_materials { + Ok(m) => { + materials.extend(m); + }, + Err(e) => { + eprintln!("Failed to parse mtl file {:?}: {}", material_path, e); + } + } } - Some(Token::MaterialUsage) => { - // TODO: handle material usage - // this can be used multiple times before f is mentioned - // to apply the material to those faces after it + Some(ObjToken::MaterialUsage) => { + // Split into meshes by material usage + let name = { + if current_mesh_name.is_empty() && !object_name.is_empty() { + object_name.clone() + } else if !current_mesh_name.is_empty() { + current_mesh_name.clone() + } else { + "default_mesh".to_string() + } + }; + if !vertices.is_empty() { + meshes.push(ObjMesh{ + name, + vertices: vertices.clone(), + indices: indices.clone(), + material: current_material.clone() + }); + } + vertices.clear(); + indices.clear(); + indices_counter = 0; + + let mat_name = iter.next().unwrap_or("").to_string(); + current_material = materials.get(&mat_name).cloned(); } - _ => {}, + Some(ObjToken::Line) | Some(ObjToken::Point) => { + // we don't handle lines or points + } + _ => { println!("Unhandled obj token: {}", token) }, } } } @@ -236,6 +401,7 @@ pub fn load_obj(file: std::fs::File) -> Result Result (glm::Vec3, glm::Vec3, glm::Vec3) { let pos = glm::vec3(matrix.c0.w, matrix.c1.w, matrix.c2.w); @@ -89,7 +89,7 @@ pub struct Mesh { } impl Mesh { - pub fn new(name: &str, vertices: Vec, indices: Vec) -> Mesh { + pub fn new(name: &str, vertices: Vec, indices: Vec, material: Option) -> Mesh { let mut vao = 0; let mut vbo = 0; let mut ebo = 0; @@ -129,7 +129,7 @@ impl Mesh { vertices, indices, // textures, - material: Material::new("default".to_string(), glm::vec3(0.4, 0.4, 0.4), glm::vec3(0.7, 0.7, 0.7), glm::vec3(0.1, 0.1, 0.1), 32.0), + material: material.unwrap_or_default(), vbo, vao, ebo, @@ -151,10 +151,10 @@ impl Mesh { let model_mat = glm::ext::translate(&model_mat, glm::vec3(self.position.x, self.position.y, self.position.z)); shader.set_mat4fv("model", &model_mat); - shader.set_3fv("material.ambient", self.material.ambient); - shader.set_3fv("material.diffuse", self.material.diffuse); - shader.set_3fv("material.specular", self.material.specular); - shader.set_float("material.shininess", self.material.shininess); + shader.set_3fv("material.ambient", self.material.ambient_color); + shader.set_3fv("material.diffuse", self.material.diffuse_color); + shader.set_3fv("material.specular", self.material.specular_color); + shader.set_float("material.shininess", self.material.specular_exponent); // for i in 0..self.textures.len() { // unsafe { @@ -248,29 +248,3 @@ impl Texture { } } -#[derive(Debug)] -pub struct Material { - pub name: String, - pub ambient: glm::Vec3, - pub diffuse: glm::Vec3, - pub specular: glm::Vec3, - pub shininess: f32, -} - -impl Material { - pub fn new(name: String, ambient: glm::Vec3, diffuse: glm::Vec3, specular: glm::Vec3, shininess: f32) -> Self { - Material { - name, - ambient, - diffuse, - specular, - shininess, - } - } -} - -impl std::fmt::Display for Material { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "Ambient: {:?}\nDiffuse: {:?}\nSpecular: {:?}\nShininess: {}", self.ambient, self.diffuse, self.specular, self.shininess) - } -} diff --git a/src/model.rs b/src/model.rs index cc6aa7c..a537b0b 100644 --- a/src/model.rs +++ b/src/model.rs @@ -1,10 +1,8 @@ -use std::path::PathBuf; - -use crate::{mesh::{Mesh, Vertex, Texture, Material, apply_rotation}, shader::Shader, utils, ui::ui, log, aabb, importer}; - use russimp; use anyhow::{Result, anyhow}; +use crate::{mesh::{Mesh, Vertex, Texture, apply_rotation}, shader::Shader, utils, ui::ui, log, aabb, importer}; + const SUPPORTED_TEXTURE_TYPES: [russimp::material::TextureType; 2] = [ russimp::material::TextureType::Diffuse, russimp::material::TextureType::Specular, @@ -285,7 +283,7 @@ impl Model { let mut meshes = Vec::new(); for mesh in obj.meshes.into_iter() { - meshes.push(Mesh::new(&mesh.name, mesh.vertices, mesh.indices)); + meshes.push(Mesh::new(&mesh.name, mesh.vertices, mesh.indices, mesh.material)); } Model {