diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c8a8aca7..f9b3a31f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ### Added +- Added support for parry’s new `Voxels` collider shape with `ColliderBuilder::voxels`, + `ColliderBuilder::voxels_from_points`, and `ColliderBuilder::voxelized_mesh`. - `MeshConverter` now implements `Copy`. ## v0.24.0 (10 April 2025) diff --git a/crates/rapier2d-f64/Cargo.toml b/crates/rapier2d-f64/Cargo.toml index 0ad3cfc23..358996c19 100644 --- a/crates/rapier2d-f64/Cargo.toml +++ b/crates/rapier2d-f64/Cargo.toml @@ -68,7 +68,7 @@ vec_map = { version = "0.8", optional = true } web-time = { version = "1.1", optional = true } num-traits = "0.2" nalgebra = "0.33" -parry2d-f64 = "0.19.0" +parry2d-f64 = "0.20.0" simba = "0.9" approx = "0.5" rayon = { version = "1", optional = true } diff --git a/crates/rapier2d/Cargo.toml b/crates/rapier2d/Cargo.toml index 3036331b6..da7a1c3a5 100644 --- a/crates/rapier2d/Cargo.toml +++ b/crates/rapier2d/Cargo.toml @@ -69,7 +69,7 @@ vec_map = { version = "0.8", optional = true } web-time = { version = "1.1", optional = true } num-traits = "0.2" nalgebra = "0.33" -parry2d = "0.19.0" +parry2d = "0.20.0" simba = "0.9" approx = "0.5" rayon = { version = "1", optional = true } diff --git a/crates/rapier3d-f64/Cargo.toml b/crates/rapier3d-f64/Cargo.toml index cbc0b6318..4931c6d93 100644 --- a/crates/rapier3d-f64/Cargo.toml +++ b/crates/rapier3d-f64/Cargo.toml @@ -71,7 +71,7 @@ vec_map = { version = "0.8", optional = true } web-time = { version = "1.1", optional = true } num-traits = "0.2" nalgebra = "0.33" -parry3d-f64 = "0.19.0" +parry3d-f64 = "0.20.0" simba = "0.9" approx = "0.5" rayon = { version = "1", optional = true } diff --git a/crates/rapier3d/Cargo.toml b/crates/rapier3d/Cargo.toml index 92f677b86..979ea1bb2 100644 --- a/crates/rapier3d/Cargo.toml +++ b/crates/rapier3d/Cargo.toml @@ -73,7 +73,7 @@ vec_map = { version = "0.8", optional = true } web-time = { version = "1.1", optional = true } num-traits = "0.2" nalgebra = "0.33" -parry3d = "0.19.0" +parry3d = "0.20.0" simba = "0.9" approx = "0.5" rayon = { version = "1", optional = true } diff --git a/crates/rapier_testbed2d-f64/Cargo.toml b/crates/rapier_testbed2d-f64/Cargo.toml index e2c17ab5d..5955c864f 100644 --- a/crates/rapier_testbed2d-f64/Cargo.toml +++ b/crates/rapier_testbed2d-f64/Cargo.toml @@ -63,7 +63,7 @@ profiling = "1.0" puffin_egui = { version = "0.29", optional = true } serde_json = "1" serde = { version = "1.0.215", features = ["derive"] } - +indexmap = "2" # Dependencies for native only. [target.'cfg(not(target_arch = "wasm32"))'.dependencies] diff --git a/crates/rapier_testbed2d/Cargo.toml b/crates/rapier_testbed2d/Cargo.toml index b96ab22af..e41c8394b 100644 --- a/crates/rapier_testbed2d/Cargo.toml +++ b/crates/rapier_testbed2d/Cargo.toml @@ -63,6 +63,7 @@ profiling = "1.0" puffin_egui = { version = "0.29", optional = true } serde = { version = "1.0.215", features = ["derive"] } serde_json = "1" +indexmap = "2" # Dependencies for native only. [target.'cfg(not(target_arch = "wasm32"))'.dependencies] diff --git a/crates/rapier_testbed3d-f64/Cargo.toml b/crates/rapier_testbed3d-f64/Cargo.toml index 7095020b1..4fc16dc3c 100644 --- a/crates/rapier_testbed3d-f64/Cargo.toml +++ b/crates/rapier_testbed3d-f64/Cargo.toml @@ -64,6 +64,7 @@ bevy_pbr = "0.15" bevy_sprite = "0.15" profiling = "1.0" puffin_egui = { version = "0.29", optional = true, git = "https://github.com/Vrixyz/puffin.git", branch = "expose_ui_options" } +indexmap = "2" # Dependencies for native only. [target.'cfg(not(target_arch = "wasm32"))'.dependencies] diff --git a/crates/rapier_testbed3d/Cargo.toml b/crates/rapier_testbed3d/Cargo.toml index 1bb340996..1fb27f880 100644 --- a/crates/rapier_testbed3d/Cargo.toml +++ b/crates/rapier_testbed3d/Cargo.toml @@ -65,6 +65,7 @@ bevy_pbr = "0.15" bevy_sprite = "0.15" profiling = "1.0" puffin_egui = { version = "0.29", optional = true } +indexmap = "2" # Dependencies for native only. [target.'cfg(not(target_arch = "wasm32"))'.dependencies] diff --git a/examples2d/Cargo.toml b/examples2d/Cargo.toml index 25e339a30..cc27f467d 100644 --- a/examples2d/Cargo.toml +++ b/examples2d/Cargo.toml @@ -16,6 +16,7 @@ enhanced-determinism = ["rapier2d/enhanced-determinism"] rand = "0.8" lyon = "0.17" usvg = "0.14" +dot_vox = "5" [dependencies.rapier_testbed2d] path = "../crates/rapier_testbed2d" diff --git a/examples2d/all_examples2.rs b/examples2d/all_examples2.rs index 0bb84edcd..2268a5933 100644 --- a/examples2d/all_examples2.rs +++ b/examples2d/all_examples2.rs @@ -1,4 +1,5 @@ #![allow(dead_code)] +#![allow(clippy::type_complexity)] #[cfg(target_arch = "wasm32")] use wasm_bindgen::prelude::*; @@ -42,6 +43,7 @@ mod s2d_joint_grid; mod s2d_pyramid; mod sensor2; mod trimesh2; +mod voxels2; mod utils; @@ -67,6 +69,7 @@ pub fn main() { ("Rope Joints", rope_joints2::init_world), ("Sensor", sensor2::init_world), ("Trimesh", trimesh2::init_world), + ("Voxels", voxels2::init_world), ("Joint motor position", joint_motor_position2::init_world), ("(Debug) box ball", debug_box_ball2::init_world), ("(Debug) compression", debug_compression2::init_world), diff --git a/examples2d/trimesh2.rs b/examples2d/trimesh2.rs index 1e4084ae7..9d2bac996 100644 --- a/examples2d/trimesh2.rs +++ b/examples2d/trimesh2.rs @@ -1,12 +1,6 @@ use rapier2d::prelude::*; use rapier_testbed2d::Testbed; -use lyon::math::Point; -use lyon::path::PathEvent; -use lyon::tessellation::geometry_builder::*; -use lyon::tessellation::{self, FillOptions, FillTessellator}; -use usvg::prelude::*; - pub fn init_world(testbed: &mut Testbed) { /* * World @@ -43,59 +37,17 @@ pub fn init_world(testbed: &mut Testbed) { /* * Create the trimeshes from a tessellated SVG. */ - let mut fill_tess = FillTessellator::new(); - let opt = usvg::Options::default(); - let rtree = usvg::Tree::from_str(RAPIER_SVG_STR, &opt).unwrap(); - let mut ith = 0; - - for node in rtree.root().descendants() { - if let usvg::NodeKind::Path(ref p) = *node.borrow() { - let transform = node.transform(); - if p.fill.is_some() { - let path = PathConvIter { - iter: p.data.iter(), - first: Point::new(0.0, 0.0), - prev: Point::new(0.0, 0.0), - deferred: None, - needs_end: false, - }; - - let mut mesh: VertexBuffers<_, u32> = VertexBuffers::new(); - fill_tess - .tessellate( - path, - &FillOptions::tolerance(0.01), - &mut BuffersBuilder::new(&mut mesh, VertexCtor { prim_id: 0 }), - ) - .expect("Tessellation failed."); - - let angle = transform.get_rotate() as f32; - - let (sx, sy) = ( - transform.get_scale().0 as f32 * 0.2, - transform.get_scale().1 as f32 * 0.2, - ); - - let indices: Vec<_> = mesh.indices.chunks(3).map(|v| [v[0], v[1], v[2]]).collect(); - let vertices: Vec<_> = mesh - .vertices - .iter() - .map(|v| point![v.position[0] * sx, v.position[1] * -sy]) - .collect(); - - for k in 0..5 { - let collider = ColliderBuilder::trimesh(vertices.clone(), indices.clone()) - .unwrap() - .contact_skin(0.2); - let rigid_body = RigidBodyBuilder::dynamic() - .translation(vector![ith as f32 * 8.0 - 20.0, 20.0 + k as f32 * 11.0]) - .rotation(angle); - let handle = bodies.insert(rigid_body); - colliders.insert_with_parent(collider, handle, &mut bodies); - } - - ith += 1; - } + let rapier_logo_buffers = crate::utils::svg::rapier_logo(); + + for (ith, (vtx, idx)) in rapier_logo_buffers.into_iter().enumerate() { + for k in 0..5 { + let collider = ColliderBuilder::trimesh(vtx.clone(), idx.clone()) + .unwrap() + .contact_skin(0.2); + let rigid_body = RigidBodyBuilder::dynamic() + .translation(vector![ith as f32 * 8.0 - 20.0, 20.0 + k as f32 * 11.0]); + let handle = bodies.insert(rigid_body); + colliders.insert_with_parent(collider, handle, &mut bodies); } } @@ -105,151 +57,3 @@ pub fn init_world(testbed: &mut Testbed) { testbed.set_world(bodies, colliders, impulse_joints, multibody_joints); testbed.look_at(point![0.0, 20.0], 17.0); } - -const RAPIER_SVG_STR: &str = r#" - - - - - - - - - - - - - - - - - - - - - - - - - - - -"#; - -pub struct PathConvIter<'a> { - iter: std::slice::Iter<'a, usvg::PathSegment>, - prev: Point, - first: Point, - needs_end: bool, - deferred: Option, -} - -impl Iterator for PathConvIter<'_> { - type Item = PathEvent; - fn next(&mut self) -> Option { - if self.deferred.is_some() { - return self.deferred.take(); - } - - let next = self.iter.next(); - match next { - Some(usvg::PathSegment::MoveTo { x, y }) => { - if self.needs_end { - let last = self.prev; - let first = self.first; - self.needs_end = false; - self.prev = Point::new(*x as f32, *y as f32); - self.deferred = Some(PathEvent::Begin { at: self.prev }); - self.first = self.prev; - Some(PathEvent::End { - last, - first, - close: false, - }) - } else { - self.first = Point::new(*x as f32, *y as f32); - Some(PathEvent::Begin { at: self.first }) - } - } - Some(usvg::PathSegment::LineTo { x, y }) => { - self.needs_end = true; - let from = self.prev; - self.prev = Point::new(*x as f32, *y as f32); - Some(PathEvent::Line { - from, - to: self.prev, - }) - } - Some(usvg::PathSegment::CurveTo { - x1, - y1, - x2, - y2, - x, - y, - }) => { - self.needs_end = true; - let from = self.prev; - self.prev = Point::new(*x as f32, *y as f32); - Some(PathEvent::Cubic { - from, - ctrl1: Point::new(*x1 as f32, *y1 as f32), - ctrl2: Point::new(*x2 as f32, *y2 as f32), - to: self.prev, - }) - } - Some(usvg::PathSegment::ClosePath) => { - self.needs_end = false; - self.prev = self.first; - Some(PathEvent::End { - last: self.prev, - first: self.first, - close: true, - }) - } - None => { - if self.needs_end { - self.needs_end = false; - let last = self.prev; - let first = self.first; - Some(PathEvent::End { - last, - first, - close: false, - }) - } else { - None - } - } - } - } -} - -pub struct VertexCtor { - pub prim_id: u32, -} - -impl FillVertexConstructor for VertexCtor { - fn new_vertex(&mut self, vertex: tessellation::FillVertex) -> GpuVertex { - GpuVertex { - position: vertex.position().to_array(), - prim_id: self.prim_id, - } - } -} - -impl StrokeVertexConstructor for VertexCtor { - fn new_vertex(&mut self, vertex: tessellation::StrokeVertex) -> GpuVertex { - GpuVertex { - position: vertex.position().to_array(), - prim_id: self.prim_id, - } - } -} - -#[repr(C)] -#[derive(Copy, Clone)] -pub struct GpuVertex { - pub position: [f32; 2], - pub prim_id: u32, -} diff --git a/examples2d/utils/mod.rs b/examples2d/utils/mod.rs index 788912266..f9536d6a7 100644 --- a/examples2d/utils/mod.rs +++ b/examples2d/utils/mod.rs @@ -1 +1,2 @@ pub mod character; +pub mod svg; diff --git a/examples2d/utils/svg.rs b/examples2d/utils/svg.rs new file mode 100644 index 000000000..9306d29b5 --- /dev/null +++ b/examples2d/utils/svg.rs @@ -0,0 +1,211 @@ +use rapier2d::prelude::*; + +use lyon::math::Point; +use lyon::path::PathEvent; +use lyon::tessellation::geometry_builder::*; +use lyon::tessellation::{self, FillOptions, FillTessellator}; +use rapier2d::na::{Point2, Rotation2}; +use usvg::prelude::*; + +const RAPIER_SVG_STR: &str = r#" + + + + + + + + + + + + + + + + + + + + + + + + + + + +"#; + +pub fn rapier_logo() -> Vec<(Vec>, Vec<[u32; 3]>)> { + tessellate_svg_str(RAPIER_SVG_STR) +} + +pub fn tessellate_svg_str(svg_str: &str) -> Vec<(Vec>, Vec<[u32; 3]>)> { + let mut result = vec![]; + let mut fill_tess = FillTessellator::new(); + let opt = usvg::Options::default(); + let rtree = usvg::Tree::from_str(svg_str, &opt).unwrap(); + + for node in rtree.root().descendants() { + if let usvg::NodeKind::Path(ref p) = *node.borrow() { + let transform = node.transform(); + if p.fill.is_some() { + let path = PathConvIter { + iter: p.data.iter(), + first: Point::new(0.0, 0.0), + prev: Point::new(0.0, 0.0), + deferred: None, + needs_end: false, + }; + + let mut mesh: VertexBuffers<_, u32> = VertexBuffers::new(); + fill_tess + .tessellate( + path, + &FillOptions::tolerance(0.01), + &mut BuffersBuilder::new(&mut mesh, VertexCtor { prim_id: 0 }), + ) + .expect("Tessellation failed."); + + let angle = transform.get_rotate() as f32; + + let (sx, sy) = ( + transform.get_scale().0 as f32 * 0.2, + transform.get_scale().1 as f32 * 0.2, + ); + + let indices: Vec<_> = mesh.indices.chunks(3).map(|v| [v[0], v[1], v[2]]).collect(); + let vertices: Vec<_> = mesh + .vertices + .iter() + .map(|v| { + Rotation2::new(angle) * point![v.position[0] * sx, v.position[1] * -sy] + }) + .collect(); + + result.push((vertices, indices)); + } + } + } + + result +} + +struct PathConvIter<'a> { + iter: std::slice::Iter<'a, usvg::PathSegment>, + prev: Point, + first: Point, + needs_end: bool, + deferred: Option, +} + +impl Iterator for PathConvIter<'_> { + type Item = PathEvent; + fn next(&mut self) -> Option { + if self.deferred.is_some() { + return self.deferred.take(); + } + + let next = self.iter.next(); + match next { + Some(usvg::PathSegment::MoveTo { x, y }) => { + if self.needs_end { + let last = self.prev; + let first = self.first; + self.needs_end = false; + self.prev = Point::new(*x as f32, *y as f32); + self.deferred = Some(PathEvent::Begin { at: self.prev }); + self.first = self.prev; + Some(PathEvent::End { + last, + first, + close: false, + }) + } else { + self.first = Point::new(*x as f32, *y as f32); + Some(PathEvent::Begin { at: self.first }) + } + } + Some(usvg::PathSegment::LineTo { x, y }) => { + self.needs_end = true; + let from = self.prev; + self.prev = Point::new(*x as f32, *y as f32); + Some(PathEvent::Line { + from, + to: self.prev, + }) + } + Some(usvg::PathSegment::CurveTo { + x1, + y1, + x2, + y2, + x, + y, + }) => { + self.needs_end = true; + let from = self.prev; + self.prev = Point::new(*x as f32, *y as f32); + Some(PathEvent::Cubic { + from, + ctrl1: Point::new(*x1 as f32, *y1 as f32), + ctrl2: Point::new(*x2 as f32, *y2 as f32), + to: self.prev, + }) + } + Some(usvg::PathSegment::ClosePath) => { + self.needs_end = false; + self.prev = self.first; + Some(PathEvent::End { + last: self.prev, + first: self.first, + close: true, + }) + } + None => { + if self.needs_end { + self.needs_end = false; + let last = self.prev; + let first = self.first; + Some(PathEvent::End { + last, + first, + close: false, + }) + } else { + None + } + } + } + } +} + +struct VertexCtor { + prim_id: u32, +} + +impl FillVertexConstructor for VertexCtor { + fn new_vertex(&mut self, vertex: tessellation::FillVertex) -> GpuVertex { + GpuVertex { + position: vertex.position().to_array(), + prim_id: self.prim_id, + } + } +} + +impl StrokeVertexConstructor for VertexCtor { + fn new_vertex(&mut self, vertex: tessellation::StrokeVertex) -> GpuVertex { + GpuVertex { + position: vertex.position().to_array(), + prim_id: self.prim_id, + } + } +} + +#[repr(C)] +#[derive(Copy, Clone)] +struct GpuVertex { + position: [f32; 2], + prim_id: u32, +} diff --git a/examples2d/voxels2.rs b/examples2d/voxels2.rs new file mode 100644 index 000000000..7c3ebadf7 --- /dev/null +++ b/examples2d/voxels2.rs @@ -0,0 +1,122 @@ +use rapier2d::parry::transformation::voxelization::FillMode; +use rapier2d::prelude::*; +use rapier_testbed2d::Testbed; + +const VOXEL_SIZE: Real = 0.1; // 0.25; + +pub fn init_world(testbed: &mut Testbed) { + /* + * Voxel geometry type selection. + */ + // TODO: make the testbed support custom enums (or at least a list of option from strings and + // associated constants). + let settings = testbed.example_settings_mut(); + let geometry_mode = settings.get_or_set_string( + "Voxels mode", + 0, + vec!["PseudoCube".to_string(), "PseudoBall".to_string()], + ); + let falling_objects = settings.get_or_set_string( + "Falling objects", + 3, // Defaults to Mixed. + vec![ + "Ball".to_string(), + "Cuboid".to_string(), + "Capsule".to_string(), + "Mixed".to_string(), + ], + ); + let voxel_size_y = settings.get_or_set_f32("Voxel size y", 1.0, 0.5..=2.0); + let voxel_size = Vector::new(1.0, voxel_size_y); + + let primitive_geometry = if geometry_mode == 0 { + VoxelPrimitiveGeometry::PseudoCube + } else { + VoxelPrimitiveGeometry::PseudoBall + }; + + /* + * World + */ + let mut bodies = RigidBodySet::new(); + let mut colliders = ColliderSet::new(); + let impulse_joints = ImpulseJointSet::new(); + let multibody_joints = MultibodyJointSet::new(); + + /* + * Create dynamic objects to fall on voxels. + */ + let nx = 50; + for i in 0..nx { + for j in 0..10 { + let rb = RigidBodyBuilder::dynamic().translation(vector![ + i as f32 * 2.0 - nx as f32 / 2.0, + 20.0 + j as f32 * 2.0 + ]); + let rb_handle = bodies.insert(rb); + + let falling_objects = if falling_objects == 3 { + j % 3 + } else { + falling_objects + }; + + let ball_radius = 0.5; + let co = match falling_objects { + 0 => ColliderBuilder::ball(ball_radius), + 1 => ColliderBuilder::cuboid(ball_radius, ball_radius), + 2 => ColliderBuilder::capsule_y(ball_radius, ball_radius), + _ => unreachable!(), + }; + colliders.insert_with_parent(co, rb_handle, &mut bodies); + } + } + + /* + * Voxelization. + */ + let polyline = vec![ + point![0.0, 0.0], + point![0.0, 10.0], + point![7.0, 4.0], + point![14.0, 10.0], + point![14.0, 0.0], + point![13.0, 7.0], + point![7.0, 2.0], + point![1.0, 7.0], + ]; + let indices: Vec<_> = (0..polyline.len() as u32) + .map(|i| [i, (i + 1) % polyline.len() as u32]) + .collect(); + let rb = bodies.insert(RigidBodyBuilder::fixed().translation(vector![-20.0, -10.0])); + let shape = SharedShape::voxelized_mesh( + primitive_geometry, + &polyline, + &indices, + 0.2, + FillMode::default(), + ); + + colliders.insert_with_parent(ColliderBuilder::new(shape), rb, &mut bodies); + + /* + * A voxel wavy floor. + */ + let voxels: Vec<_> = (0..300) + .map(|i| { + let y = (i as f32 / 20.0).sin().clamp(-0.5, 0.5) * 20.0; + point![(i as f32 - 125.0) * voxel_size.x / 2.0, y * voxel_size.y] + }) + .collect(); + colliders.insert(ColliderBuilder::voxels_from_points( + primitive_geometry, + voxel_size, + &voxels, + )); + + /* + * Set up the testbed. + */ + testbed.set_world(bodies, colliders, impulse_joints, multibody_joints); + testbed.look_at(point![0.0, 20.0], 17.0); +} diff --git a/examples3d/Cargo.toml b/examples3d/Cargo.toml index 223ab2167..c2ddc0af2 100644 --- a/examples3d/Cargo.toml +++ b/examples3d/Cargo.toml @@ -19,6 +19,8 @@ wasm-bindgen = "0.2" obj-rs = { version = "0.7", default-features = false } serde = "1" bincode = "1" +serde_json = "1" +dot_vox = "5" [dependencies.rapier_testbed3d] path = "../crates/rapier_testbed3d" diff --git a/examples3d/all_examples3.rs b/examples3d/all_examples3.rs index 56451a61f..1a6909a50 100644 --- a/examples3d/all_examples3.rs +++ b/examples3d/all_examples3.rs @@ -1,4 +1,5 @@ #![allow(dead_code)] +#![allow(clippy::type_complexity)] #[cfg(target_arch = "wasm32")] use wasm_bindgen::prelude::*; @@ -58,6 +59,7 @@ mod trimesh3; mod urdf3; mod vehicle_controller3; mod vehicle_joints3; +mod voxels3; #[cfg_attr(target_arch = "wasm32", wasm_bindgen(start))] pub fn main() { @@ -87,6 +89,7 @@ pub fn main() { ("Spring Joints", spring_joints3::init_world), ("TriMesh", trimesh3::init_world), ("Urdf", urdf3::init_world), + ("Voxels", voxels3::init_world), ("Vehicle controller", vehicle_controller3::init_world), ("Vehicle joints", vehicle_joints3::init_world), ("Keva tower", keva3::init_world), diff --git a/examples3d/voxels3.rs b/examples3d/voxels3.rs new file mode 100644 index 000000000..c3f939db0 --- /dev/null +++ b/examples3d/voxels3.rs @@ -0,0 +1,324 @@ +use obj::raw::object::Polygon; +use rapier3d::parry::bounding_volume; +use rapier3d::parry::transformation::voxelization::FillMode; +use rapier3d::prelude::*; +use rapier_testbed3d::KeyCode; +use rapier_testbed3d::Testbed; +use std::fs::File; +use std::io::BufReader; + +pub fn init_world(testbed: &mut Testbed) { + /* + * Voxel geometry type selection. + */ + + let settings = testbed.example_settings_mut(); + + let geometry_mode = settings.get_or_set_string( + "Voxels mode", + 0, + vec!["PseudoCube".to_string(), "PseudoBall".to_string()], + ); + let falling_objects = settings.get_or_set_string( + "Falling objects", + 5, // Defaults to Mixed. + vec![ + "Ball".to_string(), + "Cuboid".to_string(), + "Cylinder".to_string(), + "Cone".to_string(), + "Capsule".to_string(), + "Mixed".to_string(), + ], + ); + + let voxel_size_y = settings.get_or_set_f32("Voxel size y", 1.0, 0.5..=2.0); + let voxel_size = Vector::new(1.0, voxel_size_y, 1.0); + + // TODO: give a better placement to the objs. + // settings.get_or_set_bool("Load .obj", false); + let load_obj = false; + + let primitive_geometry = if geometry_mode == 0 { + VoxelPrimitiveGeometry::PseudoCube + } else { + VoxelPrimitiveGeometry::PseudoBall + }; + + /* + * World + */ + let mut bodies = RigidBodySet::new(); + let mut colliders = ColliderSet::new(); + let impulse_joints = ImpulseJointSet::new(); + let multibody_joints = MultibodyJointSet::new(); + + /* + * Create a bowl for the ground + */ + /* + * Create the convex decompositions. + */ + if load_obj { + let geoms = models(); + let ngeoms = geoms.len(); + let width = (ngeoms as f32).sqrt() as usize; + let num_duplications = 1; // 4; + let shift = 7.0f32; + + for (igeom, obj_path) in geoms.into_iter().enumerate() { + let deltas = Isometry::identity(); + + let mut shapes = Vec::new(); + println!("Parsing and decomposing: {}", obj_path); + + let input = BufReader::new(File::open(obj_path).unwrap()); + + if let Ok(model) = obj::raw::parse_obj(input) { + let mut vertices: Vec<_> = model + .positions + .iter() + .map(|v| point![v.0, v.1, v.2]) + .collect(); + let indices: Vec<_> = model + .polygons + .into_iter() + .flat_map(|p| match p { + Polygon::P(idx) => idx.into_iter(), + Polygon::PT(idx) => { + Vec::from_iter(idx.into_iter().map(|i| i.0)).into_iter() + } + Polygon::PN(idx) => { + Vec::from_iter(idx.into_iter().map(|i| i.0)).into_iter() + } + Polygon::PTN(idx) => { + Vec::from_iter(idx.into_iter().map(|i| i.0)).into_iter() + } + }) + .collect(); + + // Compute the size of the model, to scale it and have similar size for everything. + let aabb = bounding_volume::details::point_cloud_aabb(&deltas, &vertices); + let center = aabb.center(); + let diag = (aabb.maxs - aabb.mins).norm(); + + vertices + .iter_mut() + .for_each(|p| *p = (*p - center.coords) * 6.0 / diag); + + let indices: Vec<_> = indices + .chunks(3) + .map(|idx| [idx[0] as u32, idx[1] as u32, idx[2] as u32]) + .collect(); + + let decomposed_shape = SharedShape::voxelized_mesh( + primitive_geometry, + &vertices, + &indices, + 0.1, + FillMode::default(), + ); + + shapes.push(decomposed_shape); + + for k in 1..num_duplications + 1 { + let x = (igeom % width) as f32 * shift - 3.0; + let y = (igeom / width) as f32 * shift + 4.0; + let z = k as f32 * shift - 3.0; + + let body = RigidBodyBuilder::fixed().translation(vector![x, y, z]); + let handle = bodies.insert(body); + + for shape in &shapes { + let collider = ColliderBuilder::new(shape.clone()); + colliders.insert_with_parent(collider, handle, &mut bodies); + } + } + } + } + } + + /* + * Create a voxelized wavy floor. + */ + let mut samples = vec![]; + let n = 200; + for i in 0..n { + for j in 0..n { + let y = (i as f32 / n as f32 * 10.0).sin().clamp(-0.8, 0.8) + * (j as f32 / n as f32 * 10.0).cos().clamp(-0.8, 0.8) + * 16.0; + + samples.push(point![i as f32, y * voxel_size_y, j as f32]); + + if i == 0 || i == n - 1 || j == 0 || j == n - 1 { + // Create walls so the object at the edge don’t fall into the infinite void. + for k in 0..4 { + samples.push(point![i as f32, (y + k as f32) * voxel_size_y, j as f32]); + } + } + } + } + let collider = + ColliderBuilder::voxels_from_points(primitive_geometry, voxel_size, &samples).build(); + let floor_aabb = collider.compute_aabb(); + colliders.insert(collider); + + /* + * Some dynamic primitives. + */ + let nik = 30; + let extents = floor_aabb.extents() * 0.75; + let margin = (floor_aabb.extents() - extents) / 2.0; + let ball_radius = 0.5; + for i in 0..nik { + for j in 0..5 { + for k in 0..nik { + let rb = RigidBodyBuilder::dynamic().translation(vector![ + floor_aabb.mins.x + margin.x + i as f32 * extents.x / nik as f32, + floor_aabb.maxs.y + j as f32 * 2.0, + floor_aabb.mins.z + margin.z + k as f32 * extents.z / nik as f32, + ]); + let rb_handle = bodies.insert(rb); + + let falling_objects = if falling_objects == 5 { + j % 5 + } else { + falling_objects + }; + + let co = match falling_objects { + 0 => ColliderBuilder::ball(ball_radius), + 1 => ColliderBuilder::cuboid(ball_radius, ball_radius, ball_radius), + 2 => ColliderBuilder::cylinder(ball_radius, ball_radius), + 3 => ColliderBuilder::cone(ball_radius, ball_radius), + 4 => ColliderBuilder::capsule_y(ball_radius, ball_radius), + _ => unreachable!(), + }; + colliders.insert_with_parent(co, rb_handle, &mut bodies); + } + } + } + + // Add callback for handling voxels edition, and highlighting the voxel + // pointed at by the mouse. We spawn two fake colliders that don’t interact + // with anything. They are used as gizmos to indicate where the ray hits on voxels + // by highlighting the voxel and drawing a small ball at the intersection. + let hit_indicator_handle = + colliders.insert(ColliderBuilder::ball(0.1).collision_groups(InteractionGroups::none())); + let hit_highlight_handle = colliders.insert( + ColliderBuilder::cuboid(0.51, 0.51, 0.51).collision_groups(InteractionGroups::none()), + ); + testbed.set_initial_collider_color(hit_indicator_handle, [0.5, 0.5, 0.1]); + testbed.set_initial_collider_color(hit_highlight_handle, [0.1, 0.5, 0.1]); + + testbed.add_callback(move |graphics, physics, _, _| { + let Some(graphics) = graphics else { return }; + let Some((mouse_orig, mouse_dir)) = graphics.mouse().ray else { + return; + }; + + let ray = Ray::new(mouse_orig, mouse_dir); + let filter = QueryFilter { + predicate: Some(&|_, co: &Collider| co.shape().as_voxels().is_some()), + ..Default::default() + }; + if let Some((handle, hit)) = physics.query_pipeline.cast_ray_and_get_normal( + &physics.bodies, + &physics.colliders, + &ray, + Real::MAX, + true, + filter, + ) { + // Highlight the voxel. + let hit_collider = &physics.colliders[handle]; + let hit_local_normal = hit_collider + .position() + .inverse_transform_vector(&hit.normal); + let voxels = hit_collider.shape().as_voxels().unwrap(); + let FeatureId::Face(id) = hit.feature else { + unreachable!() + }; + let voxel_key = voxels.voxel_key_at(id); + let voxel_center = hit_collider.position() * voxels.voxel_center(voxel_key); + let voxel_size = voxels.voxel_size(); + let hit_highlight = physics.colliders.get_mut(hit_highlight_handle).unwrap(); + hit_highlight.set_translation(voxel_center.coords); + hit_highlight + .shape_mut() + .as_cuboid_mut() + .unwrap() + .half_extents = voxel_size / 2.0 + Vector::repeat(0.001); + graphics.update_collider(hit_highlight_handle, &physics.colliders); + + // Show the hit point. + let hit_pt = ray.point_at(hit.time_of_impact); + let hit_indicator = physics.colliders.get_mut(hit_indicator_handle).unwrap(); + hit_indicator.set_translation(hit_pt.coords); + hit_indicator.shape_mut().as_ball_mut().unwrap().radius = voxel_size.norm() / 3.5; + graphics.update_collider(hit_indicator_handle, &physics.colliders); + + // If a relevant key was pressed, edit the shape. + if graphics.keys().pressed(KeyCode::Space) { + let removal_mode = graphics.keys().pressed(KeyCode::ShiftLeft); + let voxels = physics + .colliders + .get_mut(handle) + .unwrap() + .shape_mut() + .as_voxels_mut() + .unwrap(); + let mut affected_key = voxel_key; + + if !removal_mode { + let imax = hit_local_normal.iamax(); + if hit_local_normal[imax] >= 0.0 { + affected_key[imax] += 1; + } else { + affected_key[imax] -= 1; + } + } + + voxels.insert_voxel_at_key(affected_key, !removal_mode); + graphics.update_collider(handle, &physics.colliders); + } + } else { + // When there is no hit, move the indicators behind the camera. + let behind_camera = mouse_orig - mouse_dir * 1000.0; + let hit_indicator = physics.colliders.get_mut(hit_indicator_handle).unwrap(); + hit_indicator.set_translation(behind_camera.coords); + let hit_highlight = physics.colliders.get_mut(hit_highlight_handle).unwrap(); + hit_highlight.set_translation(behind_camera.coords); + } + }); + + /* + * Set up the testbed. + */ + testbed.set_world(bodies, colliders, impulse_joints, multibody_joints); + testbed.look_at(point![100.0, 100.0, 100.0], Point::origin()); +} + +fn models() -> Vec { + vec![ + // "assets/3d/camel_decimated.obj".to_string(), + "assets/3d/chair.obj".to_string(), + "assets/3d/cup_decimated.obj".to_string(), + "assets/3d/dilo_decimated.obj".to_string(), + "assets/3d/feline_decimated.obj".to_string(), + "assets/3d/genus3_decimated.obj".to_string(), + "assets/3d/hand2_decimated.obj".to_string(), + "assets/3d/hand_decimated.obj".to_string(), + "assets/3d/hornbug.obj".to_string(), + "assets/3d/octopus_decimated.obj".to_string(), + "assets/3d/rabbit_decimated.obj".to_string(), + // "assets/3d/rust_logo.obj".to_string(), + "assets/3d/rust_logo_simplified.obj".to_string(), + "assets/3d/screwdriver_decimated.obj".to_string(), + "assets/3d/table.obj".to_string(), + "assets/3d/tstTorusModel.obj".to_string(), + // "assets/3d/tstTorusModel2.obj".to_string(), + // "assets/3d/tstTorusModel3.obj".to_string(), + ] +} diff --git a/src/geometry/collider.rs b/src/geometry/collider.rs index 4f5312f76..c6797fff0 100644 --- a/src/geometry/collider.rs +++ b/src/geometry/collider.rs @@ -1,4 +1,6 @@ use crate::dynamics::{CoefficientCombineRule, MassProperties, RigidBodyHandle}; +#[cfg(feature = "dim3")] +use crate::geometry::HeightFieldFlags; use crate::geometry::{ ActiveCollisionTypes, BroadPhaseProxyIndex, ColliderBroadPhaseData, ColliderChanges, ColliderFlags, ColliderMassProps, ColliderMaterial, ColliderParent, ColliderPosition, @@ -10,10 +12,8 @@ use crate::pipeline::{ActiveEvents, ActiveHooks}; use crate::prelude::ColliderEnabled; use na::Unit; use parry::bounding_volume::{Aabb, BoundingVolume}; -use parry::shape::{Shape, TriMeshBuilderError, TriMeshFlags}; - -#[cfg(feature = "dim3")] -use crate::geometry::HeightFieldFlags; +use parry::shape::{Shape, TriMeshBuilderError, TriMeshFlags, VoxelPrimitiveGeometry}; +use parry::transformation::voxelization::FillMode; #[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))] #[derive(Clone, Debug)] @@ -573,6 +573,55 @@ impl ColliderBuilder { Self::new(SharedShape::halfspace(outward_normal)) } + /// Initializes a shape made of voxels. + /// + /// Each voxel has the size `voxel_size` and grid coordinate given by `centers`. + /// The `primitive_geometry` controls the behavior of collision detection at voxels boundaries. + /// + /// For initializing a voxels shape from points in space, see [`Self::voxels_from_points`]. + /// For initializing a voxels shape from a mesh to voxelize, see [`Self::voxelized_mesh`]. + pub fn voxels( + primitive_geometry: VoxelPrimitiveGeometry, + voxel_size: Vector, + voxels: &[Point], + ) -> Self { + Self::new(SharedShape::voxels(primitive_geometry, voxel_size, voxels)) + } + + /// Initializes a collider made of voxels. + /// + /// Each voxel has the size `voxel_size` and contains at least one point from `centers`. + /// The `primitive_geometry` controls the behavior of collision detection at voxels boundaries. + pub fn voxels_from_points( + primitive_geometry: VoxelPrimitiveGeometry, + voxel_size: Vector, + points: &[Point], + ) -> Self { + Self::new(SharedShape::voxels_from_points( + primitive_geometry, + voxel_size, + points, + )) + } + + /// Initializes a voxels obtained from the decomposition of the given trimesh (in 3D) + /// or polyline (in 2D) into voxelized convex parts. + pub fn voxelized_mesh( + primitive_geometry: VoxelPrimitiveGeometry, + vertices: &[Point], + indices: &[[u32; DIM]], + voxel_size: Real, + fill_mode: FillMode, + ) -> Self { + Self::new(SharedShape::voxelized_mesh( + primitive_geometry, + vertices, + indices, + voxel_size, + fill_mode, + )) + } + /// Initialize a new collider builder with a cylindrical shape defined by its half-height /// (along the Y axis) and its radius. #[cfg(feature = "dim3")] diff --git a/src/geometry/mod.rs b/src/geometry/mod.rs index 1e94365a8..4fb9584a5 100644 --- a/src/geometry/mod.rs +++ b/src/geometry/mod.rs @@ -17,7 +17,7 @@ pub use self::narrow_phase::NarrowPhase; pub use parry::bounding_volume::BoundingVolume; pub use parry::query::{PointQuery, PointQueryWithLocation, RayCast, TrackedContact}; -pub use parry::shape::SharedShape; +pub use parry::shape::{SharedShape, VoxelPrimitiveGeometry, VoxelState, VoxelType, Voxels}; use crate::math::{Real, Vector}; diff --git a/src/pipeline/debug_render_pipeline/debug_render_pipeline.rs b/src/pipeline/debug_render_pipeline/debug_render_pipeline.rs index d12eee2ee..d22e8ffb0 100644 --- a/src/pipeline/debug_render_pipeline/debug_render_pipeline.rs +++ b/src/pipeline/debug_render_pipeline/debug_render_pipeline.rs @@ -459,6 +459,10 @@ impl DebugRenderPipeline { let vtx = s.to_polyline(self.style.border_subdivisions); backend.draw_line_strip(object, &vtx, pos, &Vector::repeat(1.0), color, true) } + TypedShape::Voxels(s) => { + let (vtx, idx) = s.to_polyline(); + backend.draw_polyline(object, &vtx, &idx, pos, &Vector::repeat(1.0), color) + } TypedShape::Custom(_) => {} } } @@ -613,6 +617,10 @@ impl DebugRenderPipeline { let (vtx, idx) = s.to_outline(self.style.border_subdivisions); backend.draw_polyline(object, &vtx, &idx, pos, &Vector::repeat(1.0), color) } + TypedShape::Voxels(s) => { + let (vtx, idx) = s.to_outline(); + backend.draw_polyline(object, &vtx, &idx, pos, &Vector::repeat(1.0), color) + } TypedShape::Custom(_) => {} } } diff --git a/src_testbed/camera2d.rs b/src_testbed/camera2d.rs index 78bff9c9c..312e52055 100644 --- a/src_testbed/camera2d.rs +++ b/src_testbed/camera2d.rs @@ -7,6 +7,9 @@ use bevy::input::mouse::MouseWheel; use bevy::prelude::*; use bevy::render::camera::Camera; +#[cfg(target_os = "macos")] +const LINE_TO_PIXEL_RATIO: f32 = 0.0005; +#[cfg(not(target_os = "macos"))] const LINE_TO_PIXEL_RATIO: f32 = 0.1; #[derive(Component, PartialEq, Debug, Clone, serde::Serialize, serde::Deserialize)] diff --git a/src_testbed/lib.rs b/src_testbed/lib.rs index 1bbc4654a..dcce50067 100644 --- a/src_testbed/lib.rs +++ b/src_testbed/lib.rs @@ -1,4 +1,5 @@ #![allow(clippy::too_many_arguments)] + extern crate nalgebra as na; pub use crate::graphics::{BevyMaterial, GraphicsManager}; diff --git a/src_testbed/objects/node.rs b/src_testbed/objects/node.rs index c19e9f7e9..e722d9f54 100644 --- a/src_testbed/objects/node.rs +++ b/src_testbed/objects/node.rs @@ -17,7 +17,7 @@ use rapier::math::{Isometry, Real, Vector}; use crate::graphics::{BevyMaterial, InstancedMaterials, SELECTED_OBJECT_MATERIAL_KEY}; #[cfg(feature = "dim2")] use { - na::{Point2, Vector2}, + na::{vector, Point2, Vector2}, rapier::geometry::{Ball, Cuboid}, }; @@ -439,6 +439,26 @@ fn generate_collider_mesh(co_shape: &dyn Shape) -> Option { .collect(); bevy_mesh((vertices, trimesh.indices().to_vec())) } + ShapeType::Voxels => { + let mut vtx = vec![]; + let mut idx = vec![]; + let voxels = co_shape.as_voxels().unwrap(); + let sz = voxels.voxel_size() / 2.0; + for (_, center, data) in voxels.centers() { + if !data.is_empty() { + let bid = vtx.len() as u32; + let center = point![center.x, center.y, 0.0]; + vtx.push(center + vector![sz.x, sz.y, 0.0]); + vtx.push(center + vector![-sz.x, sz.y, 0.0]); + vtx.push(center + vector![-sz.x, -sz.y, 0.0]); + vtx.push(center + vector![sz.x, -sz.y, 0.0]); + idx.push([bid, bid + 1, bid + 2]); + idx.push([bid + 2, bid + 3, bid]); + } + } + + bevy_mesh((vtx, idx)) + } ShapeType::Polyline => { let polyline = co_shape.as_polyline().unwrap(); bevy_polyline(( @@ -495,6 +515,10 @@ fn generate_collider_mesh(co_shape: &dyn Shape) -> Option { let poly = co_shape.as_round_convex_polyhedron().unwrap(); bevy_mesh(poly.inner_shape.to_trimesh()) } + ShapeType::Voxels => { + let voxels = co_shape.as_voxels().unwrap(); + bevy_mesh(voxels.to_trimesh()) + } _ => return None, }; diff --git a/src_testbed/settings.rs b/src_testbed/settings.rs index 1e26a0f61..29655a651 100644 --- a/src_testbed/settings.rs +++ b/src_testbed/settings.rs @@ -1,8 +1,9 @@ -use std::collections::HashMap; +use indexmap::IndexMap; use std::ops::RangeInclusive; #[derive(Clone, PartialEq, Debug, serde::Serialize, serde::Deserialize)] pub enum SettingValue { + Label(String), U32 { value: u32, range: RangeInclusive, @@ -11,11 +12,18 @@ pub enum SettingValue { value: f32, range: RangeInclusive, }, + Bool { + value: bool, + }, + String { + value: usize, + range: Vec, + }, } #[derive(Default, Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)] pub struct ExampleSettings { - values: HashMap, + values: IndexMap, } impl ExampleSettings { @@ -35,6 +43,27 @@ impl ExampleSettings { self.values.iter_mut() } + pub fn set_bool(&mut self, key: &str, value: bool) { + self.values + .insert(key.to_string(), SettingValue::Bool { value }); + } + + pub fn get_or_set_bool(&mut self, key: &'static str, default: bool) -> bool { + let to_insert = SettingValue::Bool { value: default }; + let entry = self + .values + .entry(key.to_string()) + .or_insert(to_insert.clone()); + match entry { + SettingValue::Bool { value } => *value, + _ => { + // The entry doesn’t have the right type. Overwrite with the new value. + *entry = to_insert; + default + } + } + } + pub fn set_u32(&mut self, key: &str, value: u32, range: RangeInclusive) { self.values .insert(key.to_string(), SettingValue::U32 { value, range }); @@ -90,6 +119,47 @@ impl ExampleSettings { } } + pub fn set_string(&mut self, key: &str, selected: usize, range: Vec) { + self.values.insert( + key.to_string(), + SettingValue::String { + value: selected, + range, + }, + ); + } + + pub fn get_or_set_string( + &mut self, + key: &'static str, + default: usize, + range: Vec, + ) -> usize { + let to_insert = SettingValue::String { + value: default, + range, + }; + let entry = self + .values + .entry(key.to_string()) + .or_insert(to_insert.clone()); + match entry { + SettingValue::String { value, .. } => *value, + _ => { + // The entry doesn’t have the right type. Overwrite with the new value. + *entry = to_insert; + default + } + } + } + + pub fn get_bool(&self, key: &'static str) -> Option { + match self.values.get(key)? { + SettingValue::Bool { value } => Some(*value), + _ => None, + } + } + pub fn get_u32(&self, key: &'static str) -> Option { match self.values.get(key)? { SettingValue::U32 { value, .. } => Some(*value), @@ -103,4 +173,23 @@ impl ExampleSettings { _ => None, } } + + pub fn get_string_id(&self, key: &'static str) -> Option { + match self.values.get(key)? { + SettingValue::String { value, .. } => Some(*value), + _ => None, + } + } + + pub fn get_string(&self, key: &'static str) -> Option<&str> { + match self.values.get(key)? { + SettingValue::String { value, range } => Some(&range[*value]), + _ => None, + } + } + + pub fn set_label(&mut self, key: &'static str, value: impl Into) { + self.values + .insert(key.to_string(), SettingValue::Label(value.into())); + } } diff --git a/src_testbed/testbed.rs b/src_testbed/testbed.rs index 45f494baa..a321d6ad9 100644 --- a/src_testbed/testbed.rs +++ b/src_testbed/testbed.rs @@ -1014,18 +1014,20 @@ fn draw_contacts(_nf: &NarrowPhase, _colliders: &ColliderSet) { #[cfg(feature = "dim3")] fn setup_graphics_environment(mut commands: Commands) { commands.insert_resource(AmbientLight { - brightness: 100.0, + brightness: 200.0, ..Default::default() }); commands.spawn(( DirectionalLight { - shadows_enabled: false, + // shadows_enabled: true, ..Default::default() }, Transform { - translation: Vec3::new(10.0, 2.0, 10.0), - rotation: Quat::from_rotation_x(-std::f32::consts::FRAC_PI_4), + translation: Vec3::new(-100.0, 200.0, -100.0), + rotation: Quat::from_rotation_x( + -(std::f32::consts::FRAC_PI_4 + std::f32::consts::FRAC_PI_6), + ), ..Default::default() }, )); diff --git a/src_testbed/ui.rs b/src_testbed/ui.rs index 54d4f5eba..94b850ce0 100644 --- a/src_testbed/ui.rs +++ b/src_testbed/ui.rs @@ -439,6 +439,9 @@ fn example_settings_ui(ui_context: &mut EguiContexts, state: &mut TestbedState) for (name, value) in state.example_settings.iter_mut() { let prev_value = value.clone(); match value { + SettingValue::Label(value) => { + ui.label(format!("{}: {}", name, value)); + } SettingValue::F32 { value, range } => { ui.add(Slider::new(value, range.clone()).text(name)); } @@ -454,6 +457,19 @@ fn example_settings_ui(ui_context: &mut EguiContexts, state: &mut TestbedState) ui.add(Slider::new(value, range.clone()).text(name)); }); } + SettingValue::Bool { value } => { + ui.checkbox(value, name); + } + SettingValue::String { value, range } => { + ComboBox::from_label(name) + .width(150.0) + .selected_text(&range[*value]) + .show_ui(ui, |ui| { + for (id, name) in range.iter().enumerate() { + ui.selectable_value(value, id, name); + } + }); + } } any_changed = any_changed || *value != prev_value;