diff --git a/Cargo.toml b/Cargo.toml index fa22b5d8..31816d85 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ bytemuck = { version = "1", features = ["derive"] } choir = "0.7" egui = "0.23" glam = { version = "0.24", features = ["mint"] } +gltf = { version = "1.1", default-features = false } log = "0.4" mint = "0.5" naga = { version = "0.14", features = ["wgsl-in", "span", "validate"] } @@ -44,9 +45,11 @@ blade-asset = { version = "0.2", path = "blade-asset" } blade-egui = { version = "0.2", path = "blade-egui" } blade-graphics = { version = "0.3", path = "blade-graphics" } blade-render = { version = "0.2", path = "blade-render" } +base64 = { workspace = true } choir = { workspace = true } colorsys = "0.6" egui = { workspace = true } +gltf = { workspace = true } nalgebra = { version = "0.32", features = ["mint"] } log = { workspace = true } mint = { workspace = true, features = ["serde"] } diff --git a/blade-render/Cargo.toml b/blade-render/Cargo.toml index abad5b94..0aa50d99 100644 --- a/blade-render/Cargo.toml +++ b/blade-render/Cargo.toml @@ -22,7 +22,7 @@ blade-macros = { version = "0.2.1", path = "../blade-macros" } bytemuck = { workspace = true } choir = { workspace = true } exr = { version = "1.6", optional = true } -gltf = { version = "1.1", default-features = false, features = ["names", "utils"], optional = true } +gltf = { workspace = true, features = ["names", "utils"], optional = true } glam = { workspace = true } log = { workspace = true } mikktspace = { package = "bevy_mikktspace", version = "0.10", optional = true } diff --git a/src/config.rs b/src/config.rs index b483f203..f79238c6 100644 --- a/src/config.rs +++ b/src/config.rs @@ -28,10 +28,28 @@ impl Default for Visual { #[derive(serde::Deserialize)] pub enum Shape { - Ball { radius: f32 }, - Cylinder { half_height: f32, radius: f32 }, - Cuboid { half: mint::Vector3 }, - ConvexHull { points: Vec> }, + Ball { + radius: f32, + }, + Cylinder { + half_height: f32, + radius: f32, + }, + Cuboid { + half: mint::Vector3, + }, + ConvexHull { + points: Vec>, + #[serde(default)] + border_radius: f32, + }, + TriMesh { + model: String, + #[serde(default)] + convex: bool, + #[serde(default)] + border_radius: f32, + }, } fn default_friction() -> f32 { diff --git a/src/lib.rs b/src/lib.rs index 9f6c2795..bb36c6f2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -18,6 +18,7 @@ use blade_graphics as gpu; use std::{ops, path::Path, sync::Arc}; pub mod config; +mod trimesh; #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub enum JointKind { @@ -273,7 +274,7 @@ impl Engine { let render_config = blade_render::RenderConfig { screen_size, surface_format, - max_debug_lines: 1000, + max_debug_lines: 1 << 14, }; let renderer = blade_render::Renderer::new( command_encoder, @@ -639,26 +640,54 @@ impl Engine { let mut colliders = Vec::new(); for cc in config.colliders.iter() { + use rapier3d::geometry::ColliderBuilder; + let isometry = nalgebra::geometry::Isometry3::from_parts( nalgebra::Vector3::from(cc.pos).into(), make_quaternion(cc.rot), ); let builder = match cc.shape { - config::Shape::Ball { radius } => rapier3d::geometry::ColliderBuilder::ball(radius), + config::Shape::Ball { radius } => ColliderBuilder::ball(radius), config::Shape::Cylinder { half_height, radius, - } => rapier3d::geometry::ColliderBuilder::cylinder(half_height, radius), - config::Shape::Cuboid { half } => { - rapier3d::geometry::ColliderBuilder::cuboid(half.x, half.y, half.z) - } - config::Shape::ConvexHull { ref points } => { + } => ColliderBuilder::cylinder(half_height, radius), + config::Shape::Cuboid { half } => ColliderBuilder::cuboid(half.x, half.y, half.z), + config::Shape::ConvexHull { + ref points, + border_radius, + } => { let pv = points .iter() .map(|p| nalgebra::Vector3::from(*p).into()) .collect::>(); - rapier3d::geometry::ColliderBuilder::convex_hull(&pv) - .expect("Unable to build convex full") + let result = if border_radius != 0.0 { + ColliderBuilder::round_convex_hull(&pv, border_radius) + } else { + ColliderBuilder::convex_hull(&pv) + }; + result.expect("Unable to build convex hull shape") + } + config::Shape::TriMesh { + ref model, + convex, + border_radius, + } => { + let trimesh = trimesh::load(&format!("data/{}", model)); + if convex && border_radius != 0.0 { + ColliderBuilder::round_convex_mesh( + trimesh.points, + &trimesh.triangles, + border_radius, + ) + .expect("Unable to build rounded convex mesh") + } else if convex { + ColliderBuilder::convex_mesh(trimesh.points, &trimesh.triangles) + .expect("Unable to build convex mesh") + } else { + assert_eq!(border_radius, 0.0); + ColliderBuilder::trimesh(trimesh.points, trimesh.triangles) + } } }; let collider = builder diff --git a/src/trimesh.rs b/src/trimesh.rs new file mode 100644 index 00000000..24d70679 --- /dev/null +++ b/src/trimesh.rs @@ -0,0 +1,102 @@ +use std::unimplemented; + +#[derive(Default)] +pub struct TriMesh { + pub points: Vec>, + pub triangles: Vec<[u32; 3]>, +} + +impl TriMesh { + fn populate_from_gltf( + &mut self, + g_node: gltf::Node, + parent_transform: nalgebra::Matrix4, + data_buffers: &[Vec], + ) { + let name = g_node.name().unwrap_or(""); + let transform = parent_transform * nalgebra::Matrix4::from(g_node.transform().matrix()); + + for child in g_node.children() { + self.populate_from_gltf(child, transform, data_buffers); + } + + let g_mesh = match g_node.mesh() { + Some(mesh) => mesh, + None => return, + }; + + for (prim_index, g_primitive) in g_mesh.primitives().enumerate() { + if g_primitive.mode() != gltf::mesh::Mode::Triangles { + log::warn!( + "Skipping primitive '{}'[{}] for having mesh mode {:?}", + name, + prim_index, + g_primitive.mode() + ); + continue; + } + + let reader = g_primitive.reader(|buffer| Some(&data_buffers[buffer.index()])); + + // Read the vertices into memory + profiling::scope!("Read data"); + let base_vertex = self.points.len() as u32; + + match reader.read_indices() { + Some(read) => { + let mut read_u32 = read.into_u32(); + let tri_count = read_u32.len() / 3; + for _ in 0..tri_count { + let mut tri = [0u32; 3]; + for index in tri.iter_mut() { + *index = base_vertex + read_u32.next().unwrap(); + } + self.triangles.push(tri); + } + } + None => { + log::warn!("Missing index buffer for '{name}'"); + continue; + } + } + + for pos in reader.read_positions().unwrap() { + let point = transform.transform_point(&pos.into()); + self.points.push(point); + } + } + } +} + +pub fn load(path: &str) -> TriMesh { + use base64::engine::{general_purpose::URL_SAFE as ENCODING_ENGINE, Engine as _}; + + let gltf::Gltf { document, mut blob } = gltf::Gltf::open(path).unwrap(); + // extract buffers + let mut data_buffers = Vec::new(); + for buffer in document.buffers() { + let mut data = match buffer.source() { + gltf::buffer::Source::Uri(uri) => { + if let Some(rest) = uri.strip_prefix("data:") { + let (_before, after) = rest.split_once(";base64,").unwrap(); + ENCODING_ENGINE.decode(after).unwrap() + } else { + unimplemented!("Unexpected reference to external file: {uri}"); + } + } + gltf::buffer::Source::Bin => blob.take().unwrap(), + }; + assert!(data.len() >= buffer.length()); + while data.len() % 4 != 0 { + data.push(0); + } + data_buffers.push(data); + } + + let scene = document.scenes().next().expect("Document has no scenes?"); + let mut trimesh = TriMesh::default(); + for g_node in scene.nodes() { + trimesh.populate_from_gltf(g_node, nalgebra::Matrix4::identity(), &data_buffers); + } + trimesh +}