feat: add support for Voxels collider (#823)

* feat: start adding voxels support and some additional testbed demo settings

* feat: add support for parry’s new Voxels collider shape

* fix voxels demos

* feat: support rectangular voxels and additional voxels initialization

* chore: switch to parry 0.20

* chore: fix cargo doc

* Fix testbed build
This commit is contained in:
Sébastien Crozet
2025-04-24 12:11:53 +02:00
committed by GitHub
parent 1c67c5e7f2
commit e44f636249
27 changed files with 891 additions and 223 deletions

View File

@@ -2,6 +2,8 @@
### Added
- Added support for parrys 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)

View File

@@ -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 }

View File

@@ -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 }

View File

@@ -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 }

View File

@@ -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 }

View File

@@ -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 = { version = "2", features = ["serde"] }
# Dependencies for native only.
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]

View File

@@ -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 = { version = "2", features = ["serde"] }
# Dependencies for native only.
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]

View File

@@ -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 = { version = "2", features = ["serde"] }
# Dependencies for native only.
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]

View File

@@ -65,6 +65,7 @@ bevy_pbr = "0.15"
bevy_sprite = "0.15"
profiling = "1.0"
puffin_egui = { version = "0.29", optional = true }
indexmap = { version = "2", features = ["serde"] }
# Dependencies for native only.
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]

View File

@@ -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"

View File

@@ -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),

View File

