From 3437d4d40f137b3c018c640359fbb21efdfc13e6 Mon Sep 17 00:00:00 2001 From: mars Date: Fri, 20 May 2022 21:18:01 -0600 Subject: [PATCH] WIP scene hierarchy + glTF loading --- editor/Cargo.toml | 1 + editor/src/import.rs | 112 +++++++++++++++++++++++++++++++++++++++++-- editor/src/main.rs | 14 +++--- editor/src/model.rs | 8 +--- editor/src/render.rs | 23 +++++---- editor/src/ui.rs | 40 +++++++++++----- 6 files changed, 162 insertions(+), 36 deletions(-) diff --git a/editor/Cargo.toml b/editor/Cargo.toml index 2e04027..8161f45 100644 --- a/editor/Cargo.toml +++ b/editor/Cargo.toml @@ -11,6 +11,7 @@ egui = "0.17.0" egui-winit = "0.17.0" egui_wgpu_backend = "0.17.0" glam = "0.20" +gltf = { version = "1.0", features = ["utils"] } legion = "^0.4" pollster = "0.2" puffin = "^0.13" diff --git a/editor/src/import.rs b/editor/src/import.rs index 93b0a2a..0749908 100644 --- a/editor/src/import.rs +++ b/editor/src/import.rs @@ -28,11 +28,115 @@ pub fn load_stl(path: std::path::PathBuf) -> model::Model { name: name.clone(), objects: vec![model::Object { name, - mesh: model::Mesh { - transform: Default::default(), - data: model::MeshData { vertices, indices }, - }, + transform: Default::default(), + mesh: Some(model::Mesh { vertices, indices }), children: vec![], }], } } + +pub fn load_gltf(path: std::path::PathBuf) -> model::Model { + use gltf::*; + + let name = path.file_name().map(|v| v.to_string_lossy().to_string()); + let mut file = std::fs::File::open(path).unwrap(); + let model = Gltf::from_reader(&mut file).unwrap(); + + GltfLoader::load(name, &model) +} + +pub struct GltfLoader<'a> { + pub model: &'a gltf::Gltf, + pub buffers: Vec>, +} + +impl<'a> GltfLoader<'a> { + pub fn load(name: Option, model: &'a gltf::Gltf) -> model::Model { + let buffers = Self::load_buffers(model); + let mut loader = Self { model, buffers }; + + let mut model = model::Model { + name, + objects: vec![], + }; + + for scene in loader.model.scenes() { + for node in scene.nodes() { + model.objects.push(loader.load_node(node)); + } + } + + model + } + + pub fn load_buffers(model: &gltf::Gltf) -> Vec> { + let mut buffer_data = Vec::>::new(); + for buffer in model.buffers() { + match buffer.source() { + gltf::buffer::Source::Bin => { + buffer_data.push(model.blob.as_deref().unwrap().into()); + } + _ => panic!("URI buffer sources are unsupported"), + } + } + buffer_data + } + + pub fn load_node(&mut self, node: gltf::Node) -> model::Object { + let transform = model::Transform::default(); + + let mut object = model::Object { + name: node.name().map(str::to_string), + transform, + mesh: None, + children: vec![], + }; + + if let Some(mesh) = node.mesh() { + assert_eq!(1, mesh.primitives().len()); + let primitive = mesh.primitives().next().unwrap(); + object.mesh = Some(self.load_primitive_mesh(primitive)); + } + + for child in node.children() { + object.children.push(self.load_node(child)); + } + + object + } + + pub fn load_primitive_mesh(&mut self, primitive: gltf::Primitive) -> model::Mesh { + use gltf::mesh::util::{ReadIndices, ReadTexCoords}; + if primitive.mode() != gltf::mesh::Mode::Triangles { + panic!("glTF primitive must be triangle list"); + } + + let reader = primitive.reader(|buffer| Some(&self.buffers[buffer.index()])); + let positions = reader.read_positions().unwrap(); + let mut normals = reader.read_normals().unwrap(); + + let tex_coords = reader.read_tex_coords(0).unwrap(); + let mut tex_coords = if let ReadTexCoords::F32(tex_coords) = tex_coords { + tex_coords + } else { + panic!("only f32 texture coordinates are supported") + }; + + let mut vertices = Vec::new(); + for position in positions { + let normal = normals.next().unwrap(); + let tex_coords = tex_coords.next().unwrap(); + vertices.push(model::BasicVertex { + position: position.into(), + }); + } + + let indices = match reader.read_indices().unwrap() { + ReadIndices::U32(indices) => indices.collect(), + ReadIndices::U16(indices) => indices.map(|i| i as u32).collect(), + ReadIndices::U8(indices) => indices.map(|i| i as u32).collect(), + }; + + model::Mesh { vertices, indices } + } +} diff --git a/editor/src/main.rs b/editor/src/main.rs index 0efdc3b..eb219b7 100644 --- a/editor/src/main.rs +++ b/editor/src/main.rs @@ -107,12 +107,14 @@ impl Application { match self.file_receiver.try_recv() { Ok(ui::FileEvent::Save) => println!("Saving!"), Ok(ui::FileEvent::SaveAs(path)) => println!("Saving as: {:?}", path), - Ok(ui::FileEvent::Import(kind, path)) => match kind { - ui::ImportKind::Stl => { - let model = import::load_stl(path); - let widgets = self.render_state.load_model(&model); - self.objects.extend(widgets.into_iter()); - } + Ok(ui::FileEvent::Import(kind, path)) => { + let model = match kind { + ui::ImportKind::Stl => import::load_stl(path), + ui::ImportKind::Gltf => import::load_gltf(path), + }; + + let widgets = self.render_state.load_model(&model); + self.objects.extend(widgets.into_iter()); }, Err(crossbeam_channel::TryRecvError::Empty) => break, Err(crossbeam_channel::TryRecvError::Disconnected) => { diff --git a/editor/src/model.rs b/editor/src/model.rs index 46ee0e8..d019292 100644 --- a/editor/src/model.rs +++ b/editor/src/model.rs @@ -5,16 +5,12 @@ pub struct Model { pub struct Object { pub name: Option, - pub mesh: Mesh, + pub transform: Transform, + pub mesh: Option, pub children: Vec, } pub struct Mesh { - pub transform: Transform, - pub data: MeshData, -} - -pub struct MeshData { pub vertices: Vec, pub indices: Vec, } diff --git a/editor/src/render.rs b/editor/src/render.rs index bd61531..ce2639d 100644 --- a/editor/src/render.rs +++ b/editor/src/render.rs @@ -272,14 +272,19 @@ impl RenderState { } pub fn load_object(&mut self, object: &model::Object) -> ObjectWidget { - let mesh = self.load_mesh_data(&object.mesh.data); + let mesh = object.mesh.as_ref().map(|mesh| cyborg::scene::Mesh { + mesh: self.load_mesh(mesh), + }); - let entity = self.world.push(( - cyborg::scene::Mesh { mesh }, - cyborg::scene::Transform { - transform: object.mesh.transform.to_mat4(), - }, - )); + let transform = cyborg::scene::Transform { + transform: object.transform.to_mat4(), + }; + + let entity = if let Some(mesh) = mesh { + self.world.push((mesh, transform)) + } else { + self.world.push((transform, ())) + }; let mut children = Vec::new(); for child in object.children.iter() { @@ -288,7 +293,7 @@ impl RenderState { ObjectWidget { name: object.name.clone(), - transform: object.mesh.transform.clone(), + transform: object.transform.clone(), entity, children, dirty: false, @@ -296,7 +301,7 @@ impl RenderState { } } - pub fn load_mesh_data(&mut self, data: &model::MeshData) -> MeshHandle { + pub fn load_mesh(&mut self, data: &model::Mesh) -> MeshHandle { use cyborg::pass::{ mesh::{MeshPass, Vertex}, RenderPassBox, diff --git a/editor/src/ui.rs b/editor/src/ui.rs index fe01817..6099518 100644 --- a/editor/src/ui.rs +++ b/editor/src/ui.rs @@ -13,6 +13,7 @@ pub enum FileEvent { #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] pub enum ImportKind { Stl, + Gltf, } #[derive(Copy, Clone, Debug, PartialEq, Eq)] @@ -130,18 +131,12 @@ impl UserInterface { ui.menu_button("Import...", |ui| { if ui.button("STL").clicked() { ui.close_menu(); + self.on_import(ImportKind::Stl); + } - let import_kind = ImportKind::Stl; - let file_sender = self.file_sender.to_owned(); - std::thread::spawn(move || { - let dialog = rfd::FileDialog::new().add_filter("STL", &["stl"]); - if let Some(paths) = dialog.pick_files() { - for path in paths.iter() { - let event = FileEvent::Import(import_kind, path.into()); - file_sender.send(event).unwrap(); - } - } - }); + if ui.button("glTF").clicked() { + ui.close_menu(); + self.on_import(ImportKind::Gltf); } }); @@ -216,6 +211,29 @@ impl UserInterface { pub fn ui_node_editor(&mut self, ui: &mut egui::Ui) { ui.label("Node editor goes here!"); } + + pub fn on_import(&self, kind: ImportKind) { + let kind_name = match kind { + ImportKind::Stl => "STL", + ImportKind::Gltf => "glTF", + }; + + let extensions: &[&str] = match kind { + ImportKind::Stl => &["stl"], + ImportKind::Gltf => &["gltf", "glb"], + }; + + let file_sender = self.file_sender.to_owned(); + std::thread::spawn(move || { + let dialog = rfd::FileDialog::new().add_filter(kind_name, extensions); + if let Some(paths) = dialog.pick_files() { + for path in paths.iter() { + let event = FileEvent::Import(kind, path.into()); + file_sender.send(event).unwrap(); + } + } + }); + } } pub struct ViewportWidget {