From f9466947919ab3ad0f69b82241abd9d37287e4f6 Mon Sep 17 00:00:00 2001 From: Paris DOUADY Date: Wed, 27 Dec 2023 18:33:21 +0100 Subject: [PATCH] add LOD generation and loading --- Cargo.lock | 36 +- assets/models/flour_factory.glb | 4 +- assets/models_opt/.gitignore | 1 + assets_gui/Cargo.toml | 3 +- assets_gui/src/lod.rs | 357 ++++++++++++++++++ assets_gui/src/main.rs | 98 +++-- assets_gui/src/orbit_camera.rs | 7 +- assets_gui/src/yakui_gui.rs | 151 +++++--- engine/Cargo.toml | 3 +- engine/src/drawables/instanced_mesh.rs | 38 +- engine/src/drawables/lit_mesh.rs | 222 +++-------- engine/src/drawables/water.rs | 45 +-- engine/src/geometry/tesselator.rs | 131 +++---- engine/src/gfx.rs | 16 +- engine/src/lamplights.rs | 2 +- engine/src/lib.rs | 3 + engine/src/meshbuild.rs | 264 +++++++++++++ engine/src/meshload.rs | 214 +++++++---- engine_demo/src/main.rs | 9 +- engine_demo/src/spheres.rs | 4 +- geom/src/aabb3.rs | 19 +- geom/src/lib.rs | 2 + geom/src/matrix4.rs | 8 + geom/src/quaternion.rs | 18 + geom/src/sphere.rs | 22 ++ goryak/src/dragvalue.rs | 60 +-- .../src/rendering/map_rendering/map_mesh.rs | 89 ++--- native_app/src/rendering/orbit_camera.rs | 20 +- 28 files changed, 1290 insertions(+), 556 deletions(-) create mode 100644 assets/models_opt/.gitignore create mode 100644 assets_gui/src/lod.rs create mode 100644 engine/src/meshbuild.rs create mode 100644 geom/src/sphere.rs diff --git a/Cargo.lock b/Cargo.lock index 1810dccc3..6828ab9f5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -199,6 +199,7 @@ dependencies = [ "goryak", "inline_tweak", "log", + "meshopt2", "yakui", ] @@ -908,7 +909,6 @@ dependencies = [ "rayon", "serde", "slotmapd", - "smallvec", "smol_str 0.2.0", "wgpu", "winit", @@ -1048,6 +1048,15 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "float-cmp" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" +dependencies = [ + "num-traits", +] + [[package]] name = "flume" version = "0.11.0" @@ -1208,23 +1217,24 @@ dependencies = [ [[package]] name = "gltf" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad2dcfb6dd7a66f9eb3d181a29dcfb22d146b0bcdc2e1ed1713cbf03939a88ea" +checksum = "3b78f069cf941075835822953c345b9e1edd67ae347b81ace3aea9de38c2ef33" dependencies = [ "base64", "byteorder", "gltf-json", "image", "lazy_static", + "serde_json", "urlencoding", ] [[package]] name = "gltf-derive" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2cbcea5dd47e7ad4e9ee6f040384fcd7204bbf671aa4f9e7ca7dfc9bfa1de20" +checksum = "438ffe1a5540d75403feaf23636b164e816e93f6f03131674722b3886ce32a57" dependencies = [ "inflections", "proc-macro2", @@ -1234,9 +1244,9 @@ dependencies = [ [[package]] name = "gltf-json" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d5b810806b78dde4b71a95cc0e6fdcab34c4c617da3574df166f9987be97d03" +checksum = "655951ba557f2bc69ea4b0799446bae281fa78efae6319968bdd2c3e9a06d8e1" dependencies = [ "gltf-derive", "serde", @@ -1769,6 +1779,18 @@ dependencies = [ "autocfg", ] +[[package]] +name = "meshopt2" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd54b87eddb8f60bbbb9502fc77fc207072b69e1d739c85c9e97703626deda7" +dependencies = [ + "bitflags 2.4.1", + "cc", + "float-cmp", + "thiserror", +] + [[package]] name = "metal" version = "0.27.0" diff --git a/assets/models/flour_factory.glb b/assets/models/flour_factory.glb index 916c99f78..ed189e8ff 100644 --- a/assets/models/flour_factory.glb +++ b/assets/models/flour_factory.glb @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:25c3f9ed42143b33ae9d644f259414a6f80a73956cf51ff1ff075dd359aa177d -size 3951736 +oid sha256:a7edc55120ddefdf0d1851c8b96bd3a4de3c84cbc625c376cfb5a14ffef801fd +size 2806744 diff --git a/assets/models_opt/.gitignore b/assets/models_opt/.gitignore new file mode 100644 index 000000000..f59ec20aa --- /dev/null +++ b/assets/models_opt/.gitignore @@ -0,0 +1 @@ +* \ No newline at end of file diff --git a/assets_gui/Cargo.toml b/assets_gui/Cargo.toml index 64d2fb5fa..3de2cb9dd 100644 --- a/assets_gui/Cargo.toml +++ b/assets_gui/Cargo.toml @@ -12,4 +12,5 @@ log = { version = "0.4.11", features=["max_level_info", "release_max_le inline_tweak = "1.0.8" egui = { workspace = true } yakui = { git = "https://github.com/SecondHalfGames/yakui" } -goryak = { path = "../goryak" } \ No newline at end of file +goryak = { path = "../goryak" } +meshopt2 = "0.2.0" \ No newline at end of file diff --git a/assets_gui/src/lod.rs b/assets_gui/src/lod.rs new file mode 100644 index 000000000..0a8af4bcd --- /dev/null +++ b/assets_gui/src/lod.rs @@ -0,0 +1,357 @@ +use std::borrow::Cow; +use std::collections::BTreeMap; +use std::path::PathBuf; + +use meshopt2::{optimize_vertex_fetch, SimplifyOptions, Vertex, VertexDataAdapter}; + +use common::unwrap_cont; +use engine::gltf::json::validation::{Checked, USize64}; +use engine::gltf::json::{accessor, Accessor, Index, Value}; +use engine::gltf::{json, Document, Semantic}; +use engine::meshload::CPUMesh; +use geom::{Vec2, Vec3}; + +#[derive(Debug)] +pub enum LodGenerateError { + AlreadyHasLOD, +} + +#[derive(Debug, Copy, Clone)] +pub struct LodGenerateParams { + pub n_lods: usize, + pub quality: f32, + pub sloppy: bool, +} + +pub fn lod_generate(m: &mut CPUMesh, params: LodGenerateParams) -> Result<(), LodGenerateError> { + let doc = &mut m.gltf_doc; + let data = &mut m.gltf_data; + + let scene = doc.default_scene().expect("no default scene"); + + let mut generated_lods = vec![]; + + let getnode = |id: usize| doc.nodes().nth(id).unwrap(); + + for (node, lod, _, _, _) in engine::meshload::find_nodes(&scene, getnode) { + let Some(mesh) = node.mesh() else { + continue; + }; + if lod != 0 { + return Err(LodGenerateError::AlreadyHasLOD); + } + + let mut primitives = mesh.primitives().collect::>(); + primitives.sort_unstable_by_key(|x| x.material().index()); + + for i in 0..params.n_lods { + let mut primitive_lods = vec![]; + + for primitive in &primitives { + let reader = primitive.reader(|b| Some(&data.get(b.index())?.0[..b.length()])); + + let positions = unwrap_cont!(reader.read_positions()).map(Vec3::from); + let normals = unwrap_cont!(reader.read_normals()).map(Vec3::from); + let uv = unwrap_cont!(reader.read_tex_coords(0)) + .into_f32() + .map(Vec2::from); + let indices: Vec = unwrap_cont!(reader.read_indices()).into_u32().collect(); + + let mut vertices = Vec::new(); + + for ((p, normal), uv) in positions.zip(normals).zip(uv) { + vertices.push(Vertex { + p: [p.x, p.y, p.z], + n: [normal.x, normal.y, normal.z], + t: [uv.x, uv.y], + }); + } + + let position_offset = 0; + let vertex_stride = std::mem::size_of::(); + let vertex_data = meshopt2::typed_to_bytes(&vertices); + + let adapter = VertexDataAdapter::new(vertex_data, vertex_stride, position_offset) + .expect("failed to create vertex data reader"); + + let target_count = indices.len() / 3 / (i + 1); + let target_error = (0.1 + i as f32 * 0.1) * (1.0 - params.quality); + + let mut optimized_indices = if params.sloppy { + meshopt2::simplify_sloppy(&indices, &adapter, target_count, target_error, None) + } else { + meshopt2::simplify( + &indices, + &adapter, + target_count, + target_error, + SimplifyOptions::empty(), + None, + ) + }; + + let optimized_vertices = optimize_vertex_fetch(&mut *optimized_indices, &vertices); + + primitive_lods.push((primitive.index(), optimized_vertices, optimized_indices)); + } + generated_lods.push((i, node.index(), mesh.index(), primitive_lods)); + } + } + + let glb_buffer_id = engine::meshload::glb_buffer_id(&doc); + let glb_data = &mut data[glb_buffer_id].0; + + let mut json = doc.clone().into_json(); + + let mut create_view = |count: usize, + data: Vec, + component_type: accessor::ComponentType, + type_: accessor::Type, + min: Option, + max: Option| { + let view = json::buffer::View { + buffer: Index::new(glb_buffer_id as u32), + byte_length: USize64::from(data.len()), + byte_offset: Some(USize64::from(glb_data.len())), + byte_stride: None, + extensions: Default::default(), + extras: Default::default(), + name: None, + target: None, + }; + glb_data.extend(data); + + json.accessors.push(Accessor { + buffer_view: Some(Index::new(json.buffer_views.len() as u32)), + byte_offset: None, + component_type: Checked::Valid(accessor::GenericComponentType(component_type)), + count: USize64::from(count), + extensions: Default::default(), + extras: Default::default(), + type_: Checked::Valid(type_), + min, + max, + name: None, + normalized: false, + sparse: None, + }); + json.buffer_views.push(view); + + Index::::new(json.accessors.len() as u32 - 1) + }; + + for (lod_id, node_id, mesh_id, primitives) in generated_lods { + let new_idx_id = json.meshes.len() as u32; + let orig_mesh = json.meshes.get_mut(mesh_id).unwrap(); + + let mut triangles = 0; + let mut json_primitives = vec![]; + for (primitive_id, vertices, indices) in primitives { + triangles += indices.len() / 3; + let json_primitive = orig_mesh.primitives.get(primitive_id).unwrap(); + + let mut json_attributes = BTreeMap::new(); + + let mut minp = [f32::MAX, f32::MAX, f32::MAX]; + let mut maxp = [f32::MIN, f32::MIN, f32::MIN]; + + for v in vertices.iter() { + minp[0] = minp[0].min(v.p[0]); + minp[1] = minp[1].min(v.p[1]); + minp[2] = minp[2].min(v.p[2]); + + maxp[0] = maxp[0].max(v.p[0]); + maxp[1] = maxp[1].max(v.p[1]); + maxp[2] = maxp[2].max(v.p[2]); + } + + let minp = minp.iter().map(|&x| Value::from(x)).collect(); + let maxp = maxp.iter().map(|&x| Value::from(x)).collect(); + + let positions = vertices.iter().map(|v| v.p).collect::>(); + json_attributes.insert( + Checked::Valid(Semantic::Positions), + create_view( + positions.len(), + to_padded_byte_vector(positions), + accessor::ComponentType::F32, + accessor::Type::Vec3, + Some(minp), + Some(maxp), + ), + ); + + let normals = vertices.iter().map(|v| v.n).collect::>(); + json_attributes.insert( + Checked::Valid(Semantic::Normals), + create_view( + normals.len(), + to_padded_byte_vector(normals), + accessor::ComponentType::F32, + accessor::Type::Vec3, + None, + None, + ), + ); + + let tex_coords = vertices.iter().map(|v| v.t).collect::>(); + json_attributes.insert( + Checked::Valid(Semantic::TexCoords(0)), + create_view( + tex_coords.len(), + to_padded_byte_vector(tex_coords), + accessor::ComponentType::F32, + accessor::Type::Vec2, + None, + None, + ), + ); + + let indice_view = create_view( + indices.len(), + to_padded_byte_vector(indices), + accessor::ComponentType::U32, + accessor::Type::Scalar, + None, + None, + ); + + let json_primitive = json::mesh::Primitive { + attributes: json_attributes, + extensions: json_primitive.extensions.clone(), + extras: json_primitive.extras.clone(), + indices: Some(indice_view), + material: json_primitive.material.clone(), + mode: json_primitive.mode, + targets: json_primitive.targets.clone(), + }; + json_primitives.push(json_primitive); + } + + let lod_mesh = json::Mesh { + extensions: orig_mesh.extensions.clone(), + extras: orig_mesh.extras.clone(), + name: None, + primitives: json_primitives, + weights: orig_mesh.weights.clone(), + }; + + let orig_node = json.nodes.get_mut(node_id).unwrap(); + if orig_node.extensions.is_none() { + orig_node.extensions = Some(json::extensions::scene::Node::default()); + } + + let ext = orig_node.extensions.as_mut().unwrap(); + + let entry = ext.others.entry("MSFT_lod".to_string()); + + fn autocoverage(triangles: usize) -> f32 { + (triangles as f32 / 100000.0).min(0.5) + } + + let obj = entry.or_insert_with(|| { + Value::Object( + [ + ("ids".to_string(), Value::Array(vec![])), + ( + "screencoverage".to_string(), + Value::Array(vec![autocoverage(m.n_triangles).into()]), + ), + ] + .into_iter() + .collect(), + ) + }); + obj["ids"].as_array_mut().unwrap().push(new_idx_id.into()); + obj["screencoverage"] + .as_array_mut() + .unwrap() + .push(autocoverage(triangles).into()); + + let lod_node = json::Node { + camera: None, + children: None, + extensions: None, + extras: Default::default(), + matrix: None, + name: Some(format!( + "lod {} for {} ({})", + lod_id, + orig_mesh.name.as_deref().unwrap_or(""), + node_id + )), + mesh: Some(Index::new(json.meshes.len() as u32)), + rotation: None, + scale: None, + translation: None, + skin: None, + weights: None, + }; + + json.meshes.push(lod_mesh); + json.nodes.push(lod_node); + //json.scenes[0] + // .nodes + // .push(Index::new(json.nodes.len() as u32 - 1)); // add to scene to show in blender + } + + json.buffers[glb_buffer_id].byte_length = USize64::from(glb_data.len()); + + if !json.extensions_used.contains(&"MSFT_lod".to_string()) { + json.extensions_used.push("MSFT_lod".to_string()); + } + + *doc = Document::from_json_without_validation(json); + Ok(()) +} + +pub fn export_doc_opt(mesh: &CPUMesh) { + let glb_buffer_id = engine::meshload::glb_buffer_id(&mesh.gltf_doc); + let json = mesh.gltf_doc.clone().into_json(); + let glb_data = &*mesh.gltf_data[glb_buffer_id]; + + let json_string = json::serialize::to_string(&json).expect("Serialization error"); + let mut json_offset = json_string.len(); + align_to_multiple_of_four(&mut json_offset); + + let glb = engine::gltf::binary::Glb { + header: engine::gltf::binary::Header { + magic: *b"glTF", + version: 2, + // N.B., the size of binary glTF file is limited to range of `u32`. + length: (json_offset + glb_data.len()) + .try_into() + .expect("file size exceeds binary glTF limit"), + }, + bin: Some(Cow::Borrowed(glb_data)), + json: Cow::Owned(json_string.into_bytes()), + }; + let Some(name) = mesh.asset_path.file_name() else { + log::error!("asset path has no file name"); + return; + }; + + let mut asset_path = PathBuf::new(); + asset_path.push("assets"); + asset_path.push("models_opt"); + asset_path.push(name); + + let writer = std::fs::File::create(asset_path).expect("I/O error"); + glb.to_writer(writer).expect("glTF binary output error"); +} + +fn to_padded_byte_vector(vec: Vec) -> Vec { + let byte_length = vec.len() * std::mem::size_of::(); + let byte_capacity = vec.capacity() * std::mem::size_of::(); + let alloc = vec.into_boxed_slice(); + let ptr = Box::<[T]>::into_raw(alloc) as *mut u8; + let mut new_vec = unsafe { Vec::from_raw_parts(ptr, byte_length, byte_capacity) }; + while new_vec.len() % 4 != 0 { + new_vec.push(0); // pad to multiple of four bytes + } + new_vec +} + +fn align_to_multiple_of_four(n: &mut usize) { + *n = (*n + 3) & !3; +} diff --git a/assets_gui/src/main.rs b/assets_gui/src/main.rs index 4a20afe98..07f7f495d 100644 --- a/assets_gui/src/main.rs +++ b/assets_gui/src/main.rs @@ -1,19 +1,31 @@ +use crate::lod::{export_doc_opt, lod_generate, LodGenerateParams}; +use common::unwrap_cont; use engine::meshload::load_mesh_with_properties; -use engine::{Context, FrameContext, GfxContext, GfxSettings, SpriteBatchBuilder}; +use engine::{ + Context, FrameContext, GfxContext, GfxSettings, InstancedMeshBuilder, MeshInstance, + SpriteBatchBuilder, +}; use geom::{vec3, InfiniteFrustrum, LinearColor, Plane, Vec2, Vec3}; -use std::path::Path; +use std::path::{Path, PathBuf}; use crate::orbit_camera::OrbitCamera; use crate::yakui_gui::{Gui, Inspected, Shown}; mod companies; +mod lod; mod orbit_camera; mod yakui_gui; +#[derive(Debug)] +pub enum GUIAction { + GenerateLOD(PathBuf, LodGenerateParams), +} + struct State { camera: OrbitCamera, last_inspect: Inspected, gui: Gui, + actions: Vec, } impl engine::framework::State for State { @@ -24,19 +36,6 @@ impl engine::framework::State for State { gfx.sun_shadowmap = GfxContext::mk_shadowmap(&gfx.device, 2048); gfx.update_simplelit_bg(); - //let mut style = (*ctx.egui.egui.style()).clone(); - // - //style.text_styles = [ - // (egui::TextStyle::Small, FontId::new(15.0, Proportional)), - // (egui::TextStyle::Body, FontId::new(18.5, Proportional)), - // (egui::TextStyle::Button, FontId::new(18.5, Proportional)), - // (egui::TextStyle::Heading, FontId::new(25.0, Proportional)), - // (egui::TextStyle::Monospace, FontId::new(18.0, Monospace)), - //] - //.into(); - // - //ctx.egui.egui.set_style(style); - let camera = OrbitCamera::new(); let mut gui = Gui::new(); @@ -47,12 +46,17 @@ impl engine::framework::State for State { camera, last_inspect: Inspected::None, gui, + actions: vec![], } } fn update(&mut self, ctx: &mut Context) { self.camera.camera_movement(ctx); + for action in std::mem::take(&mut self.actions) { + self.do_action(ctx, action); + } + if self.gui.inspected != self.last_inspect { self.last_inspect = self.gui.inspected; self.gui.shown = create_shown(&mut ctx.gfx, self, self.gui.inspected); @@ -97,6 +101,31 @@ impl engine::framework::State for State { } } +impl State { + fn do_action(&mut self, ctx: &mut Context, action: GUIAction) { + log::info!("{:?}", action); + let gfx = &mut ctx.gfx; + match action { + GUIAction::GenerateLOD(path, params) => { + let Ok((_, mut cpumesh)) = load_mesh_with_properties( + gfx, + path.file_name().unwrap().to_str().unwrap(), + true, + ) else { + return; + }; + + if let Err(e) = lod_generate(&mut cpumesh, params) { + log::error!("{:?}", e); + } + export_doc_opt(&cpumesh); + + self.last_inspect = Inspected::None; + } + } + } +} + fn create_shown(gfx: &mut GfxContext, state: &State, inspected: Inspected) -> Shown { match inspected { Inspected::None => Shown::None, @@ -119,17 +148,34 @@ fn create_shown(gfx: &mut GfxContext, state: &State, inspected: Inspected) -> Sh Shown::Sprite(sb.build(gfx).unwrap()) } Some(x) if x == "glb" => { - let model = match load_mesh_with_properties(gfx, &comp.asset_location) { - Ok(x) => x, - Err(e) => { - return Shown::Error(format!( - "could not load model {}:\n{:?}", - comp.asset_location, e - )) - } - }; - - Shown::Model(model) + let (mesh, cpu) = + match load_mesh_with_properties(gfx, &comp.asset_location, false) { + Ok(x) => x, + Err(e) => { + return Shown::Error(format!( + "could not load model {}:\n{:?}", + comp.asset_location, e + )) + } + }; + let size = mesh.lods[0].bounding_sphere.radius; + let mut meshes = vec![]; + for (i, mut lod) in mesh.lods.iter().cloned().enumerate() { + let mut cpy = mesh.clone(); + lod.screen_coverage = 0.0; + cpy.lods = vec![lod].into_boxed_slice(); + + let mut b: InstancedMeshBuilder = InstancedMeshBuilder::new(cpy); + b.instances.push(MeshInstance { + pos: Vec3::x(i as f32 * size * 2.0), + dir: Vec3::X, + tint: LinearColor::WHITE, + }); + + meshes.push(unwrap_cont!(b.build(gfx))); + } + + Shown::Model((mesh, meshes, cpu)) } Some(_) => Shown::Error(format!( "unknown asset type for path: {}", diff --git a/assets_gui/src/orbit_camera.rs b/assets_gui/src/orbit_camera.rs index 15daa8837..7bd429dbc 100644 --- a/assets_gui/src/orbit_camera.rs +++ b/assets_gui/src/orbit_camera.rs @@ -1,5 +1,5 @@ use engine::{Context, MouseButton}; -use geom::{Camera, Matrix4, Radians, Vec2, Vec3}; +use geom::{Camera, Radians, Vec2, Vec3}; pub struct OrbitCamera { pub camera: Camera, @@ -21,11 +21,8 @@ impl OrbitCamera { } pub fn update(&mut self, ctx: &mut Context) { - let viewproj = self.camera.build_view_projection_matrix(); - let inv_viewproj = viewproj.invert().unwrap_or_else(Matrix4::zero); + ctx.gfx.set_camera(self.camera); - ctx.gfx.set_proj(viewproj); - ctx.gfx.set_inv_proj(inv_viewproj); let params = ctx.gfx.render_params.value_mut(); params.cam_pos = self.camera.eye(); params.cam_dir = -self.camera.dir(); diff --git a/assets_gui/src/yakui_gui.rs b/assets_gui/src/yakui_gui.rs index e28a7d9dd..8fc2638ab 100644 --- a/assets_gui/src/yakui_gui.rs +++ b/assets_gui/src/yakui_gui.rs @@ -5,20 +5,21 @@ use yakui::{ }; use common::descriptions::{BuildingGen, CompanyKind}; -use engine::meshload::MeshProperties; +use engine::meshload::CPUMesh; use engine::wgpu::RenderPass; -use engine::{set_cursor_icon, CursorIcon, Drawable, GfxContext, Mesh, SpriteBatch}; +use engine::{set_cursor_icon, CursorIcon, Drawable, GfxContext, InstancedMesh, Mesh, SpriteBatch}; use geom::Matrix4; use goryak::{ background, button_primary, center_width, checkbox_value, combo_box, divider, dragvalue, icon, interact_box_radius, is_hovered, labelc, on_background, on_secondary, on_secondary_container, - on_surface, outline, outline_variant, round_rect, scroll_vertical, secondary, - secondary_container, set_theme, stretch_width, surface, surface_variant, use_changed, - CountGrid, Draggable, MainAxisAlignItems, RoundRect, Theme, + on_surface, outline_variant, round_rect, scroll_vertical, secondary, secondary_container, + set_theme, stretch_width, surface, surface_variant, use_changed, CountGrid, Draggable, + MainAxisAlignItems, RoundRect, Theme, }; use crate::companies::Companies; -use crate::State; +use crate::lod::LodGenerateParams; +use crate::{GUIAction, State}; #[derive(PartialEq, Eq, Copy, Clone)] pub enum Inspected { @@ -30,7 +31,7 @@ pub enum Inspected { pub enum Shown { None, Error(String), - Model((Mesh, MeshProperties)), + Model((Mesh, Vec, CPUMesh)), Sprite(SpriteBatch), } @@ -167,6 +168,89 @@ impl State { } fn model_properties(&mut self) { + Self::model_properties_container(|| { + let tc = on_secondary_container(); // text color + labelc(tc, "Model properties"); + match self.gui.shown { + Shown::None => { + labelc(tc, "No model selected"); + } + Shown::Error(ref e) => { + labelc(tc, e.clone()); + } + Shown::Model((ref mesh, _, ref mut props)) => { + let params = use_state(|| LodGenerateParams { + n_lods: 3, + quality: 0.9, + sloppy: false, + }); + + row(|| { + let mut c = List::column(); + c.main_axis_size = MainAxisSize::Min; + c.show(|| { + CountGrid::col(2) + .main_axis_size(MainAxisSize::Min) + .show(|| { + params.modify(|mut params| { + labelc(tc, "n_lods"); + dragvalue().min(1.0).max(4.0).show(&mut params.n_lods); + + labelc(tc, "quality"); + dragvalue().min(0.0).max(1.0).show(&mut params.quality); + + labelc(tc, "sloppy"); + checkbox_value(&mut params.sloppy); + + params + }); + }); + if button_primary("Generate LODs").clicked { + let asset_path = &props.asset_path; + + self.actions + .push(GUIAction::GenerateLOD(asset_path.clone(), params.get())); + } + }); + + let mut lod_details = CountGrid::col(1 + mesh.lods.len()); + lod_details.main_axis_size = MainAxisSize::Min; + lod_details.show(|| { + labelc(tc, ""); + for (i, _) in mesh.lods.iter().enumerate() { + labelc(tc, format!("LOD{}", i)); + } + + labelc(tc, "Vertices"); + for lod in &*mesh.lods { + labelc(tc, format!("{}", lod.n_vertices)); + } + + labelc(tc, "Triangles"); + for lod in &*mesh.lods { + labelc(tc, format!("{}", lod.n_indices / 3)); + } + + labelc(tc, "Draw calls"); + for lod in &*mesh.lods { + labelc(tc, format!("{}", lod.primitives.len())); + } + + labelc(tc, "Coverage"); + for lod in &*mesh.lods { + labelc(tc, format!("{:.3}", lod.screen_coverage)); + } + }); + }); + } + Shown::Sprite(ref _sprite) => { + labelc(tc, "Sprite"); + } + } + }); + } + + fn model_properties_container(children: impl FnOnce()) { let mut l = List::column(); l.main_axis_alignment = MainAxisAlignment::End; l.cross_axis_alignment = CrossAxisAlignment::Stretch; @@ -175,54 +259,7 @@ impl State { Pad::all(8.0).show(|| { round_rect(10.0, secondary_container(), || { Pad::all(5.0).show(|| { - let tc = on_secondary_container(); // text color - column(|| { - labelc(tc, "Model properties"); - match &self.gui.shown { - Shown::None => { - labelc(tc, "No model selected"); - } - Shown::Error(e) => { - labelc(tc, e.clone()); - } - Shown::Model((_, props)) => { - row(|| { - column(|| { - labelc(tc, "Vertices"); - labelc(tc, "Triangles"); - labelc(tc, "Materials"); - labelc(tc, "Textures"); - labelc(tc, "Draw calls"); - }); - column(|| { - labelc( - on_background(), - format!("{}", props.n_vertices), - ); - labelc( - on_background(), - format!("{}", props.n_triangles), - ); - labelc( - on_background(), - format!("{}", props.n_materials), - ); - labelc( - on_background(), - format!("{}", props.n_textures), - ); - labelc( - on_background(), - format!("{}", props.n_draw_calls), - ); - }); - }); - } - Shown::Sprite(_sprite) => { - labelc(tc, "Sprite"); - } - } - }); + column(children); }); }); }); @@ -443,7 +480,7 @@ impl Drawable for Shown { fn draw<'a>(&'a self, gfx: &'a GfxContext, rp: &mut RenderPass<'a>) { match self { Shown::None | Shown::Error(_) => {} - Shown::Model((mesh, _)) => mesh.draw(gfx, rp), + Shown::Model((_, mesh, _)) => mesh.draw(gfx, rp), Shown::Sprite(sprite) => sprite.draw(gfx, rp), } } @@ -456,7 +493,7 @@ impl Drawable for Shown { ) { match self { Shown::None | Shown::Error(_) => {} - Shown::Model((mesh, _)) => mesh.draw_depth(gfx, rp, shadow_cascade), + Shown::Model((_, mesh, _)) => mesh.draw_depth(gfx, rp, shadow_cascade), Shown::Sprite(sprite) => sprite.draw_depth(gfx, rp, shadow_cascade), } } diff --git a/engine/Cargo.toml b/engine/Cargo.toml index 377c6884a..831d97551 100644 --- a/engine/Cargo.toml +++ b/engine/Cargo.toml @@ -20,13 +20,12 @@ egui-winit = { git = "https://github.com/emilk/egui", default-features = fals bytemuck = "1.7.2" image = { version = "0.24.3", default-features = false, features = ["png"] } log = "0.4.11" -gltf = { version = "1.2.0", default-features=false, features=["import", "utils", "names"] } +gltf = { version = "1.2.0", default-features=false, features=["import", "utils", "names", "extensions", "extras"] } itertools = { version = "0.11.0", default-features = false } profiling = { version = "1.0.1", default-features = false } rayon = "1.6" beul = "1.0.0" slotmapd = "1.0" -smallvec = "1.10.0" inline_tweak = "1.0.8" egui-wgpu = { git = "https://github.com/emilk/egui" } cpal = "0.15.0" diff --git a/engine/src/drawables/instanced_mesh.rs b/engine/src/drawables/instanced_mesh.rs index 1abfc3fd4..f80297daf 100644 --- a/engine/src/drawables/instanced_mesh.rs +++ b/engine/src/drawables/instanced_mesh.rs @@ -1,4 +1,5 @@ #![allow(clippy::collapsible_else_if)] + use crate::pbuffer::PBuffer; use crate::{Drawable, GfxContext, Mesh, MeshPipeline}; use geom::{LinearColor, Matrix4, Vec3}; @@ -30,7 +31,7 @@ impl MeshInstance { pub struct InstancedMeshBuilder { mesh: Mesh, - ibuffer: Option>, + ibuffer: PBuffer, pub instances: Vec, } @@ -39,7 +40,7 @@ impl InstancedMeshBuilder { InstancedMeshBuilder { mesh, instances: Vec::with_capacity(4), - ibuffer: PERSISTENT.then(|| Box::new(PBuffer::new(BufferUsages::VERTEX))), + ibuffer: PBuffer::new(BufferUsages::VERTEX), } } @@ -51,9 +52,7 @@ impl InstancedMeshBuilder { let mut temp; let ibuffer; if PERSISTENT { - unsafe { - ibuffer = self.ibuffer.as_deref_mut().unwrap_unchecked(); - } + ibuffer = &mut self.ibuffer; } else { temp = PBuffer::new(BufferUsages::VERTEX); ibuffer = &mut temp; @@ -78,14 +77,18 @@ pub struct InstancedMesh { impl Drawable for InstancedMesh { fn draw<'a>(&'a self, gfx: &'a GfxContext, rp: &mut RenderPass<'a>) { + let Some(lod_select) = self.mesh.lods.first() else { + return; + }; + rp.set_bind_group(1, &gfx.render_params.bindgroup, &[]); rp.set_bind_group(3, &gfx.simplelit_bg, &[]); rp.set_vertex_buffer(0, self.mesh.vertex_buffer.slice(..)); rp.set_vertex_buffer(1, self.instance_buffer.slice(..)); rp.set_index_buffer(self.mesh.index_buffer.slice(..), IndexFormat::Uint32); - for (mat, offset, length) in self.mesh.iter_materials() { - let mat = gfx.material(mat); + for (mat, indices) in &lod_select.primitives { + let mat = gfx.material(*mat); let pipeline = gfx.get_pipeline(MeshPipeline { instanced: true, alpha: false, @@ -94,8 +97,9 @@ impl Drawable for InstancedMesh { }); rp.set_pipeline(pipeline); rp.set_bind_group(2, &mat.bg, &[]); - rp.draw_indexed(offset..offset + length, 0, 0..self.n_instances); - gfx.perf.drawcall(length / 3 * self.n_instances); + rp.draw_indexed(indices.clone(), 0, 0..self.n_instances); + gfx.perf + .drawcall((indices.end - indices.start) / 3 * self.n_instances); } } @@ -105,12 +109,16 @@ impl Drawable for InstancedMesh { rp: &mut RenderPass<'a>, shadow_cascade: Option<&Matrix4>, ) { + let Some(lod_select) = self.mesh.lods.first() else { + return; + }; + rp.set_vertex_buffer(0, self.mesh.vertex_buffer.slice(..)); rp.set_vertex_buffer(1, self.instance_buffer.slice(..)); rp.set_index_buffer(self.mesh.index_buffer.slice(..), IndexFormat::Uint32); - for (mat, offset, length) in self.mesh.iter_materials() { - let mat = gfx.material(mat); + for (mat, indices) in &lod_select.primitives { + let mat = gfx.material(*mat); rp.set_pipeline(gfx.get_pipeline(MeshPipeline { instanced: true, alpha: mat.transparent, @@ -121,9 +129,11 @@ impl Drawable for InstancedMesh { if mat.transparent { rp.set_bind_group(1, &mat.bg, &[]); } - rp.draw_indexed(offset..offset + length, 0, 0..self.n_instances); - gfx.perf - .depth_drawcall(length / 3 * self.n_instances, shadow_cascade.is_some()); + rp.draw_indexed(indices.clone(), 0, 0..self.n_instances); + gfx.perf.depth_drawcall( + (indices.end - indices.start) / 3 * self.n_instances, + shadow_cascade.is_some(), + ); } } } diff --git a/engine/src/drawables/lit_mesh.rs b/engine/src/drawables/lit_mesh.rs index f393d3800..a989c7c94 100644 --- a/engine/src/drawables/lit_mesh.rs +++ b/engine/src/drawables/lit_mesh.rs @@ -1,188 +1,45 @@ -use crate::pbuffer::PBuffer; -use crate::{ - CompiledModule, Drawable, GfxContext, IndexType, Material, MaterialID, MeshInstance, - MeshVertex, MikktGeometry, PipelineBuilder, RenderParams, Texture, Uniform, TL, -}; -use geom::Matrix4; -use smallvec::SmallVec; use std::sync::Arc; -use wgpu::{ - BindGroupLayout, BufferUsages, Device, IndexFormat, RenderPass, RenderPipeline, - VertexBufferLayout, -}; - -pub struct MeshBuilder { - pub(crate) vertices: Vec, - pub(crate) indices: Vec, - pub(crate) vi_buffers: Option>, - /// List of materialID and the starting offset - pub(crate) materials: SmallVec<[(MaterialID, u32); 1]>, -} - -impl MikktGeometry for MeshBuilder { - fn num_faces(&self) -> usize { - self.indices.len() / 3 - } - - fn num_vertices_of_face(&self, _face: usize) -> usize { - 3 - } - - fn position(&self, face: usize, vert: usize) -> [f32; 3] { - let i = self.indices[face * 3 + vert] as usize; - self.vertices[i].position - } - - fn normal(&self, face: usize, vert: usize) -> [f32; 3] { - let i = self.indices[face * 3 + vert] as usize; - self.vertices[i].normal.into() - } - - fn tex_coord(&self, face: usize, vert: usize) -> [f32; 2] { - let i = self.indices[face * 3 + vert] as usize; - self.vertices[i].uv - } - fn set_tangent_encoded(&mut self, tangent: [f32; 4], face: usize, vert: usize) { - let i = self.indices[face * 3 + vert] as usize; - self.vertices[i].tangent = tangent; - } -} - -impl MeshBuilder { - pub fn new(mat: MaterialID) -> Self { - Self { - vertices: vec![], - indices: vec![], - vi_buffers: PERSISTENT.then(|| { - Box::new(( - PBuffer::new(BufferUsages::VERTEX), - PBuffer::new(BufferUsages::INDEX), - )) - }), - materials: smallvec::smallvec![(mat, 0)], - } - } +use wgpu::{BindGroupLayout, Device, IndexFormat, RenderPass, RenderPipeline, VertexBufferLayout}; - pub fn new_without_mat() -> Self { - Self { - vertices: vec![], - indices: vec![], - vi_buffers: PERSISTENT.then(|| { - Box::new(( - PBuffer::new(BufferUsages::VERTEX), - PBuffer::new(BufferUsages::INDEX), - )) - }), - materials: Default::default(), - } - } +use geom::{Matrix4, Sphere}; - pub fn clear(&mut self) { - self.vertices.clear(); - self.indices.clear(); - } - - pub fn extend(&mut self, vertices: &[MeshVertex], indices: &[IndexType]) -> &mut Self { - let offset = self.vertices.len() as IndexType; - self.vertices.extend_from_slice(vertices); - self.indices.extend(indices.iter().map(|x| x + offset)); - self - } - - /// Sets the material for all future indice pushes - pub fn set_material(&mut self, material: MaterialID) { - let n = self.indices.len() as u32; - self.materials.push((material, n)); - } - - #[inline(always)] - pub fn extend_with(&mut self, f: impl FnOnce(&mut Vec, &mut dyn FnMut(IndexType))) { - let offset = self.vertices.len() as IndexType; - let vertices = &mut self.vertices; - let indices = &mut self.indices; - let mut x = move |index: IndexType| { - indices.push(index + offset); - }; - f(vertices, &mut x); - } - - pub fn compute_tangents(&mut self) { - if !crate::geometry::generate_tangents(self) { - log::warn!("failed to generate tangents"); - } - } - - pub fn build(&mut self, gfx: &GfxContext) -> Option { - if self.vertices.is_empty() { - return None; - } - - let mut tmpv; - let mut tmpi; - let vbuffer; - let ibuffer; - - if PERSISTENT { - let x = self.vi_buffers.as_deref_mut().unwrap(); - vbuffer = &mut x.0; - ibuffer = &mut x.1; - } else { - tmpv = PBuffer::new(BufferUsages::VERTEX); - tmpi = PBuffer::new(BufferUsages::INDEX); - vbuffer = &mut tmpv; - ibuffer = &mut tmpi; - } - - vbuffer.write(gfx, bytemuck::cast_slice(&self.vertices)); - ibuffer.write(gfx, bytemuck::cast_slice(&self.indices)); - - // convert materials to mesh format (from offsets to lengths) - let mut materials = SmallVec::with_capacity(self.materials.len()); - let mut mats = self.materials.iter().peekable(); - while let Some((mat, start)) = mats.next() { - let end = mats - .peek() - .map(|(_, x)| *x) - .unwrap_or(self.indices.len() as u32); - let l = end - start; - if l == 0 { - continue; - } - materials.push((*mat, l)); - } - - Some(Mesh { - vertex_buffer: vbuffer.inner()?, - index_buffer: ibuffer.inner()?, - materials, - skip_depth: false, - }) - } -} +use crate::meshbuild::MeshLod; +use crate::{ + CompiledModule, Drawable, GfxContext, Material, MeshInstance, MeshVertex, PipelineBuilder, + RenderParams, Texture, Uniform, TL, +}; #[derive(Clone)] pub struct Mesh { pub vertex_buffer: Arc, pub index_buffer: Arc, - /// List of materialID and the indice length - pub materials: SmallVec<[(MaterialID, u32); 1]>, + pub lods: Box<[MeshLod]>, pub skip_depth: bool, } impl Mesh { - /// Returns an iterator over the materials used by this mesh - /// The iterator returns the materialID, the index offset and the number of indices for that material - pub fn iter_materials(&self) -> impl Iterator + '_ { - let mut offset = 0; - self.materials.iter().map(move |(mat, n)| { - let ret = (*mat, offset, *n); - offset += *n; - ret - }) + pub(crate) fn lod_select(&self, gfx: &GfxContext) -> Option<&MeshLod> { + self.lods.iter().find(|x| x.passes_culling(gfx)) } } +/// Returns the screen area of a sphere between [0..1] where 1 is the entire screen (if the sphere fits within the screen) +pub fn screen_coverage(gfx: &GfxContext, s: Sphere) -> f32 { + let v = gfx.projection.value(); + let proj_center = v * s.center.w(1.0); + let proj_center_side = + v * (s.center + s.radius * gfx.render_params.value().cam_dir.perp_up()).w(1.0); + + let proj_center = proj_center.xyz() / proj_center.w; + let proj_center_side = proj_center_side.xyz() / proj_center_side.w; + + let proj_radius2 = (proj_center_side - proj_center).mag2(); + + let screen_area = proj_radius2 * std::f32::consts::PI; + screen_area +} + #[derive(Clone, Copy, Hash)] pub(crate) struct MeshPipeline { pub(crate) instanced: bool, @@ -244,13 +101,17 @@ impl PipelineBuilder for MeshPipeline { impl Drawable for Mesh { fn draw<'a>(&'a self, gfx: &'a GfxContext, rp: &mut RenderPass<'a>) { + let Some(lod) = self.lod_select(gfx) else { + return; + }; + rp.set_bind_group(1, &gfx.render_params.bindgroup, &[]); rp.set_bind_group(3, &gfx.simplelit_bg, &[]); rp.set_vertex_buffer(0, self.vertex_buffer.slice(..)); rp.set_index_buffer(self.index_buffer.slice(..), IndexFormat::Uint32); - for (mat, offset, length) in self.iter_materials() { - let mat = gfx.material(mat); + for (mat, index_range) in &lod.primitives { + let mat = gfx.material(*mat); rp.set_pipeline(gfx.get_pipeline(MeshPipeline { instanced: false, alpha: false, @@ -258,9 +119,9 @@ impl Drawable for Mesh { depth: false, })); rp.set_bind_group(2, &mat.bg, &[]); - rp.draw_indexed(offset..offset + length, 0, 0..1); + rp.draw_indexed(index_range.clone(), 0, 0..1); - gfx.perf.drawcall(length / 3); + gfx.perf.drawcall((index_range.end - index_range.start) / 3); } } @@ -273,11 +134,14 @@ impl Drawable for Mesh { if self.skip_depth { return; } + let Some(lod) = self.lod_select(gfx) else { + return; + }; rp.set_vertex_buffer(0, self.vertex_buffer.slice(..)); rp.set_index_buffer(self.index_buffer.slice(..), IndexFormat::Uint32); - for (mat, offset, length) in self.iter_materials() { - let mat = gfx.material(mat); + for (mat, index_range) in &lod.primitives { + let mat = gfx.material(*mat); rp.set_pipeline(gfx.get_pipeline(MeshPipeline { instanced: false, alpha: mat.transparent, @@ -288,10 +152,12 @@ impl Drawable for Mesh { if mat.transparent { rp.set_bind_group(1, &mat.bg, &[]); } - rp.draw_indexed(offset..offset + length, 0, 0..1); + rp.draw_indexed(index_range.clone(), 0, 0..1); - gfx.perf - .depth_drawcall(length / 3, shadow_cascade.is_some()); + gfx.perf.depth_drawcall( + (index_range.end - index_range.start) / 3, + shadow_cascade.is_some(), + ); } } } diff --git a/engine/src/drawables/water.rs b/engine/src/drawables/water.rs index a3b15eae1..8d84c9c7e 100644 --- a/engine/src/drawables/water.rs +++ b/engine/src/drawables/water.rs @@ -1,5 +1,6 @@ +use crate::meshbuild::MeshBuilder; use crate::{ - CompiledModule, Drawable, GfxContext, Mesh, MeshBuilder, MeshVertex, PipelineBuilder, Texture, + CompiledModule, Drawable, GfxContext, Mesh, MeshVertex, PipelineBuilder, Texture, TextureBuilder, TL, }; use geom::AABB; @@ -20,26 +21,28 @@ impl Water { pub fn new(gfx: &mut GfxContext, bounds: AABB) -> Self { let mut mb = MeshBuilder::::new_without_mat(); - mb.vertices.extend_from_slice(&[ - MeshVertex { - position: [bounds.ll.x, bounds.ll.y, -10.0], - ..Default::default() - }, - MeshVertex { - position: [bounds.ur.x, bounds.ll.y, -10.0], - ..Default::default() - }, - MeshVertex { - position: [bounds.ur.x, bounds.ur.y, -10.0], - ..Default::default() - }, - MeshVertex { - position: [bounds.ll.x, bounds.ur.y, -10.0], - ..Default::default() - }, - ]); - - mb.indices.extend_from_slice(&[0, 1, 2, 2, 3, 0]); + mb.extend( + None, + &[ + MeshVertex { + position: [bounds.ll.x, bounds.ll.y, -10.0], + ..Default::default() + }, + MeshVertex { + position: [bounds.ur.x, bounds.ll.y, -10.0], + ..Default::default() + }, + MeshVertex { + position: [bounds.ur.x, bounds.ur.y, -10.0], + ..Default::default() + }, + MeshVertex { + position: [bounds.ll.x, bounds.ur.y, -10.0], + ..Default::default() + }, + ], + &[0, 1, 2, 2, 3, 0], + ); // unwrap ok: we just added vertices let mesh = mb.build(gfx).unwrap(); diff --git a/engine/src/geometry/tesselator.rs b/engine/src/geometry/tesselator.rs index 31c80913d..c0417c569 100644 --- a/engine/src/geometry/tesselator.rs +++ b/engine/src/geometry/tesselator.rs @@ -1,5 +1,6 @@ use crate::geometry::earcut::earcut; -use crate::{GfxContext, Material, MeshBuilder, MeshVertex, MetallicRoughness}; +use crate::meshbuild::MeshBuilder; +use crate::{GfxContext, Material, MeshVertex, MetallicRoughness}; use geom::{vec3, Intersect, LinearColor, Segment, Vec2, Vec3, AABB}; use itertools::Itertools; @@ -65,7 +66,7 @@ impl Tesselator { let n_pointsu32 = n_points as u32; let normal = self.normal; - self.meshbuilder.extend_with(|vertices, index_push| { + self.meshbuilder.extend_with(None, |vertices, index_push| { vertices.push(MeshVertex { position: p.into(), color, @@ -107,7 +108,7 @@ impl Tesselator { let color: [f32; 4] = self.color.into(); let normal = self.normal; - self.meshbuilder.extend_with(|vertices, index_push| { + self.meshbuilder.extend_with(None, |vertices, index_push| { vertices.extend(points.iter().map(|p| MeshVertex { position: [p.x, p.y, z], color, @@ -141,7 +142,7 @@ impl Tesselator { let color = self.color.into(); let normal = self.normal; - self.meshbuilder.extend_with(|vertices, index_push| { + self.meshbuilder.extend_with(None, |vertices, index_push| { vertices.push(MeshVertex { position: (p + Vec3::x(r + halfthick)).into(), color, @@ -247,7 +248,7 @@ impl Tesselator { tangent: [0.0; 4], }, ]; - self.meshbuilder.extend(&verts, &[2, 1, 0, 3, 2, 0]); + self.meshbuilder.extend(None, &verts, &[2, 1, 0, 3, 2, 0]); true } @@ -307,6 +308,7 @@ impl Tesselator { ]; self.meshbuilder.extend( + None, &verts, if self.normal.z < 0.0 { &[2, 1, 0, 3, 2, 0] @@ -369,75 +371,76 @@ impl Tesselator { let color = self.color.into(); let normal = self.normal; let swap = (self.normal.z < 0.0) as u32 * 2; - self.meshbuilder.extend_with(move |verts, index_push| { - let mut idx_quad = move |index| { - index_push(index * 2 + swap); - index_push(index * 2 + 1); - index_push(index * 2 + 2 - swap); - - index_push(index * 2 + 3 - swap); - index_push(index * 2 + 2); - index_push(index * 2 + 1 + swap); - }; - - let mut pvert = move |pos: Vec3| { - verts.push(MeshVertex { - position: pos.into(), - color, - normal, - uv: [0.0; 2], - tangent: [0.0; 4], - }); - }; - - let mut index: u32 = 0; - for (a, elbow, c) in points.tuple_windows() { - let a: Vec3 = a; - let elbow: Vec3 = elbow; - let c: Vec3 = c; - if index == 0 { - let nor = -first_dir.perpendicular(); - pvert(a + (nor * (offset + halfthick)).z0()); - pvert(a + (nor * (offset - halfthick)).z0()); - } + self.meshbuilder + .extend_with(None, move |verts, index_push| { + let mut idx_quad = move |index| { + index_push(index * 2 + swap); + index_push(index * 2 + 1); + index_push(index * 2 + 2 - swap); + + index_push(index * 2 + 3 - swap); + index_push(index * 2 + 2); + index_push(index * 2 + 1 + swap); + }; - let ae = unwrap_or!((elbow - a).xy().try_normalize(), continue); - let ce = unwrap_or!((elbow - c).xy().try_normalize(), continue); - - let dir = match (ae + ce).try_normalize() { - Some(x) => { - let d = ae.perp_dot(ce); - if d.abs() < 0.01 { - -ae.perpendicular() - } else if d < 0.0 { - -x - } else { - x - } - } - None => -ae.perpendicular(), + let mut pvert = move |pos: Vec3| { + verts.push(MeshVertex { + position: pos.into(), + color, + normal, + uv: [0.0; 2], + tangent: [0.0; 4], + }); }; - let mul = 1.0 + (1.0 + ae.dot(ce).min(0.0)) * (std::f32::consts::SQRT_2 - 1.0); + let mut index: u32 = 0; + for (a, elbow, c) in points.tuple_windows() { + let a: Vec3 = a; + let elbow: Vec3 = elbow; + let c: Vec3 = c; + if index == 0 { + let nor = -first_dir.perpendicular(); + pvert(a + (nor * (offset + halfthick)).z0()); + pvert(a + (nor * (offset - halfthick)).z0()); + } - let p1 = elbow + (mul * dir * (offset + halfthick)).z0(); - let p2 = elbow + (mul * dir * (offset - halfthick)).z0(); - pvert(p1); - pvert(p2); - idx_quad(index); + let ae = unwrap_or!((elbow - a).xy().try_normalize(), continue); + let ce = unwrap_or!((elbow - c).xy().try_normalize(), continue); + + let dir = match (ae + ce).try_normalize() { + Some(x) => { + let d = ae.perp_dot(ce); + if d.abs() < 0.01 { + -ae.perpendicular() + } else if d < 0.0 { + -x + } else { + x + } + } + None => -ae.perpendicular(), + }; - index += 1; - if index as usize == n_points - 2 { - let nor = -last_dir.perpendicular(); + let mul = 1.0 + (1.0 + ae.dot(ce).min(0.0)) * (std::f32::consts::SQRT_2 - 1.0); - let p1 = c + ((offset + halfthick) * nor).z0(); - let p2 = c + ((offset - halfthick) * nor).z0(); + let p1 = elbow + (mul * dir * (offset + halfthick)).z0(); + let p2 = elbow + (mul * dir * (offset - halfthick)).z0(); pvert(p1); pvert(p2); idx_quad(index); + + index += 1; + if index as usize == n_points - 2 { + let nor = -last_dir.perpendicular(); + + let p1 = c + ((offset + halfthick) * nor).z0(); + let p2 = c + ((offset - halfthick) * nor).z0(); + pvert(p1); + pvert(p2); + idx_quad(index); + } } - } - }); + }); true } diff --git a/engine/src/gfx.rs b/engine/src/gfx.rs index abe228c3a..5942c0a46 100644 --- a/engine/src/gfx.rs +++ b/engine/src/gfx.rs @@ -6,7 +6,7 @@ use crate::{ UvVertex, TL, }; use common::FastMap; -use geom::{vec2, LinearColor, Matrix4, Vec2, Vec3}; +use geom::{vec2, Camera, InfiniteFrustrum, LinearColor, Matrix4, Plane, Vec2, Vec3}; use serde::{Deserialize, Serialize}; use std::cell::RefCell; use std::collections::HashMap; @@ -49,6 +49,7 @@ pub struct GfxContext { pub tick: u64, pub(crate) pipelines: RefCell, pub(crate) projection: Uniform, + pub(crate) frustrum: InfiniteFrustrum, pub(crate) sun_projection: [Uniform; N_CASCADES], pub render_params: Uniform, pub(crate) texture_cache_paths: FastMap>, @@ -344,6 +345,7 @@ impl GfxContext { default_material: Material::new_default(&device, &queue, &null_texture), tick: 0, projection, + frustrum: InfiniteFrustrum::new([Plane::X; 5]), sun_projection: [(); 4].map(|_| Uniform::new(Matrix4::zero(), &device)), render_params: Uniform::new(Default::default(), &device), texture_cache_paths: textures, @@ -541,12 +543,14 @@ impl GfxContext { self.render_params.value_mut().time = time; } - pub fn set_proj(&mut self, proj: Matrix4) { - *self.projection.value_mut() = proj; - } + pub fn set_camera(&mut self, cam: Camera) { + let viewproj = cam.build_view_projection_matrix(); + let inv_viewproj = viewproj.invert().unwrap_or_else(Matrix4::zero); + + *self.projection.value_mut() = viewproj; + self.render_params.value_mut().inv_proj = inv_viewproj; - pub fn set_inv_proj(&mut self, proj: Matrix4) { - self.render_params.value_mut().inv_proj = proj; + self.frustrum = InfiniteFrustrum::from_reversez_invviewproj(cam.eye(), inv_viewproj); } pub fn start_frame(&mut self, sco: &SurfaceTexture) -> (Encoders, TextureView) { diff --git a/engine/src/lamplights.rs b/engine/src/lamplights.rs index 398f77750..3e675f7c0 100644 --- a/engine/src/lamplights.rs +++ b/engine/src/lamplights.rs @@ -103,7 +103,7 @@ impl LampLights { 0.0, ); - let mut l = lights.collect::>(); + let mut l = lights.collect::>(); l.sort_unstable_by_key(|x| { OrderedFloat(x.distance2(origin + Vec3::splat(Self::LIGHTCHUNK_SIZE as f32 / 2.0))) }); diff --git a/engine/src/lib.rs b/engine/src/lib.rs index 90d2e5587..d3bb40ce1 100644 --- a/engine/src/lib.rs +++ b/engine/src/lib.rs @@ -23,6 +23,7 @@ mod texture; mod uniform; mod vertex_types; +mod meshbuild; #[cfg(feature = "yakui")] pub mod yakui; @@ -34,6 +35,7 @@ pub use gfx::*; pub use input::*; pub use lamplights::*; pub use material::*; +pub use meshbuild::*; pub use pbr::*; pub use perf_counters::*; pub use pipelines::*; @@ -46,5 +48,6 @@ pub use vertex_types::*; pub use winit::window::CursorGrabMode; pub use winit::window::CursorIcon; +pub use gltf; pub use image; pub use wgpu; diff --git a/engine/src/meshbuild.rs b/engine/src/meshbuild.rs new file mode 100644 index 000000000..51eff942d --- /dev/null +++ b/engine/src/meshbuild.rs @@ -0,0 +1,264 @@ +use crate::pbuffer::PBuffer; +use crate::{GfxContext, IndexType, MaterialID, Mesh, MeshVertex, MikktGeometry}; +use geom::{Sphere, Vec3, AABB3}; +use std::ops::Range; +use wgpu::BufferUsages; + +pub struct MeshBuilder { + vertices: Vec, + indices: Vec, + vi_buffers: Option>, + /// List of materialID and the starting offset + lods: Vec, + current_lod: usize, + default_mat: Option, +} + +struct MikktGenerate<'a> { + vertices: &'a mut [MeshVertex], + indices: &'a [IndexType], +} + +impl<'a> MikktGeometry for MikktGenerate<'a> { + fn num_faces(&self) -> usize { + self.indices.len() / 3 + } + + fn num_vertices_of_face(&self, _face: usize) -> usize { + 3 + } + + fn position(&self, face: usize, vert: usize) -> [f32; 3] { + let i = self.indices[face * 3 + vert] as usize; + self.vertices[i].position + } + + fn normal(&self, face: usize, vert: usize) -> [f32; 3] { + let i = self.indices[face * 3 + vert] as usize; + self.vertices[i].normal.into() + } + + fn tex_coord(&self, face: usize, vert: usize) -> [f32; 2] { + let i = self.indices[face * 3 + vert] as usize; + self.vertices[i].uv + } + + fn set_tangent_encoded(&mut self, tangent: [f32; 4], face: usize, vert: usize) { + let i = self.indices[face * 3 + vert] as usize; + self.vertices[i].tangent = tangent; + } +} + +impl MeshBuilder { + pub fn new(default_mat: MaterialID) -> Self { + Self { + default_mat: Some(default_mat), + ..Self::new_without_mat() + } + } + + pub fn new_without_mat() -> Self { + Self { + vertices: vec![], + indices: vec![], + vi_buffers: PERSISTENT.then(|| { + Box::new(( + PBuffer::new(BufferUsages::VERTEX), + PBuffer::new(BufferUsages::INDEX), + )) + }), + lods: vec![MeshLod::default()], + current_lod: 0, + default_mat: None, + } + } + + pub fn lods(&self) -> &[MeshLod] { + &self.lods + } + + pub fn clear(&mut self) { + self.vertices.clear(); + self.indices.clear(); + self.lods.clear(); + self.lods.push(MeshLod::default()); + } + + pub fn extend( + &mut self, + mat: Option, + vertices: &[MeshVertex], + indices: &[IndexType], + ) -> &mut Self { + self.set_material(mat); + let offset = self.vertices.len() as IndexType; + self.vertices.extend_from_slice(vertices); + self.indices.extend(indices.iter().map(|x| x + offset)); + self.lods[self.current_lod].n_vertices += vertices.len(); + self.lods[self.current_lod].n_indices += indices.len(); + self.finish_material(); + self + } + + /// Sets the material for all future indice pushes + fn set_material(&mut self, material: Option) { + let Some(material) = material.or(self.default_mat) else { + return; + }; + let n = self.indices.len() as u32; + let primitives = &mut self.lods[self.current_lod].primitives; + if let Some(previous) = primitives.last_mut() { + if previous.0 == material && previous.1.end == n { + return; + } + } + primitives.push((material, n..n)); + } + + /// Finishes the current material + fn finish_material(&mut self) { + let n = self.indices.len() as u32; + let primitives = &mut self.lods[self.current_lod].primitives; + if let Some(previous) = primitives.last_mut() { + previous.1.end = n; + } + } + + /// new_lod indicates that all future vertex/index/material pushes will be in a new lod + pub fn set_lod(&mut self, lod_level: usize, coverage: f64) { + if self.lods.len() <= lod_level { + self.lods + .extend((self.lods.len()..=lod_level).map(|_| MeshLod::default())); + } + self.current_lod = lod_level; + self.lods[self.current_lod].screen_coverage += coverage as f32; + } + + /// Sets the bounds for the current lod + pub fn set_bounds(&mut self, bounds: AABB3) { + let aabb3 = &mut self.lods[self.current_lod].aabb3; + if aabb3.ll == Vec3::ZERO && aabb3.ur == Vec3::ZERO { + *aabb3 = bounds; + return; + } + *aabb3 = aabb3.union(bounds); + } + + #[inline(always)] + pub fn extend_with( + &mut self, + mat: Option, + f: impl FnOnce(&mut Vec, &mut dyn FnMut(IndexType)), + ) { + self.set_material(mat); + let offset = self.vertices.len() as IndexType; + let vertices = &mut self.vertices; + let indices = &mut self.indices; + let n_vertices = vertices.len(); + let n_indices = indices.len(); + let mut x = move |index: IndexType| { + indices.push(index + offset); + }; + f(vertices, &mut x); + self.finish_material(); + self.lods[self.current_lod].n_vertices += self.vertices.len() - n_vertices; + self.lods[self.current_lod].n_indices += self.indices.len() - n_indices; + } + + pub fn compute_tangents(&mut self) { + for lod in &mut self.lods { + for (_, range) in &lod.primitives { + let mut mg = MikktGenerate { + vertices: &mut self.vertices, + indices: &self.indices[range.start as usize..range.end as usize], + }; + if !crate::geometry::generate_tangents(&mut mg) { + log::warn!("failed to generate tangents"); + } + } + } + } + + pub fn build(&mut self, gfx: &GfxContext) -> Option { + if self.vertices.is_empty() { + return None; + } + self.finish_material(); + + let mut tmpv; + let mut tmpi; + let vbuffer; + let ibuffer; + + if PERSISTENT { + let x = self.vi_buffers.as_deref_mut().unwrap(); + vbuffer = &mut x.0; + ibuffer = &mut x.1; + } else { + tmpv = PBuffer::new(BufferUsages::VERTEX); + tmpi = PBuffer::new(BufferUsages::INDEX); + vbuffer = &mut tmpv; + ibuffer = &mut tmpi; + } + + vbuffer.write(gfx, bytemuck::cast_slice(&self.vertices)); + ibuffer.write(gfx, bytemuck::cast_slice(&self.indices)); + + // Compute AABB3 for each lod + for lod in &mut self.lods { + if lod.aabb3.ll != Vec3::ZERO || lod.aabb3.ur != Vec3::ZERO { + continue; + } + let mut aabb3 = AABB3::zero(); + for (_, range) in &lod.primitives { + for &idx in &self.indices[range.start as usize..range.end as usize] { + aabb3 = aabb3.union_vec(self.vertices[idx as usize].position.into()); + } + } + lod.aabb3 = aabb3; + } + + for lod in &mut self.lods { + lod.bounding_sphere = lod.aabb3.bounding_sphere(); + } + + Some(Mesh { + vertex_buffer: vbuffer.inner()?, + index_buffer: ibuffer.inner()?, + lods: self.lods.clone().into_boxed_slice(), + skip_depth: false, + }) + } +} + +#[derive(Debug, Default, Clone)] +pub struct MeshLod { + /// List of materialID and the index range + pub primitives: Vec<(MaterialID, Range)>, + /// Percentage of vertical space the mesh takes up on screen before it switches to the next lod + pub screen_coverage: f32, + pub aabb3: AABB3, + pub bounding_sphere: Sphere, + pub n_vertices: usize, + pub n_indices: usize, +} + +impl MeshLod { + pub fn draw_calls(&self) -> usize { + self.primitives.len() + } + + pub fn n_vertices(&self) -> usize { + self.n_vertices + } + + pub fn n_indices(&self) -> usize { + self.n_indices + } + + #[inline] + pub fn passes_culling(&self, gfx: &GfxContext) -> bool { + let screen_area = crate::screen_coverage(gfx, self.bounding_sphere); + screen_area >= self.screen_coverage + } +} diff --git a/engine/src/meshload.rs b/engine/src/meshload.rs index ae4ebe46c..29acb5e6a 100644 --- a/engine/src/meshload.rs +++ b/engine/src/meshload.rs @@ -1,14 +1,15 @@ +use crate::meshbuild::MeshBuilder; use crate::{ - GfxContext, IndexType, Material, MaterialID, Mesh, MeshBuilder, MeshVertex, MetallicRoughness, - Texture, TextureBuilder, + GfxContext, IndexType, Material, MaterialID, Mesh, MeshVertex, MetallicRoughness, Texture, + TextureBuilder, }; -use geom::{Color, LinearColor, Matrix4, Quaternion, Vec2, Vec3}; +use geom::{Color, LinearColor, Matrix4, Quaternion, Vec2, Vec3, AABB3}; +use gltf::buffer::Source; use gltf::image::{Data, Format}; use gltf::json::texture::{MagFilter, MinFilter}; use gltf::texture::WrappingMode; -use gltf::Document; +use gltf::{Document, Node, Scene}; use image::{DynamicImage, ImageBuffer}; -use smallvec::SmallVec; use std::collections::hash_map::Entry; use std::path::PathBuf; use std::sync::Arc; @@ -220,6 +221,7 @@ pub enum LoadMeshError { NoIndices, NoVertices, InvalidImage(ImageLoadError), + NoDefaultScene, } impl From for LoadMeshError { @@ -228,60 +230,134 @@ impl From for LoadMeshError { } } -#[derive(Copy, Clone, Default)] -pub struct MeshProperties { - pub n_vertices: usize, - pub n_triangles: usize, - pub n_materials: usize, +#[derive(Clone)] +pub struct CPUMesh { pub n_textures: usize, - pub n_draw_calls: usize, + pub n_triangles: usize, + pub gltf_doc: Document, + pub gltf_data: Vec, + pub asset_path: PathBuf, +} + +pub fn glb_buffer_id(document: &Document) -> usize { + document + .buffers() + .enumerate() + .find(|(_, b)| matches!(b.source(), Source::Bin)) + .unwrap() + .0 +} + +fn mat_rot(node: &Node) -> (Matrix4, Quaternion) { + let transform = node.transform(); + let rot_qat = Quaternion::from(transform.clone().decomposed().1); + let transform_mat = Matrix4::from(transform.matrix()); + (transform_mat, rot_qat) +} + +/// Returns a list of nodes, their LOD level, screen coverage and their global transforms +pub fn find_nodes<'a>( + scene: &'a Scene, + getnode: impl Fn(usize) -> Node<'a>, +) -> Vec<(Node<'a>, usize, f64, Matrix4, Quaternion)> { + let mut result = Vec::new(); + for node in scene.nodes() { + let (mat, rot) = mat_rot(&node); + result.push((node, 0, 0.0, mat, rot)); + } + let mut traversed = 0; + while traversed < result.len() { + let (node, lod_level, _, parent_mat, parent_rot) = result[traversed].clone(); + if lod_level > 0 { + traversed += 1; + continue; + } + + if let Some(v) = node.extension_value("MSFT_lod") { + let coverage = v.get("screencoverage").map(|v| { + v.as_array() + .unwrap() + .into_iter() + .map(|v| v.as_f64().unwrap()) + .collect::>() + }); + + if let Some(coverage) = coverage { + let mut coverage_iter = coverage.into_iter(); + + if let Some(arr) = v.get("ids").and_then(|ids| ids.as_array()) { + result[traversed].2 = coverage_iter.next().unwrap(); + + for ((lod_level, v), coverage) in arr.iter().enumerate().zip(coverage_iter) { + if let Some(id) = v.as_u64() { + let node = getnode(id as usize); + result.push((node, 1 + lod_level, coverage, parent_mat, parent_rot)); + } + } + } + } + } + + result.extend(node.children().map(|node| { + let (mat, rot) = mat_rot(&node); + (node, 0, 0.0, parent_mat * mat, parent_rot * rot) + })); + + traversed += 1; + } + + result } pub fn load_mesh(gfx: &mut GfxContext, asset_name: &str) -> Result { - load_mesh_with_properties(gfx, asset_name).map(|x| x.0) + load_mesh_with_properties(gfx, asset_name, false).map(|x| x.0) } pub fn load_mesh_with_properties( gfx: &mut GfxContext, asset_name: &str, -) -> Result<(Mesh, MeshProperties), LoadMeshError> { + force_base_model: bool, +) -> Result<(Mesh, CPUMesh), LoadMeshError> { let mut path = PathBuf::new(); - path.push("assets/models/"); + path.push("assets/models_opt/"); path.push(asset_name); - let t = Instant::now(); + if !path.exists() || force_base_model { + path.clear(); + path.push("assets/models/"); + path.push(asset_name); + } - let mut props = MeshProperties::default(); - let mut flat_vertices: Vec = vec![]; - let mut indices = vec![]; - let mut materials_idx = SmallVec::new(); + let t = Instant::now(); let (doc, data, images) = gltf::import(&path).map_err(LoadMeshError::GltfLoadError)?; let exts = doc .extensions_used() + .filter(|x| !matches!(x, &"MSFT_lod")) .fold(String::new(), |a, b| a + ", " + b); if !exts.is_empty() { log::warn!("extension not supported: {}", exts) } - let nodes = doc.nodes(); + + let scene = doc.default_scene().ok_or(LoadMeshError::NoDefaultScene)?; let (mats, needs_tangents) = load_materials(gfx, &doc, &images)?; - props.n_materials = mats.len(); - props.n_textures = images.len(); + let mut meshb = MeshBuilder::::new_without_mat(); - for node in nodes { - let mesh = unwrap_cont!(node.mesh()); - let transform = node.transform(); - let rot_qat = Quaternion::from(transform.clone().decomposed().1); - let transform_mat = Matrix4::from(transform.matrix()); + let getnode = |id| doc.nodes().nth(id).unwrap(); + for (node, lod_id, coverage, transform_mat, rot_qat) in find_nodes(&scene, getnode) { + let mesh = unwrap_cont!(node.mesh()); let mut primitives = mesh.primitives().collect::>(); primitives.sort_unstable_by_key(|x| x.material().index()); - let mut last_mat = None; + meshb.set_lod(lod_id, coverage); + for primitive in primitives { + let bbox = primitive.bounding_box(); + let reader = primitive.reader(|b| Some(&data.get(b.index())?.0[..b.length()])); let matid = primitive .material() @@ -294,66 +370,50 @@ pub fn load_mesh_with_properties( .into_f32() .map(Vec2::from); let read_indices: Vec = unwrap_cont!(reader.read_indices()).into_u32().collect(); - let raw: Vec<_> = positions - .zip(normals) - .zip(uv) - .map(|((p, n), uv)| { - let pos = transform_mat * p.w(1.0); - let pos = pos.xyz() / pos.w; - (pos, rot_qat * n, uv) - }) - .collect(); - - if raw.is_empty() { - continue; - } - - if last_mat != Some(matid) { - materials_idx.push((mats[matid], indices.len() as u32)); - } - last_mat = Some(matid); - - props.n_draw_calls += 1; - props.n_triangles += read_indices.len() / 3; - props.n_vertices += raw.len(); - - let vtx_offset = flat_vertices.len() as IndexType; - for (pos, normal, uv) in &raw { - flat_vertices.push(MeshVertex { - position: pos.into(), - normal: *normal, - uv: (*uv).into(), - color: [1.0, 1.0, 1.0, 1.0], - tangent: [0.0; 4], - }) - } - - for &[a, b, c] in bytemuck::cast_slice::(&read_indices) { - indices.push(vtx_offset + a as IndexType); - indices.push(vtx_offset + b as IndexType); - indices.push(vtx_offset + c as IndexType); - } + let raw = positions.zip(normals).zip(uv).map(|((p, n), uv)| { + let pos = transform_mat * p.w(1.0); + let pos = pos.xyz() / pos.w; + (pos, rot_qat * n, uv) + }); + + meshb.extend_with(Some(mats[matid]), |vertices, add_idx| { + for (pos, normal, uv) in raw { + vertices.push(MeshVertex { + position: pos.into(), + normal, + uv: uv.into(), + color: [1.0, 1.0, 1.0, 1.0], + tangent: [0.0; 4], + }) + } + for idx in read_indices { + add_idx(idx as IndexType); + } + }); + meshb.set_bounds(AABB3 { + ll: bbox.min.into(), + ur: bbox.max.into(), + }); } } - if indices.is_empty() { - return Err(LoadMeshError::NoIndices); - } + let props = CPUMesh { + n_textures: images.len(), + n_triangles: meshb.lods()[0].n_indices / 3, + gltf_doc: doc, + gltf_data: data, + asset_path: path, + }; - let mut meshb = MeshBuilder::::new_without_mat(); - meshb.vertices = flat_vertices; - meshb.indices = indices; - meshb.materials = materials_idx; if needs_tangents { meshb.compute_tangents(); } let m = meshb.build(gfx).ok_or(LoadMeshError::NoVertices)?; log::info!( - "loaded mesh {:?} in {}ms ({} tris){}", - path, + "loaded mesh {:?} in {}ms{}", + &props.asset_path, 1000.0 * t.elapsed().as_secs_f32(), - m.materials.iter().map(|x| x.1).sum::() / 3, if needs_tangents { " (tangents)" } else { "" } ); diff --git a/engine_demo/src/main.rs b/engine_demo/src/main.rs index 6ed87a052..b1471070b 100644 --- a/engine_demo/src/main.rs +++ b/engine_demo/src/main.rs @@ -4,9 +4,7 @@ use engine::{ Context, FrameContext, GfxSettings, InstancedMeshBuilder, Key, MeshInstance, MouseButton, ShadowQuality, }; -use geom::{ - vec3, Camera, Degrees, InfiniteFrustrum, LinearColor, Matrix4, Plane, Radians, Vec2, Vec3, -}; +use geom::{vec3, Camera, Degrees, InfiniteFrustrum, LinearColor, Plane, Radians, Vec2, Vec3}; use crate::helmet::Helmet; use crate::spheres::Spheres; @@ -110,10 +108,7 @@ impl engine::framework::State for State { let gfx = &mut ctx.gfx; - let viewproj = self.camera.build_view_projection_matrix(); - let inv_viewproj = viewproj.invert().unwrap_or_else(Matrix4::zero); - gfx.set_proj(viewproj); - gfx.set_inv_proj(inv_viewproj); + gfx.set_camera(self.camera); let params = gfx.render_params.value_mut(); params.time_always = (params.time_always + delta) % 3600.0; diff --git a/engine_demo/src/spheres.rs b/engine_demo/src/spheres.rs index 4ab75d137..01087638f 100644 --- a/engine_demo/src/spheres.rs +++ b/engine_demo/src/spheres.rs @@ -19,7 +19,7 @@ impl DemoElement for Spheres { let gfx = &mut ctx.gfx; let mesh = load_mesh(gfx, "sphere.glb").unwrap(); - let alb = gfx.material(mesh.materials[0].0).albedo.clone(); + let alb = gfx.material(mesh.lods[0].primitives[0].0).albedo.clone(); let mut meshes = vec![]; @@ -30,7 +30,7 @@ impl DemoElement for Spheres { for z in 0..N_MET { let mut c = mesh.clone(); - c.materials[0].0 = gfx.register_material(Material::new_raw( + c.lods[0].primitives[0].0 = gfx.register_material(Material::new_raw( &gfx.device, alb.clone(), MetallicRoughness { diff --git a/geom/src/aabb3.rs b/geom/src/aabb3.rs index c30854837..35413072d 100644 --- a/geom/src/aabb3.rs +++ b/geom/src/aabb3.rs @@ -1,8 +1,8 @@ -use super::{Ray3, Vec3}; +use super::{Ray3, Sphere, Vec3}; use crate::{Intersect3, Shape3, AABB}; use serde::{Deserialize, Serialize}; -#[derive(Copy, Clone, Debug, Serialize, Deserialize)] +#[derive(Default, Copy, Clone, Debug, Serialize, Deserialize)] pub struct AABB3 { pub ll: Vec3, pub ur: Vec3, @@ -67,6 +67,14 @@ impl AABB3 { } } + #[inline] + pub fn union_vec(self, other: Vec3) -> Self { + Self { + ll: self.ll.min(other), + ur: self.ur.max(other), + } + } + #[inline] pub fn center(&self) -> Vec3 { self.ll * 0.5 + self.ur * 0.5 @@ -80,6 +88,13 @@ impl AABB3 { } } + #[inline] + pub fn bounding_sphere(&self) -> Sphere { + let center = self.center(); + let radius = (self.ur - center).mag(); + Sphere { center, radius } + } + #[inline(always)] pub fn compute_code(&self, p: Vec3) -> u8 { const LEFT: u8 = 1; // 0001 diff --git a/geom/src/lib.rs b/geom/src/lib.rs index b54c00c57..d2df887d7 100644 --- a/geom/src/lib.rs +++ b/geom/src/lib.rs @@ -50,6 +50,7 @@ mod ray3; mod segment; mod segment3; pub mod skeleton; +mod sphere; mod spline3; mod splines; mod transform; @@ -83,6 +84,7 @@ pub use ray::*; pub use ray3::*; pub use segment::*; pub use segment3::*; +pub use sphere::*; pub use spline3::*; pub use splines::*; pub use transform::*; diff --git a/geom/src/matrix4.rs b/geom/src/matrix4.rs index 57c6c86a5..1577e5666 100644 --- a/geom/src/matrix4.rs +++ b/geom/src/matrix4.rs @@ -66,6 +66,14 @@ impl Mul for Matrix4 { } } +impl<'a> Mul for &'a Matrix4 { + type Output = Vec4; + + fn mul(self, rhs: Vec4) -> Self::Output { + self.x * rhs.x + self.y * rhs.y + self.z * rhs.z + self.w * rhs.w + } +} + impl Mul for Matrix4 { type Output = Matrix4; diff --git a/geom/src/quaternion.rs b/geom/src/quaternion.rs index 0e9701af2..da85f3dcc 100644 --- a/geom/src/quaternion.rs +++ b/geom/src/quaternion.rs @@ -21,6 +21,24 @@ impl Mul for Quaternion { } } +impl Mul for Quaternion { + type Output = Quaternion; + + #[inline] + fn mul(self, rhs: Quaternion) -> Quaternion { + let q1 = vec3(self.x, self.y, self.z); + let q2 = vec3(rhs.x, rhs.y, rhs.z); + let w1 = self.w; + let w2 = rhs.w; + Quaternion { + x: w1 * q2.x + w2 * q1.x + q1.cross(q2).x, + y: w1 * q2.y + w2 * q1.y + q1.cross(q2).y, + z: w1 * q2.z + w2 * q1.z + q1.cross(q2).z, + w: w1 * w2 - q1.dot(q2), + } + } +} + impl From<[f32; 4]> for Quaternion { #[inline] fn from(x: [f32; 4]) -> Self { diff --git a/geom/src/sphere.rs b/geom/src/sphere.rs new file mode 100644 index 000000000..4386c3e52 --- /dev/null +++ b/geom/src/sphere.rs @@ -0,0 +1,22 @@ +use crate::Vec3; + +#[derive(Default, Copy, Clone, Debug)] +pub struct Sphere { + pub center: Vec3, + pub radius: f32, +} + +impl Sphere { + #[inline] + pub const fn new(center: Vec3, radius: f32) -> Self { + Self { center, radius } + } + + #[inline] + pub const fn zero() -> Self { + Self { + center: Vec3::ZERO, + radius: 0.0, + } + } +} diff --git a/goryak/src/dragvalue.rs b/goryak/src/dragvalue.rs index d852b58a3..99134d6be 100644 --- a/goryak/src/dragvalue.rs +++ b/goryak/src/dragvalue.rs @@ -1,5 +1,5 @@ use yakui_core::geometry::Vec2; -use yakui_core::MainAxisAlignment; +use yakui_core::{CrossAxisAlignment, MainAxisSize}; use yakui_widgets::widgets::{List, Pad}; use yakui_widgets::{draggable, pad, use_state}; @@ -41,10 +41,16 @@ macro_rules! impl_slidable { }; } -impl_slidable!(i32; 1.0, - u32; 1.0, +impl_slidable!(u8; 1.0, + i8; 1.0, + u16; 1.0, + i16; 1.0, + u32; 1.0, + i32; 1.0, + u64; 1.0, i64; 1.0, - u64; 1.0, + usize; 1.0, + isize; 1.0, f32; 0.01, f64; 0.01); @@ -79,31 +85,31 @@ impl DragValue { pub fn show(self, value: &mut T) -> bool { let mut changed = false; - RoundRect::new(2.0) - .outline(outline(), 2.0) - .color(secondary()) - .show_children(|| { - let dragged = draggable_delta(|| { - pad(Pad::horizontal(10.0), || { - let mut l = List::row(); - l.main_axis_alignment = MainAxisAlignment::Center; - l.show(|| { - labelc(on_primary(), format!("{}", T::to_f64(*value))); + let mut l = List::column(); + l.cross_axis_alignment = CrossAxisAlignment::Center; + l.main_axis_size = MainAxisSize::Min; + l.show(|| { + let dragged = draggable_delta(|| { + RoundRect::new(2.0) + .outline(outline(), 2.0) + .color(secondary()) + .show_children(|| { + pad(Pad::horizontal(10.0), || { + labelc(on_primary(), format!("{:.3}", T::to_f64(*value))); }); }); - }); - - if let Some(dragged) = dragged { - let oldv = T::to_f64(*value); - let newv = oldv + dragged.x as f64 * self.step.unwrap_or(T::default_step()); - - *value = T::from_f64(newv.clamp( - self.min.unwrap_or(T::DEFAULT_MIN), - self.max.unwrap_or(T::DEFAULT_MAX), - )); - changed = true; - } }); + if let Some(dragged) = dragged { + let oldv = T::to_f64(*value); + let newv = oldv + dragged.x as f64 * self.step.unwrap_or(T::default_step()); + + *value = T::from_f64(newv.clamp( + self.min.unwrap_or(T::DEFAULT_MIN), + self.max.unwrap_or(T::DEFAULT_MAX), + )); + changed = true; + } + }); changed } @@ -117,7 +123,7 @@ fn draggable_delta(children: impl FnOnce()) -> Option { }; let last_val = last_val_state.get().unwrap_or(d.current); - let mut delta = d.current - last_val; + let mut delta = (d.current - last_val) / 10.0; if delta.x.abs() < 1.0 { d.current.x = last_val.x; delta.x = 0.0; diff --git a/native_app/src/rendering/map_rendering/map_mesh.rs b/native_app/src/rendering/map_rendering/map_mesh.rs index 16d486dd0..cf4606dd3 100644 --- a/native_app/src/rendering/map_rendering/map_mesh.rs +++ b/native_app/src/rendering/map_rendering/map_mesh.rs @@ -2,9 +2,10 @@ use crate::rendering::MapRenderOptions; use common::FastMap; use engine::earcut::earcut; use engine::meshload::load_mesh; +use engine::MeshBuilder; use engine::{ - Drawable, FrameContext, GfxContext, InstancedMeshBuilder, Material, Mesh, MeshBuilder, - MeshInstance, MeshVertex, MetallicRoughness, SpriteBatch, SpriteBatchBuilder, Tesselator, + Drawable, FrameContext, GfxContext, InstancedMeshBuilder, Material, Mesh, MeshInstance, + MeshVertex, MetallicRoughness, SpriteBatch, SpriteBatchBuilder, Tesselator, }; use geom::{minmax, vec2, vec3, Color, LinearColor, PolyLine3, Polygon, Radians, Vec2, Vec3}; use simulation::map::{ @@ -304,28 +305,29 @@ impl MapBuilders { let pos = from + dir * WALKING_W * 0.5; let height = l - WALKING_W; - self.crosswalk_builder.extend_with(|vertices, add_index| { - let mk_v = |position: Vec3, uv: Vec2| MeshVertex { - position: position.into(), - uv: uv.into(), - normal: Vec3::Z, - color: [1.0; 4], - tangent: [0.0; 4], - }; - - vertices.push(mk_v(pos - perp, Vec2::ZERO)); - vertices.push(mk_v(pos + perp, Vec2::ZERO)); - vertices.push(mk_v(pos + perp + dir * height, Vec2::x(height))); - vertices.push(mk_v(pos - perp + dir * height, Vec2::x(height))); - - add_index(0); - add_index(1); - add_index(2); - - add_index(0); - add_index(2); - add_index(3); - }); + self.crosswalk_builder + .extend_with(None, |vertices, add_index| { + let mk_v = |position: Vec3, uv: Vec2| MeshVertex { + position: position.into(), + uv: uv.into(), + normal: Vec3::Z, + color: [1.0; 4], + tangent: [0.0; 4], + }; + + vertices.push(mk_v(pos - perp, Vec2::ZERO)); + vertices.push(mk_v(pos + perp, Vec2::ZERO)); + vertices.push(mk_v(pos + perp + dir * height, Vec2::x(height))); + vertices.push(mk_v(pos - perp + dir * height, Vec2::x(height))); + + add_index(0); + add_index(1); + add_index(2); + + add_index(0); + add_index(2); + add_index(3); + }); } } } @@ -459,7 +461,7 @@ impl MapBuilders { let avg = -zone.0.iter().sum::() / zone.len() as f32; - zone_mesh.extend_with(|vertices, add_index| { + zone_mesh.extend_with(None, |vertices, add_index| { for p in &zone.0 { vertices.push(MeshVertex { position: p.z(building.height + 0.05).into(), @@ -480,7 +482,7 @@ impl MapBuilders { fn houses_mesh(&mut self, building: &Building) { for (face, col) in &building.mesh.faces { - self.houses_mesh.extend_with(|vertices, add_index| { + self.houses_mesh.extend_with(None, |vertices, add_index| { let o = face[1]; let u = unwrap_ret!((face[0] - o).try_normalize()); let v = unwrap_ret!((face[2] - o).try_normalize()); @@ -788,7 +790,7 @@ fn add_polyon( let mr = &mut meshb; let mut quad = move |a, b, c, d, nor| { - mr.extend_with(move |vertices, add_idx| { + mr.extend_with(None, move |vertices, add_idx| { let mut pvert = move |p: Vec3, normal: Vec3| { vertices.push(MeshVertex { position: p.into(), @@ -951,21 +953,22 @@ fn intersection_mesh( polygon.simplify(); let col = LinearColor::from(simulation::config().road_mid_col).into(); - tess.meshbuilder.extend_with(move |vertices, add_idx| { - vertices.extend(polygon.iter().map(|pos| MeshVertex { - position: pos.z(inter.pos.z - 0.001).into(), - normal: Vec3::Z, - uv: [0.0; 2], - color: col, - tangent: [0.0; 4], - })); - earcut(&polygon.0, &[], |a, b, c| { - add_idx(a as u32); - add_idx(b as u32); - add_idx(c as u32); - add_idx(c as u32); - add_idx(b as u32); - add_idx(a as u32); + tess.meshbuilder + .extend_with(None, move |vertices, add_idx| { + vertices.extend(polygon.iter().map(|pos| MeshVertex { + position: pos.z(inter.pos.z - 0.001).into(), + normal: Vec3::Z, + uv: [0.0; 2], + color: col, + tangent: [0.0; 4], + })); + earcut(&polygon.0, &[], |a, b, c| { + add_idx(a as u32); + add_idx(b as u32); + add_idx(c as u32); + add_idx(c as u32); + add_idx(b as u32); + add_idx(a as u32); + }); }); - }); } diff --git a/native_app/src/rendering/orbit_camera.rs b/native_app/src/rendering/orbit_camera.rs index 169b93cd4..5bdf038e8 100644 --- a/native_app/src/rendering/orbit_camera.rs +++ b/native_app/src/rendering/orbit_camera.rs @@ -1,11 +1,13 @@ #![allow(clippy::redundant_closure_call)] -use crate::gui::windows::settings::Settings; -use crate::inputmap::{InputAction, InputMap}; + use common::saveload::Encoder; use engine::{Context, Tesselator}; -use geom::{Camera, InfiniteFrustrum, Matrix4, Plane, Radians, Vec2, Vec3, AABB}; +use geom::{Camera, InfiniteFrustrum, Plane, Radians, Vec2, Vec3, AABB}; use simulation::map::pathfinding_crate::num_traits::Pow; +use crate::gui::windows::settings::Settings; +use crate::inputmap::{InputAction, InputMap}; + /// CameraHandler3D is the camera handler for the 3D view /// It controls the camera using an orbit view pub struct OrbitCamera { @@ -21,17 +23,7 @@ pub struct OrbitCamera { impl OrbitCamera { pub fn update(&mut self, ctx: &mut Context) { - let viewproj = self.camera.build_view_projection_matrix(); - let inv_viewproj = viewproj.invert().unwrap_or_else(Matrix4::zero); - - if inline_tweak::tweak!(true) { - self.frustrum = - InfiniteFrustrum::from_reversez_invviewproj(self.camera.eye(), inv_viewproj); - } - - ctx.gfx.set_proj(viewproj); - ctx.gfx.set_inv_proj(inv_viewproj); - + ctx.gfx.set_camera(self.camera); let params = ctx.gfx.render_params.value_mut(); params.cam_pos = self.camera.eye(); params.cam_dir = -self.camera.dir();