@@ -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;
let rapier_logo_buffers = crate::utils::svg::rapier_logo();
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;
}
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#"
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 527 131" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5;">
<g transform="matrix(1,0,0,1,1,-673)">
<g transform="matrix(1,0,0,1,-5.31448,644.547)">
<g transform="matrix(1.34947,0,0,1.34947,-1559.19,-910.299)">
<g transform="matrix(50,0,0,50,1277.28,785.746)">
<path d="M0.021,-0.074L0.074,-0.08C0.093,-0.082 0.106,-0.086 0.113,-0.092C0.119,-0.097 0.122,-0.111 0.122,-0.132L0.122,-0.597C0.122,-0.607 0.122,-0.616 0.121,-0.623C0.12,-0.63 0.117,-0.636 0.114,-0.641C0.111,-0.646 0.105,-0.65 0.098,-0.653C0.09,-0.656 0.08,-0.659 0.067,-0.661L0.025,-0.668L0.024,-0.685C0.04,-0.685 0.053,-0.685 0.063,-0.685C0.072,-0.684 0.081,-0.685 0.088,-0.686L0.222,-0.693C0.238,-0.695 0.255,-0.696 0.272,-0.696C0.349,-0.696 0.407,-0.685 0.446,-0.662C0.485,-0.639 0.504,-0.6 0.504,-0.543C0.504,-0.504 0.494,-0.473 0.474,-0.45C0.454,-0.427 0.424,-0.406 0.383,-0.387L0.493,-0.194C0.508,-0.167 0.522,-0.147 0.533,-0.132C0.544,-0.117 0.556,-0.106 0.567,-0.099C0.578,-0.092 0.589,-0.087 0.602,-0.085C0.614,-0.082 0.629,-0.08 0.647,-0.077L0.647,-0.059C0.633,-0.06 0.618,-0.06 0.601,-0.06L0.498,-0.06C0.483,-0.06 0.472,-0.06 0.465,-0.059L0.313,-0.341C0.302,-0.362 0.286,-0.372 0.263,-0.372L0.21,-0.372L0.21,-0.138C0.21,-0.122 0.213,-0.11 0.22,-0.103C0.226,-0.096 0.237,-0.09 0.253,-0.086L0.302,-0.074L0.302,-0.056C0.254,-0.059 0.205,-0.06 0.156,-0.06C0.106,-0.06 0.061,-0.059 0.021,-0.056L0.021,-0.074ZM0.21,-0.4C0.343,-0.395 0.41,-0.438 0.41,-0.527C0.41,-0.569 0.396,-0.603 0.367,-0.628C0.337,-0.653 0.297,-0.665 0.245,-0.665C0.24,-0.665 0.234,-0.665 0.228,-0.665C0.222,-0.664 0.216,-0.664 0.21,-0.663L0.21,-0.4Z" style="fill:rgb(235,237,240);fill-rule:nonzero;"/>
</g>
<g transform="matrix(50,0,0,50,1309.08,785.746)">
<path d="M0.201,-0.049C0.174,-0.049 0.15,-0.054 0.129,-0.064C0.108,-0.073 0.091,-0.086 0.078,-0.103C0.065,-0.12 0.055,-0.139 0.048,-0.162C0.041,-0.185 0.038,-0.209 0.038,-0.235C0.038,-0.27 0.044,-0.302 0.056,-0.329C0.068,-0.356 0.084,-0.378 0.103,-0.396C0.122,-0.414 0.144,-0.428 0.169,-0.437C0.193,-0.446 0.218,-0.451 0.243,-0.451C0.256,-0.451 0.269,-0.45 0.28,-0.447C0.291,-0.444 0.301,-0.441 0.31,-0.438C0.32,-0.435 0.33,-0.431 0.339,-0.426L0.395,-0.455L0.404,-0.452C0.404,-0.4 0.404,-0.348 0.405,-0.297C0.405,-0.245 0.405,-0.193 0.405,-0.141C0.405,-0.114 0.416,-0.102 0.439,-0.103C0.444,-0.103 0.449,-0.104 0.452,-0.106C0.455,-0.107 0.459,-0.109 0.462,-0.111C0.465,-0.114 0.468,-0.116 0.471,-0.118L0.481,-0.112C0.476,-0.1 0.47,-0.089 0.463,-0.08C0.456,-0.072 0.448,-0.065 0.438,-0.059C0.428,-0.052 0.416,-0.049 0.403,-0.049C0.38,-0.049 0.364,-0.056 0.355,-0.071C0.346,-0.085 0.341,-0.105 0.341,-0.13L0.341,-0.16C0.337,-0.146 0.331,-0.132 0.324,-0.119C0.316,-0.106 0.306,-0.094 0.295,-0.084C0.284,-0.073 0.27,-0.065 0.255,-0.059C0.24,-0.052 0.222,-0.049 0.201,-0.049ZM0.341,-0.27C0.341,-0.293 0.339,-0.313 0.336,-0.332C0.332,-0.351 0.326,-0.367 0.317,-0.38C0.308,-0.393 0.297,-0.403 0.284,-0.41C0.27,-0.417 0.253,-0.42 0.232,-0.42C0.218,-0.42 0.203,-0.417 0.188,-0.412C0.172,-0.406 0.158,-0.397 0.145,-0.384C0.132,-0.371 0.122,-0.354 0.114,-0.334C0.105,-0.313 0.101,-0.289 0.101,-0.26C0.101,-0.238 0.104,-0.217 0.11,-0.197C0.115,-0.176 0.123,-0.158 0.134,-0.143C0.145,-0.128 0.158,-0.115 0.173,-0.107C0.188,-0.097 0.206,-0.093 0.226,-0.093C0.242,-0.093 0.257,-0.097 0.271,-0.105C0.284,-0.113 0.296,-0.124 0.306,-0.139C0.316,-0.153 0.324,-0.17 0.331,-0.189C0.337,-0.208 0.34,-0.229 0.341,-0.252L0.341,-0.27Z" style="fill:rgb(235,237,240);fill-rule:nonzero;"/>
</g>
<g transform="matrix(50,0,0,50,1334.28,785.746)">
<path d="M0.156,-0.398C0.217,-0.443 0.276,-0.463 0.332,-0.459C0.379,-0.456 0.416,-0.437 0.445,-0.402C0.473,-0.367 0.487,-0.32 0.487,-0.262C0.487,-0.232 0.482,-0.204 0.471,-0.177C0.46,-0.15 0.445,-0.127 0.426,-0.108C0.407,-0.089 0.384,-0.073 0.358,-0.062C0.331,-0.051 0.303,-0.045 0.272,-0.045C0.235,-0.045 0.197,-0.055 0.156,-0.074L0.156,0.078C0.156,0.087 0.159,0.093 0.165,0.099C0.17,0.103 0.181,0.107 0.198,0.11L0.258,0.12L0.258,0.137C0.249,0.136 0.238,0.135 0.227,0.135C0.215,0.134 0.201,0.134 0.186,0.133L0.145,0.133C0.122,0.133 0.1,0.133 0.079,0.134C0.058,0.135 0.04,0.136 0.023,0.137L0.023,0.12L0.065,0.112C0.075,0.11 0.082,0.107 0.085,0.103C0.088,0.098 0.089,0.09 0.089,0.077L0.089,-0.364C0.089,-0.389 0.085,-0.401 0.076,-0.402L0.027,-0.41L0.03,-0.424C0.047,-0.431 0.061,-0.437 0.074,-0.443C0.086,-0.449 0.095,-0.454 0.102,-0.458C0.109,-0.463 0.117,-0.468 0.124,-0.475C0.131,-0.482 0.136,-0.489 0.141,-0.496L0.156,-0.496L0.156,-0.398ZM0.156,-0.367L0.156,-0.152C0.156,-0.128 0.167,-0.108 0.188,-0.093C0.209,-0.077 0.236,-0.069 0.27,-0.069C0.291,-0.069 0.311,-0.073 0.328,-0.082C0.345,-0.09 0.36,-0.101 0.373,-0.117C0.386,-0.132 0.395,-0.149 0.402,-0.17C0.409,-0.191 0.412,-0.213 0.412,-0.238C0.412,-0.266 0.408,-0.291 0.401,-0.314C0.394,-0.336 0.383,-0.355 0.37,-0.37C0.356,-0.385 0.34,-0.397 0.321,-0.404C0.302,-0.411 0.282,-0.413 0.259,-0.41C0.226,-0.406 0.192,-0.392 0.156,-0.367Z" style="fill:rgb(235,237,240);fill-rule:nonzero;"/>
</g>
<g transform="matrix(50,0,0,50,1360.53,785.746)">
<path d="M0.246,-0.056C0.235,-0.057 0.221,-0.058 0.203,-0.059C0.184,-0.06 0.162,-0.06 0.135,-0.06C0.108,-0.06 0.086,-0.06 0.068,-0.059C0.049,-0.058 0.035,-0.057 0.024,-0.056L0.024,-0.073L0.078,-0.084C0.099,-0.088 0.109,-0.1 0.109,-0.12L0.109,-0.319C0.109,-0.335 0.105,-0.347 0.097,-0.354C0.089,-0.361 0.073,-0.368 0.049,-0.373L0.049,-0.388C0.072,-0.394 0.094,-0.403 0.113,-0.415C0.132,-0.427 0.147,-0.442 0.158,-0.46L0.175,-0.46L0.175,-0.117C0.175,-0.107 0.177,-0.1 0.18,-0.095C0.183,-0.09 0.188,-0.087 0.197,-0.085L0.246,-0.073L0.246,-0.056ZM0.137,-0.667C0.15,-0.667 0.161,-0.663 0.17,-0.654C0.179,-0.645 0.184,-0.634 0.184,-0.621C0.184,-0.61 0.179,-0.6 0.17,-0.591C0.16,-0.582 0.148,-0.578 0.135,-0.578C0.123,-0.578 0.113,-0.582 0.105,-0.591C0.096,-0.599 0.092,-0.609 0.092,-0.621C0.092,-0.634 0.096,-0.645 0.106,-0.654C0.115,-0.663 0.125,-0.667 0.137,-0.667Z" style="fill:rgb(235,237,240);fill-rule:nonzero;"/>
</g>
<g transform="matrix(50,0,0,50,1374.03,785.746)">
<path d="M0.386,-0.309L0.111,-0.309C0.109,-0.296 0.108,-0.283 0.108,-0.271C0.108,-0.22 0.122,-0.178 0.149,-0.147C0.176,-0.115 0.214,-0.099 0.262,-0.099C0.285,-0.099 0.307,-0.103 0.326,-0.113C0.345,-0.122 0.366,-0.137 0.389,-0.158L0.393,-0.135C0.37,-0.105 0.344,-0.082 0.315,-0.067C0.285,-0.052 0.25,-0.044 0.211,-0.044C0.186,-0.044 0.164,-0.049 0.143,-0.06C0.122,-0.07 0.103,-0.084 0.088,-0.103C0.072,-0.122 0.06,-0.143 0.051,-0.169C0.042,-0.194 0.038,-0.221 0.038,-0.25C0.038,-0.279 0.043,-0.307 0.053,-0.333C0.062,-0.358 0.076,-0.38 0.093,-0.399C0.11,-0.418 0.131,-0.432 0.155,-0.443C0.179,-0.454 0.205,-0.459 0.233,-0.459C0.255,-0.459 0.275,-0.455 0.294,-0.448C0.312,-0.441 0.328,-0.431 0.341,-0.418C0.354,-0.405 0.365,-0.389 0.373,-0.37C0.38,-0.351 0.385,-0.331 0.386,-0.309ZM0.116,-0.332L0.261,-0.332C0.271,-0.332 0.28,-0.332 0.287,-0.333C0.294,-0.333 0.3,-0.334 0.305,-0.337C0.31,-0.339 0.313,-0.342 0.314,-0.347C0.315,-0.352 0.314,-0.358 0.312,-0.367C0.308,-0.384 0.3,-0.4 0.288,-0.414C0.275,-0.428 0.256,-0.435 0.231,-0.435C0.201,-0.435 0.176,-0.426 0.156,-0.408C0.136,-0.389 0.123,-0.364 0.116,-0.332Z" style="fill:rgb(235,237,240);fill-rule:nonzero;"/>
</g>
<g transform="matrix(50,0,0,50,1395.58,785.746)">
<path d="M0.024,-0.056L0.024,-0.072L0.072,-0.081C0.085,-0.084 0.094,-0.089 0.098,-0.096C0.101,-0.103 0.103,-0.115 0.103,-0.132L0.103,-0.324C0.103,-0.337 0.1,-0.347 0.095,-0.353C0.089,-0.359 0.076,-0.364 0.057,-0.368L0.031,-0.374L0.033,-0.389C0.059,-0.398 0.081,-0.409 0.1,-0.422C0.119,-0.435 0.135,-0.455 0.149,-0.482L0.167,-0.482C0.168,-0.464 0.169,-0.445 0.17,-0.424C0.171,-0.403 0.171,-0.381 0.171,-0.358C0.178,-0.37 0.185,-0.382 0.194,-0.393C0.202,-0.404 0.212,-0.415 0.224,-0.426C0.247,-0.448 0.267,-0.459 0.285,-0.459C0.302,-0.459 0.315,-0.454 0.324,-0.445C0.333,-0.436 0.338,-0.424 0.338,-0.409C0.338,-0.396 0.334,-0.385 0.327,-0.378C0.319,-0.37 0.309,-0.366 0.298,-0.366C0.287,-0.366 0.275,-0.369 0.263,-0.376C0.251,-0.383 0.241,-0.386 0.232,-0.386C0.225,-0.386 0.219,-0.383 0.212,-0.376C0.205,-0.369 0.198,-0.361 0.192,-0.352C0.186,-0.343 0.181,-0.333 0.177,-0.323C0.173,-0.312 0.171,-0.304 0.171,-0.297L0.171,-0.13C0.171,-0.113 0.174,-0.101 0.18,-0.096C0.185,-0.09 0.197,-0.086 0.216,-0.083L0.288,-0.072L0.288,-0.056C0.271,-0.057 0.251,-0.058 0.23,-0.059C0.208,-0.06 0.185,-0.06 0.16,-0.06L0.146,-0.06C0.123,-0.06 0.101,-0.06 0.081,-0.059C0.06,-0.058 0.041,-0.057 0.024,-0.056Z" style="fill:rgb(235,237,240);fill-rule:nonzero;"/>
</g>
</g>
</g>
</g>
</svg>
"#;
pub struct PathConvIter<'a> {
iter: std::slice::Iter<'a, usvg::PathSegment>,
prev: Point,
first: Point,
needs_end: bool,
deferred: Option<PathEvent>,
}
impl Iterator for PathConvIter<'_> {
type Item = PathEvent;
fn next(&mut self) -> Option<PathEvent> {
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<GpuVertex> for VertexCtor {
fn new_vertex(&mut self, vertex: tessellation::FillVertex) -> GpuVertex {
GpuVertex {
position: vertex.position().to_array(),
prim_id: self.prim_id,
}
}
}
impl StrokeVertexConstructor<GpuVertex> 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,
}

