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;