diff --git a/Cargo.lock b/Cargo.lock index 377e08d..53904c6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1456,6 +1456,7 @@ dependencies = [ "rfd", "serde", "simplelog", + "xml", ] [[package]] @@ -1830,6 +1831,21 @@ dependencies = [ "memchr 2.6.4", ] +[[package]] +name = "xml" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cb5b0add8d5aaa69357818de5f3b60104a62ef345dca81d0947e2148347a7f7" +dependencies = [ + "xml-rs", +] + +[[package]] +name = "xml-rs" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fcb9cbac069e033553e8bb871be2fbdffcab578eb25bd0f7c508cedc6dcd75a" + [[package]] name = "zune-inflate" version = "0.2.54" diff --git a/Cargo.toml b/Cargo.toml index 760ad7e..a1bb85b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ bincode = "1.3.3" fs4 = "0.7.0" simplelog = "0.12.1" log = "0.4.20" +xml = "0.8.10" [dependencies.imgui] version = "0.11.0" diff --git a/src/importer/collada.rs b/src/importer/collada.rs new file mode 100644 index 0000000..8d7a955 --- /dev/null +++ b/src/importer/collada.rs @@ -0,0 +1,370 @@ +use std::{io::BufReader, collections::HashMap}; + +use crate::{importer::{Object, ObjMesh}, aabb::AABB, mesh::{Mesh, Vertex}}; + +use log::{info, warn}; +use xml::reader::{EventReader, XmlEvent}; + +enum ColladaElement { + // Asset, + // LibraryAnimations, + // LibraryAnimationClips, + // LibraryCameras, + // LibraryControllers, + // LibraryForceFields, + // LibraryImages, + // LibraryLights, + // LibraryNodes, + // LibraryPhysicsMaterials, + // LibraryPhysicsModels, + // LibraryPhysicsScenes, + // LibraryVisualScenes, + // Scene, +} + +#[derive(Debug)] +enum SupportedElement { + None, + + Unit, + UpAxis, + + LibraryMaterials, + Material, + InstanceEffect, + + LibraryEffects, + Effect, + Technique, // might remove this + Diffuse, // vec4 + Shininess, + + LibraryGeometries, + Geometry, + Mesh, + Source, + Input, + FloatArray, + Polylist, + VCount, + Triangles, + P, +} + +impl SupportedElement { + fn from_str(s: &str) -> Option { + match s { + "unit" => Some(Self::Unit), + "up_axis" => Some(Self::UpAxis), + + "library_materials" => Some(Self::LibraryMaterials), + "material" => Some(Self::Material), + "instance_effect" => Some(Self::InstanceEffect), + + "library_effects" => Some(Self::LibraryEffects), + "effect" => Some(Self::Effect), + "technique" => Some(Self::Technique), + "diffuse" => Some(Self::Diffuse), + "shininess" => Some(Self::Shininess), + + "library_geometries" => Some(Self::LibraryGeometries), + "geometry" => Some(Self::Geometry), + "mesh" => Some(Self::Mesh), + "source" => Some(Self::Source), + "input" => Some(Self::Input), + "float_array" => Some(Self::FloatArray), + "polylist" => Some(Self::Polylist), + "vcount" => Some(Self::VCount), + "triangles" => Some(Self::Triangles), + "p" => Some(Self::P), + + _ => None, + } + } +} + +pub fn load_dae(file: std::fs::File) -> Result> { + let reader = BufReader::new(file); + let parser = EventReader::new(reader); + + let mut chunk_p_by = 0; + let mut current_face_count = 0; + + let mut current_source_id = String::new(); + let mut current_element = SupportedElement::None; + let mut sources = HashMap::>::new(); + let mut vcount = Vec::new(); + let mut positions = Vec::new(); + let mut normals = Vec::new(); + let mut tex_coords = Vec::new(); + + let mut vertices: Vec = Vec::new(); + let mut indices = Vec::new(); + // let meshes = Vec::new(); + // let materials = Vec::new(); + + let mut min_aabb = glm::vec3(f32::MAX, f32::MAX, f32::MAX); + let mut max_aabb = glm::vec3(f32::MIN, f32::MIN, f32::MIN); + + let now = std::time::Instant::now(); + for e in parser { + match e { + Ok(XmlEvent::StartElement { name, attributes, .. }) => { + current_element = match SupportedElement::from_str(&name.local_name) { + Some(element) => element, + None => { + // warn!("Unsupported element: {}", name.local_name); + SupportedElement::None + } + }; + + match current_element { + SupportedElement::Unit => {}, + SupportedElement::UpAxis => {}, + SupportedElement::LibraryMaterials => {}, + SupportedElement::Source => { + if let Some(source_id) = attributes.into_iter().find(|a| a.name.local_name == "id") { + current_source_id = source_id.value + } + }, + SupportedElement::Input => { + let semantic = &attributes.iter().find(|a| a.name.local_name == "semantic").unwrap().value; + // strip the # at the beginning of the source id + let source_id = &attributes.iter().find(|a| a.name.local_name == "source").unwrap().value[1..]; + + match semantic.as_str() { + "POSITION" => { + positions = sources.get(source_id) + .unwrap() + .chunks(3) + .map(|v| { + let vertex = glm::vec3(v[0], v[1], v[2]); + + min_aabb = glm::vec3( + min_aabb.x.min(vertex.x), + min_aabb.y.min(vertex.y), + min_aabb.z.min(vertex.z), + ); + + max_aabb = glm::vec3( + max_aabb.x.max(vertex.x), + max_aabb.y.max(vertex.y), + max_aabb.z.max(vertex.z), + ); + + vertex + }) + .collect::>(); + } + "NORMAL" => { + chunk_p_by += 1; + + normals = sources.get(source_id) + .unwrap() + .chunks(3) + .map(|v| glm::vec3(v[0], v[1], v[2])) + .collect::>(); + }, + "TEXCOORD" => { + chunk_p_by += 1; + + tex_coords = sources.get(source_id) + .unwrap() + .chunks(2) + .map(|v| glm::vec2(v[0], v[1])) + .collect::>(); + } + // ignore this since it's used to refer to the vertices + // tag and we already parse POSITION from that + "VERTEX" => { + chunk_p_by += 1; + }, + _ => { panic!("Unknown semantic: {}", semantic) } + } + }, + // This element is used to indicate that the vcount + // aka vertex count for every face is 3 without having to + // explicity write the vcount element + SupportedElement::Triangles => { + current_face_count = attributes.into_iter().find(|a| a.name.local_name == "count").unwrap().value.parse::().unwrap(); + + // preemptively allocate the exact amount of vertices we want + vertices = Vec::with_capacity(current_face_count * 3); + + // fill vcount with all 3s for as many faces as we got + // so we can use it later when parsing

+ vcount = vec![3; current_face_count] + }, + // This element can have a varying amount of vertices + // per face and the vcount element is required to be present (i think) + SupportedElement::Polylist => { + current_face_count = attributes.into_iter().find(|a| a.name.local_name == "count").unwrap().value.parse::().unwrap(); + + vcount = Vec::with_capacity(current_face_count); + } + SupportedElement::Geometry => { + // TODO: name attribute for the object? name + }, + _ => {} // Other elements we don't care about (should probably only be None + // here and we can nuke the elements that we don't use from the enum) + } + } + Ok(XmlEvent::EndElement { name }) => { + match SupportedElement::from_str(&name.local_name) { + Some(SupportedElement::P) => { + // reset chunk_by to 0 so we know we're done with the + // current polylist + chunk_p_by = 0; + }, + _ => { /*warn!("Unsupported element: {}", name.local_name)*/ }, + } + // TODO: each polylist has a material. meaning we split the mesh + // into multiple meshes according to the polylists. + // Some models don't have polylist and instead have a triangles + + // TODO: if we encounter mesh, we push it to the vector + + // Mesh::new(); + // println!("End: {}", name); + } + Ok(XmlEvent::Characters(data)) => { + match current_element { + SupportedElement::FloatArray => { + sources.insert( + current_source_id.clone(), + data + .trim() + .lines() + .map(|l| l + .split_ascii_whitespace() + .map(|v| v.parse::().unwrap()) + .collect::>()) + .flatten() + .collect::>() + ); + }, + SupportedElement::P => { + // TODO: get current_v_count by looping over the faces?? + // current_v_count * chunk_p_by that's 1 v count + // we also need to triangulate anything with >3 v_count + // vertices = data + // .trim() + // .lines() + // .map(|l| l + // .split_ascii_whitespace() + // .map(|i| i.parse::().unwrap()) + // .collect::>()) + // .flatten() + // .collect::>() + // .chunks(chunk_p_by) + // .map(|i| Vertex::new(positions[i[0]], normals[i[1]], tex_coords[i[2]])) + // .collect::>(); + + // println!("P: {:?}", vertices.len()); + + // println!("data: {}", data); + let data = data + .trim() + .split_ascii_whitespace() + .map(|i| i.parse::().unwrap()) + .collect::>(); + + for i in 0..current_face_count { + let current_v_count = vcount.get(i).unwrap(); + // TODO: triangulate face if vcount >3 + // TODO: get vertices and indices in one go + + let face = data + .chunks(chunk_p_by * current_v_count) + .skip(i) + .take(1) + .flatten() + .collect::>(); + + // NOTE: no idea if this is fine and doesn't break things + if face.len() == 0 { continue; } + + + // HACK: this assumes that a face only has vertices + // for faces with more, we need to triangulate and then + // push the indices properly (dk how yet) + indices.push(i as u32 * 3); + indices.push(i as u32 * 3 + 1); + indices.push(i as u32 * 3 + 2); + + // normals for this face + let normals = { + // TODO: if we don't have normals. we should generate them + // also take v_count into account here so we triangulate + // the normals immediate as well ??? + // the lternative is to do this twice in each branch + // of the coming if statement. idk + }; + + // tex_coords for this face + let tex_coords = { + // TODO: if we don't have tex_coords. return 0.0, 0.0 + }; + + if *current_v_count > 3 { + // triangulate + } else { + // vertices.push(Vertex::new()); + } + + vertices.extend(face + .chunks(chunk_p_by) + // TODO: v[0], v[1], v[2] is bad cuz im not using chunk_p_by + .map(|v| Vertex::new(positions[*v[0]], normals[*v[1]], tex_coords[*v[2]])) + .collect::>() + ); + } + }, + SupportedElement::VCount => { + vcount = data + .trim() + .lines() + .map(|l| l + .split_ascii_whitespace() + .map(|c| c.parse::().unwrap()) + .collect::>()) + .flatten() + .collect::>(); + + // println!("vcount: {:?}", vcount); + }, + _ => { + // warn!("Unsupported Characters event for element: {:?}", current_element); + } + } + } + Err(e) => { + return Err(Box::new(e)); + } + _ => {} + } + } + + // println!("vcount: {:?}", vcount); + println!("Vertices: {:?}", vertices); + println!("Indices: {:?}", indices); + // println!("Normals: {:?}", normals); + // println!("TexCoords: {:?}", tex_coords); + let elapsed = now.elapsed(); + info!("Loaded in {}ms", elapsed.as_millis()); + + todo!(); + + let aabb = AABB::new(min_aabb, max_aabb); + + // Ok(Object { + // name: "test".to_string(), + // meshes: vec![ObjMesh{ + // name: "test".to_string(), + // vertices, + // indices, + // material: None + // }], + // aabb + // }) +} diff --git a/src/importer/mod.rs b/src/importer/mod.rs index a11fce2..7b02941 100644 --- a/src/importer/mod.rs +++ b/src/importer/mod.rs @@ -1,5 +1,6 @@ mod obj; mod stl; +mod collada; use std::path::PathBuf; @@ -135,6 +136,7 @@ pub fn load_from_file(path: &PathBuf) -> Result stl::load_stl(file)?, Some(SupportedFileExtensions::OBJ) => obj::load_obj(path, file)?, + Some(SupportedFileExtensions::COLLADA) => collada::load_dae(file)?, _ => panic!("Unsupported file extension: {}", path_str), }; diff --git a/src/main.rs b/src/main.rs index 002d12e..c340d14 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,7 +10,7 @@ use simplelog::*; use threedobs::{shader, ui::ui, utils, ipc}; fn main() -> anyhow::Result<(), Box> { - let logger = threedobs::logger::WritableLog::default(); + let in_program_logger = threedobs::logger::WritableLog::default(); let log_conf = ConfigBuilder::default() .set_target_level(LevelFilter::Error) @@ -40,7 +40,7 @@ fn main() -> anyhow::Result<(), Box> { vec![ TermLogger::new(log_level, log_conf.clone(), TerminalMode::Mixed, ColorChoice::Auto), WriteLogger::new(log_level, log_conf, File::create("3dobs.log")?), - WriteLogger::new(log_level, in_program_log_conf, logger.clone()) + WriteLogger::new(log_level, in_program_log_conf, in_program_logger.clone()) ] ).unwrap(); @@ -72,7 +72,7 @@ fn main() -> anyhow::Result<(), Box> { let mut state = ui::State{ settings, - logger, + logger: in_program_logger, ..Default::default() }; diff --git a/src/ui/ui.rs b/src/ui/ui.rs index 3e582b9..aeb7124 100644 --- a/src/ui/ui.rs +++ b/src/ui/ui.rs @@ -111,9 +111,10 @@ pub fn import_model(state: &mut State) { let models = match rfd::FileDialog::new() .set_title("Import Model(s)") .set_directory("./") - .add_filter("All supported files", &["obj", "OBJ", "stl", "STL"]) + .add_filter("All supported files", &["obj", "OBJ", "stl", "STL", "dae", "DAE"]) .add_filter("Wavefront OBJ (.obj)", &["obj", "OBJ"]) .add_filter("STL (.stl)", &["stl", "STL"]) + .add_filter("COLLADA (.dae)", &["dae", "DAE"]) .pick_files() { Some(m) => m, None => return, diff --git a/src/utils.rs b/src/utils.rs index fb03f7e..79df347 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -9,7 +9,8 @@ use crate::{model, ui, importer}; pub enum SupportedFileExtensions { OBJ, - STL + STL, + COLLADA, } impl SupportedFileExtensions { @@ -17,6 +18,7 @@ impl SupportedFileExtensions { match s.to_ascii_lowercase().as_str() { "obj" => Some(Self::OBJ), "stl" => Some(Self::STL), + "dae" => Some(Self::COLLADA), _ => None } }