View File

@@ -1 +1,2 @@
pub mod character;
pub mod svg;

211
examples2d/utils/svg.rs Normal file
View File

@@ -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#"
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 527 131" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5;">
<g transform="matrix(1,0,0,1,1,-673)">
<g transform="matrix(1,0,0,1,-5.31448,644.547)">
<g transform="matrix(1.34947,0,0,1.34947,-1559.19,-910.299)">
<g transform="matrix(50,0,0,50,1277.28,785.746)">
<path d="M0.021,-0.074L0.074,-0.08C0.093,-0.082 0.106,-0.086 0.113,-0.092C0.119,-0.097 0.122,-0.111 0.122,-0.132L0.122,-0.597C0.122,-0.607 0.122,-0.616 0.121,-0.623C0.12,-0.63 0.117,-0.636 0.114,-0.641C0.111,-0.646 0.105,-0.65 0.098,-0.653C0.09,-0.656 0.08,-0.659 0.067,-0.661L0.025,-0.668L0.024,-0.685C0.04,-0.685 0.053,-0.685 0.063,-0.685C0.072,-0.684 0.081,-0.685 0.088,-0.686L0.222,-0.693C0.238,-0.695 0.255,-0.696 0.272,-0.696C0.349,-0.696 0.407,-0.685 0.446,-0.662C0.485,-0.639 0.504,-0.6 0.504,-0.543C0.504,-0.504 0.494,-0.473 0.474,-0.45C0.454,-0.427 0.424,-0.406 0.383,-0.387L0.493,-0.194C0.508,-0.167 0.522,-0.147 0.533,-0.132C0.544,-0.117 0.556,-0.106 0.567,-0.099C0.578,-0.092 0.589,-0.087 0.602,-0.085C0.614,-0.082 0.629,-0.08 0.647,-0.077L0.647,-0.059C0.633,-0.06 0.618,-0.06 0.601,-0.06L0.498,-0.06C0.483,-0.06 0.472,-0.06 0.465,-0.059L0.313,-0.341C0.302,-0.362 0.286,-0.372 0.263,-0.372L0.21,-0.372L0.21,-0.138C0.21,-0.122 0.213,-0.11 0.22,-0.103C0.226,-0.096 0.237,-0.09 0.253,-0.086L0.302,-0.074L0.302,-0.056C0.254,-0.059 0.205,-0.06 0.156,-0.06C0.106,-0.06 0.061,-0.059 0.021,-0.056L0.021,-0.074ZM0.21,-0.4C0.343,-0.395 0.41,-0.438 0.41,-0.527C0.41,-0.569 0.396,-0.603 0.367,-0.628C0.337,-0.653 0.297,-0.665 0.245,-0.665C0.24,-0.665 0.234,-0.665 0.228,-0.665C0.222,-0.664 0.216,-0.664 0.21,-0.663L0.21,-0.4Z" style="fill:rgb(235,237,240);fill-rule:nonzero;"/>
</g>
<g transform="matrix(50,0,0,50,1309.08,785.746)">
<path d="M0.201,-0.049C0.174,-0.049 0.15,-0.054 0.129,-0.064C0.108,-0.073 0.091,-0.086 0.078,-0.103C0.065,-0.12 0.055,-0.139 0.048,-0.162C0.041,-0.185 0.038,-0.209 0.038,-0.235C0.038,-0.27 0.044,-0.302 0.056,-0.329C0.068,-0.356 0.084,-0.378 0.103,-0.396C0.122,-0.414 0.144,-0.428 0.169,-0.437C0.193,-0.446 0.218,-0.451 0.243,-0.451C0.256,-0.451 0.269,-0.45 0.28,-0.447C0.291,-0.444 0.301,-0.441 0.31,-0.438C0.32,-0.435 0.33,-0.431 0.339,-0.426L0.395,-0.455L0.404,-0.452C0.404,-0.4 0.404,-0.348 0.405,-0.297C0.405,-0.245 0.405,-0.193 0.405,-0.141C0.405,-0.114 0.416,-0.102 0.439,-0.103C0.444,-0.103 0.449,-0.104 0.452,-0.106C0.455,-0.107 0.459,-0.109 0.462,-0.111C0.465,-0.114 0.468,-0.116 0.471,-0.118L0.481,-0.112C0.476,-0.1 0.47,-0.089 0.463,-0.08C0.456,-0.072 0.448,-0.065 0.438,-0.059C0.428,-0.052 0.416,-0.049 0.403,-0.049C0.38,-0.049 0.364,-0.056 0.355,-0.071C0.346,-0.085 0.341,-0.105 0.341,-0.13L0.341,-0.16C0.337,-0.146 0.331,-0.132 0.324,-0.119C0.316,-0.106 0.306,-0.094 0.295,-0.084C0.284,-0.073 0.27,-0.065 0.255,-0.059C0.24,-0.052 0.222,-0.049 0.201,-0.049ZM0.341,-0.27C0.341,-0.293 0.339,-0.313 0.336,-0.332C0.332,-0.351 0.326,-0.367 0.317,-0.38C0.308,-0.393 0.297,-0.403 0.284,-0.41C0.27,-0.417 0.253,-0.42 0.232,-0.42C0.218,-0.42 0.203,-0.417 0.188,-0.412C0.172,-0.406 0.158,-0.397 0.145,-0.384C0.132,-0.371 0.122,-0.354 0.114,-0.334C0.105,-0.313 0.101,-0.289 0.101,-0.26C0.101,-0.238 0.104,-0.217 0.11,-0.197C0.115,-0.176 0.123,-0.158 0.134,-0.143C0.145,-0.128 0.158,-0.115 0.173,-0.107C0.188,-0.097 0.206,-0.093 0.226,-0.093C0.242,-0.093 0.257,-0.097 0.271,-0.105C0.284,-0.113 0.296,-0.124 0.306,-0.139C0.316,-0.153 0.324,-0.17 0.331,-0.189C0.337,-0.208 0.34,-0.229 0.341,-0.252L0.341,-0.27Z" style="fill:rgb(235,237,240);fill-rule:nonzero;"/>
</g>
<g transform="matrix(50,0,0,50,1334.28,785.746)">
<path d="M0.156,-0.398C0.217,-0.443 0.276,-0.463 0.332,-0.459C0.379,-0.456 0.416,-0.437 0.445,-0.402C0.473,-0.367 0.487,-0.32 0.487,-0.262C0.487,-0.232 0.482,-0.204 0.471,-0.177C0.46,-0.15 0.445,-0.127 0.426,-0.108C0.407,-0.089 0.384,-0.073 0.358,-0.062C0.331,-0.051 0.303,-0.045 0.272,-0.045C0.235,-0.045 0.197,-0.055 0.156,-0.074L0.156,0.078C0.156,0.087 0.159,0.093 0.165,0.099C0.17,0.103 0.181,0.107 0.198,0.11L0.258,0.12L0.258,0.137C0.249,0.136 0.238,0.135 0.227,0.135C0.215,0.134 0.201,0.134 0.186,0.133L0.145,0.133C0.122,0.133 0.1,0.133 0.079,0.134C0.058,0.135 0.04,0.136 0.023,0.137L0.023,0.12L0.065,0.112C0.075,0.11 0.082,0.107 0.085,0.103C0.088,0.098 0.089,0.09 0.089,0.077L0.089,-0.364C0.089,-0.389 0.085,-0.401 0.076,-0.402L0.027,-0.41L0.03,-0.424C0.047,-0.431 0.061,-0.437 0.074,-0.443C0.086,-0.449 0.095,-0.454 0.102,-0.458C0.109,-0.463 0.117,-0.468 0.124,-0.475C0.131,-0.482 0.136,-0.489 0.141,-0.496L0.156,-0.496L0.156,-0.398ZM0.156,-0.367L0.156,-0.152C0.156,-0.128 0.167,-0.108 0.188,-0.093C0.209,-0.077 0.236,-0.069 0.27,-0.069C0.291,-0.069 0.311,-0.073 0.328,-0.082C0.345,-0.09 0.36,-0.101 0.373,-0.117C0.386,-0.132 0.395,-0.149 0.402,-0.17C0.409,-0.191 0.412,-0.213 0.412,-0.238C0.412,-0.266 0.408,-0.291 0.401,-0.314C0.394,-0.336 0.383,-0.355 0.37,-0.37C0.356,-0.385 0.34,-0.397 0.321,-0.404C0.302,-0.411 0.282,-0.413 0.259,-0.41C0.226,-0.406 0.192,-0.392 0.156,-0.367Z" style="fill:rgb(235,237,240);fill-rule:nonzero;"/>
</g>
<g transform="matrix(50,0,0,50,1360.53,785.746)">
<path d="M0.246,-0.056C0.235,-0.057 0.221,-0.058 0.203,-0.059C0.184,-0.06 0.162,-0.06 0.135,-0.06C0.108,-0.06 0.086,-0.06 0.068,-0.059C0.049,-0.058 0.035,-0.057 0.024,-0.056L0.024,-0.073L0.078,-0.084C0.099,-0.088 0.109,-0.1 0.109,-0.12L0.109,-0.319C0.109,-0.335 0.105,-0.347 0.097,-0.354C0.089,-0.361 0.073,-0.368 0.049,-0.373L0.049,-0.388C0.072,-0.394 0.094,-0.403 0.113,-0.415C0.132,-0.427 0.147,-0.442 0.158,-0.46L0.175,-0.46L0.175,-0.117C0.175,-0.107 0.177,-0.1 0.18,-0.095C0.183,-0.09 0.188,-0.087 0.197,-0.085L0.246,-0.073L0.246,-0.056ZM0.137,-0.667C0.15,-0.667 0.161,-0.663 0.17,-0.654C0.179,-0.645 0.184,-0.634 0.184,-0.621C0.184,-0.61 0.179,-0.6 0.17,-0.591C0.16,-0.582 0.148,-0.578 0.135,-0.578C0.123,-0.578 0.113,-0.582 0.105,-0.591C0.096,-0.599 0.092,-0.609 0.092,-0.621C0.092,-0.634 0.096,-0.645 0.106,-0.654C0.115,-0.663 0.125,-0.667 0.137,-0.667Z" style="fill:rgb(235,237,240);fill-rule:nonzero;"/>
</g>
<g transform="matrix(50,0,0,50,1374.03,785.746)">
<path d="M0.386,-0.309L0.111,-0.309C0.109,-0.296 0.108,-0.283 0.108,-0.271C0.108,-0.22 0.122,-0.178 0.149,-0.147C0.176,-0.115 0.214,-0.099 0.262,-0.099C0.285,-0.099 0.307,-0.103 0.326,-0.113C0.345,-0.122 0.366,-0.137 0.389,-0.158L0.393,-0.135C0.37,-0.105 0.344,-0.082 0.315,-0.067C0.285,-0.052 0.25,-0.044 0.211,-0.044C0.186,-0.044 0.164,-0.049 0.143,-0.06C0.122,-0.07 0.103,-0.084 0.088,-0.103C0.072,-0.122 0.06,-0.143 0.051,-0.169C0.042,-0.194 0.038,-0.221 0.038,-0.25C0.038,-0.279 0.043,-0.307 0.053,-0.333C0.062,-0.358 0.076,-0.38 0.093,-0.399C0.11,-0.418 0.131,-0.432 0.155,-0.443C0.179,-0.454 0.205,-0.459 0.233,-0.459C0.255,-0.459 0.275,-0.455 0.294,-0.448C0.312,-0.441 0.328,-0.431 0.341,-0.418C0.354,-0.405 0.365,-0.389 0.373,-0.37C0.38,-0.351 0.385,-0.331 0.386,-0.309ZM0.116,-0.332L0.261,-0.332C0.271,-0.332 0.28,-0.332 0.287,-0.333C0.294,-0.333 0.3,-0.334 0.305,-0.337C0.31,-0.339 0.313,-0.342 0.314,-0.347C0.315,-0.352 0.314,-0.358 0.312,-0.367C0.308,-0.384 0.3,-0.4 0.288,-0.414C0.275,-0.428 0.256,-0.435 0.231,-0.435C0.201,-0.435 0.176,-0.426 0.156,-0.408C0.136,-0.389 0.123,-0.364 0.116,-0.332Z" style="fill:rgb(235,237,240);fill-rule:nonzero;"/>
</g>
<g transform="matrix(50,0,0,50,1395.58,785.746)">
<path d="M0.024,-0.056L0.024,-0.072L0.072,-0.081C0.085,-0.084 0.094,-0.089 0.098,-0.096C0.101,-0.103 0.103,-0.115 0.103,-0.132L0.103,-0.324C0.103,-0.337 0.1,-0.347 0.095,-0.353C0.089,-0.359 0.076,-0.364 0.057,-0.368L0.031,-0.374L0.033,-0.389C0.059,-0.398 0.081,-0.409 0.1,-0.422C0.119,-0.435 0.135,-0.455 0.149,-0.482L0.167,-0.482C0.168,-0.464 0.169,-0.445 0.17,-0.424C0.171,-0.403 0.171,-0.381 0.171,-0.358C0.178,-0.37 0.185,-0.382 0.194,-0.393C0.202,-0.404 0.212,-0.415 0.224,-0.426C0.247,-0.448 0.267,-0.459 0.285,-0.459C0.302,-0.459 0.315,-0.454 0.324,-0.445C0.333,-0.436 0.338,-0.424 0.338,-0.409C0.338,-0.396 0.334,-0.385 0.327,-0.378C0.319,-0.37 0.309,-0.366 0.298,-0.366C0.287,-0.366 0.275,-0.369 0.263,-0.376C0.251,-0.383 0.241,-0.386 0.232,-0.386C0.225,-0.386 0.219,-0.383 0.212,-0.376C0.205,-0.369 0.198,-0.361 0.192,-0.352C0.186,-0.343 0.181,-0.333 0.177,-0.323C0.173,-0.312 0.171,-0.304 0.171,-0.297L0.171,-0.13C0.171,-0.113 0.174,-0.101 0.18,-0.096C0.185,-0.09 0.197,-0.086 0.216,-0.083L0.288,-0.072L0.288,-0.056C0.271,-0.057 0.251,-0.058 0.23,-0.059C0.208,-0.06 0.185,-0.06 0.16,-0.06L0.146,-0.06C0.123,-0.06 0.101,-0.06 0.081,-0.059C0.06,-0.058 0.041,-0.057 0.024,-0.056Z" style="fill:rgb(235,237,240);fill-rule:nonzero;"/>
</g>
</g>
</g>
</g>
</svg>
"#;
pub fn rapier_logo() -> Vec<(Vec<Point2<f32>>, Vec<[u32; 3]>)> {
tessellate_svg_str(RAPIER_SVG_STR)
}
pub fn tessellate_svg_str(svg_str: &str) -> Vec<(Vec<Point2<f32>>, 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<PathEvent>,
}
impl Iterator for PathConvIter<'_> {
type Item = PathEvent;
fn next(&mut self) -> Option<PathEvent> {
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<GpuVertex> for VertexCtor {
fn new_vertex(&mut self, vertex: tessellation::FillVertex) -> GpuVertex {
GpuVertex {
position: vertex.position().to_array(),
prim_id: self.prim_id,
}
}
}
impl StrokeVertexConstructor<GpuVertex> 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,
}

