Files
rapier/examples3d/voxels3.rs
Sébastien Crozet 317322b31b feat: reduce the amount of duplicate work the broad-phase is doing for user changes and CCD + release v0.28.0 (#872)
* feat: reduce the amount of duplicate work the broad-phase is doing for user changes and CCD

* Release v0.28.0

* chore: fix warnings

* chore: clippy fixes

* chore: more clippy fixes
2025-08-08 18:15:34 +02:00

313 lines
12 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use obj::raw::object::Polygon;
use rapier_testbed3d::KeyCode;
use rapier_testbed3d::Testbed;
use rapier3d::parry::bounding_volume;
use rapier3d::parry::transformation::voxelization::FillMode;
use rapier3d::prelude::*;
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 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);
let test_ccd = settings.get_or_set_bool("Test CCD", false);
// TODO: give a better placement to the objs.
// settings.get_or_set_bool("Load .obj", false);
let load_obj = false;
/*
* 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.iter().copied());
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(&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(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 mut 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,
]);
if test_ccd {
rb = rb.linvel(vector![0.0, -1000.0, 0.0]).ccd_enabled(true);
}
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()
};
let query_pipeline = physics.broad_phase.as_query_pipeline(
physics.narrow_phase.query_dispatcher(),
&physics.bodies,
&physics.colliders,
filter,
);
if let Some((handle, hit)) = query_pipeline.cast_ray_and_get_normal(&ray, Real::MAX, true) {
// 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_at_id(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.set_voxel(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(),
]
}