diff --git a/Cargo.lock b/Cargo.lock index 564fe931..635518ce 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -147,6 +147,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "arbitrary-chunks" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ad8689a486416c401ea15715a4694de30054248ec627edbf31f49cb64ee4086" + [[package]] name = "arrayref" version = "0.3.9" @@ -305,8 +311,10 @@ dependencies = [ "itertools 0.13.0", "libm", "nalgebra", + "obvhs", "parry2d", "parry2d-f64", + "rand 0.9.1", "serde", "slab", "smallvec", @@ -334,6 +342,7 @@ dependencies = [ "itertools 0.13.0", "libm", "nalgebra", + "obvhs", "parry3d", "parry3d-f64", "serde", @@ -923,7 +932,7 @@ dependencies = [ "glam 0.30.8", "itertools 0.14.0", "libm", - "rand", + "rand 0.8.5", "rand_distr", "serde", "smallvec", @@ -1547,6 +1556,16 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" +[[package]] +name = "block-pseudorand" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2097358495d244a0643746f4d13eedba4608137008cf9dec54e53a3b700115a6" +dependencies = [ + "chiapos-chacha8", + "nanorand", +] + [[package]] name = "block2" version = "0.5.1" @@ -1663,6 +1682,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "chiapos-chacha8" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33f8be573a85f6c2bc1b8e43834c07e32f95e489b914bf856c0549c3c269cd0a" +dependencies = [ + "rayon", +] + [[package]] name = "ciborium" version = "0.2.2" @@ -2458,8 +2486,13 @@ dependencies = [ "approx", "bytemuck", "libm", +<<<<<<< HEAD "rand", "serde_core", +======= + "rand 0.8.5", + "serde", +>>>>>>> 5baf1b3 (Make spatial queries generic over colliders (WIP)) ] [[package]] @@ -3034,6 +3067,12 @@ dependencies = [ "syn", ] +[[package]] +name = "nanorand" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "729eb334247daa1803e0a094d0a5c55711b85571179f5ec6e53eccfdf7008958" + [[package]] name = "ndk" version = "0.9.0" @@ -3386,6 +3425,19 @@ dependencies = [ "objc2-foundation", ] +[[package]] +name = "obvhs" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96831b0c2386d37ee14731a382554956523a1fc202a26aff9fa0b34a6450d583" +dependencies = [ + "bytemuck", + "glam", + "half", + "rayon", + "rdst", +] + [[package]] name = "offset-allocator" version = "0.2.0" @@ -3588,6 +3640,12 @@ dependencies = [ "thiserror 2.0.17", ] +[[package]] +name = "partition" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "947f833aaa585cf12b8ec7c0476c98784c49f33b861376ffc84ed92adebf2aba" + [[package]] name = "paste" version = "1.0.15" @@ -3832,8 +3890,24 @@ version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ +<<<<<<< HEAD "rand_chacha", "rand_core", +======= + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.3", +>>>>>>> 5baf1b3 (Make spatial queries generic over colliders (WIP)) ] [[package]] @@ -3843,7 +3917,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", ] [[package]] @@ -3855,6 +3939,15 @@ dependencies = [ "getrandom", ] +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.3", +] + [[package]] name = "rand_distr" version = "0.5.1" @@ -3862,7 +3955,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8615d50dcf34fa31f7ab52692afec947c4dd0ab803cc87cb3b0b4570ff7463" dependencies = [ "num-traits", - "rand", + "rand 0.8.5", ] [[package]] @@ -3909,6 +4002,21 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "rdst" +version = "0.20.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e7970b4e577b76a96d5e56b5f6662b66d1a4e1f5bb026ee118fc31b373c2752" +dependencies = [ + "arbitrary-chunks", + "block-pseudorand", + "criterion", + "partition", + "rayon", + "tikv-jemallocator", + "voracious_radix_sort", +] + [[package]] name = "read-fonts" version = "0.35.0" @@ -4406,6 +4514,26 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "tikv-jemalloc-sys" +version = "0.5.4+5.3.0-patched" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9402443cb8fd499b6f327e40565234ff34dbda27460c5b47db0db77443dd85d1" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "tikv-jemallocator" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "965fe0c26be5c56c94e38ba547249074803efd52adfb66de62107d95aab3eaca" +dependencies = [ + "libc", + "tikv-jemalloc-sys", +] + [[package]] name = "tinytemplate" version = "1.2.1" @@ -4676,6 +4804,15 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "voracious_radix_sort" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446e7ffcb6c27a71d05af7e51ef2ee5b71c48424b122a832f2439651e1914899" +dependencies = [ + "rayon", +] + [[package]] name = "walkdir" version = "2.5.0" diff --git a/crates/avian2d/Cargo.toml b/crates/avian2d/Cargo.toml index c23e8846..dbe8cc1d 100644 --- a/crates/avian2d/Cargo.toml +++ b/crates/avian2d/Cargo.toml @@ -33,6 +33,7 @@ parallel = [ "bevy/multi_threaded", "parry2d?/parallel", "parry2d-f64?/parallel", + "obvhs/parallel", ] enhanced-determinism = [ "dep:libm", @@ -92,6 +93,7 @@ bevy_heavy = { version = "0.3" } bevy_transform_interpolation = { version = "0.3" } libm = { version = "0.2", optional = true } approx = "0.5" +obvhs = "0.2" parry2d = { version = "0.25", optional = true } parry2d-f64 = { version = "0.25", optional = true } nalgebra = { version = "0.34", features = ["convert-glam030"], optional = true } @@ -117,6 +119,7 @@ glam = { version = "0.30", features = ["bytemuck"] } bytemuck = "1.19" criterion = { version = "0.5", features = ["html_reports"] } bevy_mod_debugdump = { version = "0.14" } +rand = "0.9" [lints] workspace = true diff --git a/crates/avian2d/examples/custom_collider.rs b/crates/avian2d/examples/custom_collider.rs index 89708bad..7f35c3fa 100644 --- a/crates/avian2d/examples/custom_collider.rs +++ b/crates/avian2d/examples/custom_collider.rs @@ -61,7 +61,7 @@ impl AnyCollider for CircleCollider { &self, position: Vector, _: impl Into, - _: AabbContext, + _: SingleContext, ) -> ColliderAabb { ColliderAabb::new(position, Vector::splat(self.radius)) } @@ -77,7 +77,7 @@ impl AnyCollider for CircleCollider { _rotation2: impl Into, prediction_distance: Scalar, manifolds: &mut Vec, - _: ContactManifoldContext, + _: PairContext, ) { // Clear the previous manifolds. manifolds.clear(); diff --git a/crates/avian2d/examples/spatial_query.rs b/crates/avian2d/examples/spatial_query.rs new file mode 100644 index 00000000..ce9255ac --- /dev/null +++ b/crates/avian2d/examples/spatial_query.rs @@ -0,0 +1,306 @@ +#![expect(clippy::unnecessary_cast)] + +use avian2d::{ + collision::{ + collider::{PairContext, QueryCollider, QueryShapeCastHit, SingleContext}, + contact_types::PackedFeatureId, + }, + math::*, + prelude::*, +}; +use bevy::{color::palettes::tailwind::GRAY_400, prelude::*, render::camera::ScalingMode}; +use examples_common_2d::ExampleCommonPlugin; +use ops::FloatPow; +use rand::Rng; + +fn main() { + App::new() + .add_plugins(( + DefaultPlugins, + ExampleCommonPlugin, + PhysicsPlugins::default() + .with_length_unit(10.0) + .build() + .disable::>() + .disable::>() + .disable::>(), + PhysicsDebugPlugin::default(), + ColliderBackendPlugin::::default(), + NarrowPhasePlugin::::default(), + SpatialQueryPlugin::::default(), + )) + .insert_gizmo_config( + PhysicsGizmos::default().with_aabb_color(GRAY_400.into()), + GizmoConfig { + line: GizmoLineConfig { + width: 0.5, + ..default() + }, + ..default() + }, + ) + .add_systems(Startup, setup) + .add_systems(FixedUpdate, move_random) + .add_systems(Update, cast_ray) + .run(); +} + +/// A simplified custom collider. +#[derive(Component, Clone, Copy, Debug, Reflect)] +struct CircleCollider { + radius: Scalar, + unscaled_radius: Scalar, + scale: Scalar, +} + +impl CircleCollider { + fn new(radius: Scalar) -> Self { + Self { + radius, + unscaled_radius: radius, + scale: 1.0, + } + } +} + +impl AnyCollider for CircleCollider { + type Context = (); + + fn aabb_with_context( + &self, + position: Vector, + _: impl Into, + _: SingleContext<()>, + ) -> ColliderAabb { + ColliderAabb::new(position, Vector::splat(self.radius)) + } + + fn contact_manifolds_with_context( + &self, + other: &Self, + position1: Vector, + rotation1: impl Into, + position2: Vector, + _: impl Into, + prediction_distance: Scalar, + manifolds: &mut Vec, + _: PairContext<()>, + ) { + // Clear the previous manifolds. + manifolds.clear(); + + let rotation1: Rotation = rotation1.into(); + + let inv_rotation1 = rotation1.inverse(); + let delta_pos = inv_rotation1 * (position2 - position1); + + let distance_squared = delta_pos.length_squared(); + let sum_radius = self.radius + other.radius; + + if distance_squared < (sum_radius + prediction_distance).powi(2) { + let local_normal1 = if distance_squared != 0.0 { + delta_pos.normalize_or_zero() + } else { + Vector::X + }; + let local_point1 = local_normal1 * self.radius; + let normal = rotation1 * local_normal1; + + let separation = distance_squared.sqrt() - sum_radius; + + let point1 = rotation1 * local_point1; + let anchor1 = point1 + normal * separation * 0.5; + let anchor2 = anchor1 + (position1 - position2); + let world_point = position1 + anchor1; + + let point = ContactPoint::new(anchor1, anchor2, world_point, -separation) + .with_feature_ids(PackedFeatureId::face(0), PackedFeatureId::face(0)); + + manifolds.push(ContactManifold::new([point], rotation1 * local_normal1)); + } + } +} + +impl QueryCollider for CircleCollider { + type CastShape = Circle; + type Shape = Circle; + + fn ray_hit(&self, ray: obvhs::ray::Ray, _: bool, _: SingleContext<()>) -> f32 { + let offset = ray.origin; + let projected = offset.dot(ray.direction); + let closest_point = offset - projected * ray.direction; + let distance_squared = self.radius.squared() - closest_point.length_squared(); + if distance_squared < 0. + || ops::copysign(projected.squared(), -projected) < -distance_squared + { + f32::INFINITY + } else { + let toi = -projected - ops::sqrt(distance_squared); + if toi > ray.tmax { + f32::INFINITY + } else { + toi.max(0.) + } + } + } + + fn ray_normal(&self, point: Vector, _: Dir2, _: bool, _: SingleContext<()>) -> Vec2 { + point.normalize_or(Vec2::Y) + } + + fn shape_cast( + &self, + shape: &Self::CastShape, + _: Rotation, + local_origin: Vec2, + local_dir: Dir2, + (tmin, tmax): (f32, f32), + context: SingleContext<()>, + ) -> Option { + let mut c = self.clone(); + c.radius += shape.radius; + let ray = obvhs::ray::Ray::new( + local_origin.extend(0.).into(), + local_dir.extend(0.).into(), + tmin, + tmax, + ); + let hit = c.ray_hit(ray, true, context); + if hit < f32::INFINITY { + Some(QueryShapeCastHit { + distance: hit, + // TODO + point: default(), + normal: default(), + }) + } else { + None + } + } + + fn shape_intersection( + &self, + shape: &Self::CastShape, + shape_rotation: Rotation, + local_origin: Vec2, + _: SingleContext<()>, + ) -> bool { + todo!() + } + + fn closest_point(&self, point: Vec2, solid: bool, _: SingleContext<()>) -> Vec2 { + todo!() + } + + fn contains_point(&self, point: Vec2, _: SingleContext<()>) -> bool { + todo!() + } +} + +// Implement mass computation for the collider shape. +// This is needed for physics to behave correctly. +impl ComputeMassProperties2d for CircleCollider { + fn mass(&self, density: f32) -> f32 { + // In 2D, the Z length is assumed to be `1.0`, so volume == area. + let volume = core::f32::consts::PI * self.radius.powi(2) as f32; + density * volume + } + + fn unit_angular_inertia(&self) -> f32 { + // Angular inertia for a circle, assuming a mass of `1.0`. + self.radius.powi(2) as f32 / 2.0 + } + + fn center_of_mass(&self) -> Vec2 { + Vec2::ZERO + } +} + +// Note: This circle collider only supports uniform scaling. +impl ScalableCollider for CircleCollider { + fn scale(&self) -> Vector { + Vector::splat(self.scale) + } + + fn set_scale(&mut self, scale: Vector, _detail: u32) { + // For non-unifprm scaling, this would need to be converted to an ellipse collider or a convex hull. + self.scale = scale.max_element(); + self.radius = self.unscaled_radius * scale.max_element(); + } +} + +const X_COUNT: i32 = 50; +const Y_COUNT: i32 = 30; +const PARTICLE_RADIUS: f32 = 5.; +const SPACING_FACTOR: f32 = 3.; + +fn setup(mut commands: Commands) { + commands.spawn(( + Camera2d, + Projection::Orthographic(OrthographicProjection { + scaling_mode: ScalingMode::FixedVertical { + viewport_height: SPACING_FACTOR * PARTICLE_RADIUS * (Y_COUNT + 1) as f32, + }, + ..OrthographicProjection::default_2d() + }), + )); + + for x in -X_COUNT / 2..=X_COUNT / 2 { + for y in -Y_COUNT / 2..=Y_COUNT / 2 { + commands.spawn(( + RigidBody::Kinematic, + Transform::from_xyz( + x as f32 * SPACING_FACTOR * PARTICLE_RADIUS, + y as f32 * SPACING_FACTOR * PARTICLE_RADIUS, + 0.0, + ), + CircleCollider::new(PARTICLE_RADIUS.adjust_precision()), + CollisionLayers::new(LayerMask::DEFAULT, LayerMask::NONE), + )); + } + } +} + +fn move_random(window: Single<&Window>, mut query: Query<(&Position, &mut LinearVelocity)>) { + let mut rng = rand::rng(); + for (pos, mut lin_vel) in query.iter_mut() { + let max_y = (Y_COUNT + 1) as f32 * 0.5 * SPACING_FACTOR * PARTICLE_RADIUS; + let aspect_ratio = window.resolution.width() / window.resolution.height(); + let out_of_x = pos.x.abs() > max_y * aspect_ratio - PARTICLE_RADIUS; + let out_of_y = pos.y.abs() > max_y - PARTICLE_RADIUS; + if out_of_x { + lin_vel.x = -lin_vel.x.copysign(pos.x); + } + if out_of_y { + lin_vel.y = -lin_vel.y.copysign(pos.y); + } + if rng.random::() < 0.15 { + lin_vel.0 += Vec2::new(rng.random_range(-0.25..0.25), rng.random_range(-0.25..0.25)); + } + } +} + +fn cast_ray(mut gizmos: Gizmos, spatial_query: SpatialQuery, time: Res