122
examples2d/voxels2.rs Normal file
View File

@@ -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);
}

View File

@@ -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"

View File

@@ -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),

324
examples3d/voxels3.rs Normal file
View File

@@ -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 dont 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 dont 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<String> {
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(),
]
}

View File

@@ -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<Real>,
voxels: &[Point<i32>],
) -> 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<Real>,
points: &[Point<Real>],
) -> 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<Real>],
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")]

View File

@@ -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};

View File

@@ -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(_) => {}
}
}

View File

@@ -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)]

View File

@@ -1,4 +1,5 @@
#![allow(clippy::too_many_arguments)]
extern crate nalgebra as na;
pub use crate::graphics::{BevyMaterial, GraphicsManager};

View File

@@ -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<Mesh> {
.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<Mesh> {
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,
};

View File

@@ -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<u32>,
@@ -11,11 +12,18 @@ pub enum SettingValue {
value: f32,
range: RangeInclusive<f32>,
},
Bool {
value: bool,
},
String {
value: usize,
range: Vec<String>,
},
}
#[derive(Default, Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct ExampleSettings {
values: HashMap<String, SettingValue>,
values: IndexMap<String, SettingValue>,
}
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 doesnt 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<u32>) {
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<String>) {
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<String>,
) -> 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 doesnt have the right type. Overwrite with the new value.
*entry = to_insert;
default
}
}
}
pub fn get_bool(&self, key: &'static str) -> Option<bool> {
match self.values.get(key)? {
SettingValue::Bool { value } => Some(*value),
_ => None,
}
}
pub fn get_u32(&self, key: &'static str) -> Option<u32> {
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<usize> {
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<String>) {
self.values
.insert(key.to_string(), SettingValue::Label(value.into()));
}
}

View File

@@ -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()
},
));

View File

@@ -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;