From d02b7dbbd323224c09fa00e6ddded2ef0dfa7f3b Mon Sep 17 00:00:00 2001 From: William Rose Date: Thu, 14 Mar 2024 18:39:16 +0000 Subject: [PATCH 01/17] Add an optional (default) dependency for rand --- crates/bevy_math/Cargo.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/bevy_math/Cargo.toml b/crates/bevy_math/Cargo.toml index 128e8529d3d2c..441c273dd6c47 100644 --- a/crates/bevy_math/Cargo.toml +++ b/crates/bevy_math/Cargo.toml @@ -14,11 +14,13 @@ thiserror = "1.0" serde = { version = "1", features = ["derive"], optional = true } libm = { version = "0.2", optional = true } approx = { version = "0.5", optional = true } +rand = { version = "0.8", default-features = false, optional = true } [dev-dependencies] approx = "0.5" [features] +default = ["rand"] serialize = ["dep:serde", "glam/serde"] # Enable approx for glam types to approximate floating point equality comparisons and assertions approx = ["dep:approx", "glam/approx"] @@ -31,6 +33,8 @@ libm = ["dep:libm", "glam/libm"] glam_assert = ["glam/glam-assert"] # Enable assertions in debug builds to check the validity of parameters passed to glam debug_glam_assert = ["glam/debug-glam-assert"] +# Enable the rand dependency for shape_sampling +rand = ["dep:rand"] [lints] workspace = true From 5745659862f33532d5105571b0964780661aedcd Mon Sep 17 00:00:00 2001 From: William Rose Date: Thu, 14 Mar 2024 19:10:07 +0000 Subject: [PATCH 02/17] Add rand to dev-dependencies so that it will have thread_rng --- crates/bevy_math/Cargo.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/bevy_math/Cargo.toml b/crates/bevy_math/Cargo.toml index 441c273dd6c47..76f714a71edf9 100644 --- a/crates/bevy_math/Cargo.toml +++ b/crates/bevy_math/Cargo.toml @@ -18,6 +18,8 @@ rand = { version = "0.8", default-features = false, optional = true } [dev-dependencies] approx = "0.5" +# Enable the use of thread_rng in examples +rand = "0.8" [features] default = ["rand"] From b5516f3e7e389f0d3affcd3d3cff0a5d27207a72 Mon Sep 17 00:00:00 2001 From: William Rose Date: Thu, 14 Mar 2024 19:13:07 +0000 Subject: [PATCH 03/17] Added shape_sampling.rs with support for Circle, Sphere, Rectangle, Cuboid, Cylinder, Capsule2d --- crates/bevy_math/src/shape_sampling.rs | 197 +++++++++++++++++++++++++ 1 file changed, 197 insertions(+) create mode 100644 crates/bevy_math/src/shape_sampling.rs diff --git a/crates/bevy_math/src/shape_sampling.rs b/crates/bevy_math/src/shape_sampling.rs new file mode 100644 index 0000000000000..4731eecebc034 --- /dev/null +++ b/crates/bevy_math/src/shape_sampling.rs @@ -0,0 +1,197 @@ +use std::f32::consts::{PI, TAU}; + +use crate::{primitives::*, Vec2, Vec3}; +use rand::Rng; + +/// Exposes methods to uniformly sample a variety of primitive shapes. +pub trait ShapeSample { + /// The type of vector returned by the sample methods, [`Vec2`] for 2D shapes and [`Vec3`] for 3D shapes. + type Output; + + /// Uniformly sample a point from inside the area/volume of this shape, centered on 0. + /// + /// # Example + /// ``` + /// # use bevy_math::prelude::*; + /// let square = Rectangle::new(2.0, 2.0); + /// + /// // Returns a Vec2 with both x and y between -1 and 1. + /// println!("{:?}", square.sample_volume(&mut rand::thread_rng())); + /// ``` + fn sample_volume(&self, rng: &mut impl Rng) -> Self::Output; + + /// Uniformly sample a point from the surface of this shape, centered on 0. + /// + /// # Example + /// ``` + /// # use bevy_math::prelude::*; + /// let square = Rectangle::new(2.0, 2.0); + /// + /// // Returns a Vec2 where one of the coordinates is at ±1, + /// // and the other is somewhere between -1 and 1. + /// println!("{:?}", square.sample_surface(&mut rand::thread_rng())); + /// ``` + fn sample_surface(&self, rng: &mut impl Rng) -> Self::Output; +} + +impl ShapeSample for Circle { + type Output = Vec2; + + fn sample_volume(&self, rng: &mut impl Rng) -> Vec2 { + // https://mathworld.wolfram.com/DiskPointPicking.html + let theta = rng.gen_range(0.0..TAU); + let r_squared = rng.gen_range(0.0..=(self.radius * self.radius)); + let r = r_squared.sqrt(); + Vec2::new(r * theta.cos(), r * theta.sin()) + } + + fn sample_surface(&self, rng: &mut impl Rng) -> Vec2 { + let theta = rng.gen_range(0.0..TAU); + Vec2::new(self.radius * theta.cos(), self.radius * theta.sin()) + } +} + +impl ShapeSample for Sphere { + type Output = Vec3; + + fn sample_volume(&self, rng: &mut impl Rng) -> Vec3 { + // https://mathworld.wolfram.com/SpherePointPicking.html + let theta = rng.gen_range(0.0..TAU); + let phi = rng.gen_range(-1.0_f32..1.0).acos(); + let r_cubed = rng.gen_range(0.0..=(self.radius * self.radius * self.radius)); + let r = r_cubed.cbrt(); + Vec3 { + x: r * phi.sin() * theta.cos(), + y: r * phi.sin() * theta.sin(), + z: r * phi.cos(), + } + } + + fn sample_surface(&self, rng: &mut impl Rng) -> Vec3 { + let theta = rng.gen_range(0.0..TAU); + let phi = rng.gen_range(-1.0_f32..1.0).acos(); + Vec3 { + x: self.radius * phi.sin() * theta.cos(), + y: self.radius * phi.sin() * theta.sin(), + z: self.radius * phi.cos(), + } + } +} + +impl ShapeSample for Rectangle { + type Output = Vec2; + + fn sample_volume(&self, rng: &mut impl Rng) -> Vec2 { + let x = rng.gen_range(-self.half_size.x..=self.half_size.x); + let y = rng.gen_range(-self.half_size.y..=self.half_size.y); + Vec2::new(x, y) + } + + fn sample_surface(&self, rng: &mut impl Rng) -> Vec2 { + let side_distance = rng.gen_range(-1.0..1.0); + let other_side = rng.gen_range(0..=1) as f32 * 2.0 - 1.0; + + if rng.gen_ratio(1, 2) { + Vec2::new(side_distance, other_side) * self.half_size + } else { + Vec2::new(other_side, side_distance) * self.half_size + } + } +} + +impl ShapeSample for Cuboid { + type Output = Vec3; + + fn sample_volume(&self, rng: &mut impl Rng) -> Vec3 { + let x = rng.gen_range(-self.half_size.x..=self.half_size.x); + let y = rng.gen_range(-self.half_size.y..=self.half_size.y); + let z = rng.gen_range(-self.half_size.z..=self.half_size.z); + Vec3::new(x, y, z) + } + + fn sample_surface(&self, rng: &mut impl Rng) -> Vec3 { + let side_distance = rng.gen_range(-1.0..1.0); + let sides = rng.gen_range(0..4); + let other_side1 = (sides & 1) as f32 * 2.0 - 1.0; + let other_side2 = (sides & 2) as f32 * 2.0 - 1.0; + + match rng.gen_range(0..=2) { + 0 => Vec3::new(side_distance, other_side1, other_side2) * self.half_size, + 1 => Vec3::new(other_side1, side_distance, other_side2) * self.half_size, + 2 => Vec3::new(other_side1, other_side2, side_distance) * self.half_size, + _ => unreachable!(), + } + } +} + +impl ShapeSample for Cylinder { + type Output = Vec3; + + fn sample_volume(&self, rng: &mut impl Rng) -> Vec3 { + let Vec2 { x, y: z } = self.base().sample_volume(rng); + let y = rng.gen_range(-self.half_height..=self.half_height); + Vec3::new(x, y, z) + } + + fn sample_surface(&self, rng: &mut impl Rng) -> Vec3 { + // This uses the area of the ends divided by the overall surface area (optimised) + // [2 (\pi r^2)]/[2 (\pi r^2) + 2 \pi r h] = r/(r + h) + if rng.gen_bool((self.radius / (self.radius + 2.0 * self.half_height)) as f64) { + let Vec2 { x, y: z } = self.base().sample_volume(rng); + if rng.gen_ratio(1, 2) { + Vec3::new(x, self.half_height, z) + } else { + Vec3::new(x, -self.half_height, z) + } + } else { + let Vec2 { x, y: z } = self.base().sample_surface(rng); + let y = rng.gen_range(-self.half_height..=self.half_height); + Vec3::new(x, y, z) + } + } +} + +impl ShapeSample for Capsule2d { + type Output = Vec2; + + fn sample_volume(&self, rng: &mut impl Rng) -> Vec2 { + let rectangle_area = self.half_length * self.radius * 4.0; + let capsule_area = rectangle_area + PI * self.radius * self.radius; + // Check if the random point should be inside the rectangle + if rng.gen_bool((rectangle_area / capsule_area) as f64) { + let rectangle = Rectangle::new(self.radius, self.half_length * 2.0); + rectangle.sample_volume(rng) + } else { + let circle = Circle::new(self.radius); + let point = circle.sample_volume(rng); + // Add half length if it is the top semi-circle, otherwise subtract half + if point.y > 0.0 { + point + Vec2::Y * self.half_length + } else { + point - Vec2::Y * self.half_length + } + } + } + + fn sample_surface(&self, rng: &mut impl Rng) -> Vec2 { + let rectangle_surface = 4.0 * self.half_length; + let capsule_surface = rectangle_surface + TAU * self.radius; + if rng.gen_bool((rectangle_surface / capsule_surface) as f64) { + let side_distance = rng.gen_range((-2.0 * self.half_length)..=(2.0 * self.half_length)); + if side_distance < 0.0 { + Vec2::new(self.radius, side_distance + self.half_length) + } else { + Vec2::new(-self.radius, side_distance - self.half_length) + } + } else { + let circle = Circle::new(self.radius); + let point = circle.sample_surface(rng); + // Add half length if it is the top semi-circle, otherwise subtract half + if point.y > 0.0 { + point + Vec2::Y * self.half_length + } else { + point - Vec2::Y * self.half_length + } + } + } +} From efef007503225115df1c0d05de7047341c4663db Mon Sep 17 00:00:00 2001 From: William Rose Date: Thu, 14 Mar 2024 19:13:24 +0000 Subject: [PATCH 04/17] Added shape_sampling to bevy_math lib.rs --- crates/bevy_math/src/lib.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/crates/bevy_math/src/lib.rs b/crates/bevy_math/src/lib.rs index 604a299ab282c..0d76500703818 100644 --- a/crates/bevy_math/src/lib.rs +++ b/crates/bevy_math/src/lib.rs @@ -14,6 +14,8 @@ pub mod primitives; mod ray; mod rects; mod rotation2d; +#[cfg(feature = "rand")] +mod shape_sampling; pub use affine3::*; pub use aspect_ratio::AspectRatio; @@ -21,9 +23,14 @@ pub use direction::*; pub use ray::{Ray2d, Ray3d}; pub use rects::*; pub use rotation2d::Rotation2d; +#[cfg(feature = "rand")] +pub use shape_sampling::ShapeSample; /// The `bevy_math` prelude. pub mod prelude { + #[doc(hidden)] + #[cfg(feature = "rand")] + pub use crate::shape_sampling::ShapeSample; #[doc(hidden)] pub use crate::{ cubic_splines::{ From 26b1e5e40671979b2a69be90c52d2a6153c026b3 Mon Sep 17 00:00:00 2001 From: William Rose Date: Thu, 14 Mar 2024 21:10:17 +0000 Subject: [PATCH 05/17] Added ShapeSample for Capsule3d --- crates/bevy_math/src/shape_sampling.rs | 42 ++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/crates/bevy_math/src/shape_sampling.rs b/crates/bevy_math/src/shape_sampling.rs index 4731eecebc034..4298e28710779 100644 --- a/crates/bevy_math/src/shape_sampling.rs +++ b/crates/bevy_math/src/shape_sampling.rs @@ -195,3 +195,45 @@ impl ShapeSample for Capsule2d { } } } + +impl ShapeSample for Capsule3d { + type Output = Vec3; + + fn sample_volume(&self, rng: &mut impl Rng) -> Vec3 { + let cylinder_vol = PI * self.radius * self.radius * 2.0 * self.half_length; + // Add 4/3 pi r^3 + let capsule_vol = cylinder_vol + 4.0 / 3.0 * PI * self.radius * self.radius * self.radius; + // Check if the random point should be inside the cylinder + if rng.gen_bool((cylinder_vol / capsule_vol) as f64) { + self.to_cylinder().sample_volume(rng) + } else { + let sphere = Sphere::new(self.radius); + let point = sphere.sample_volume(rng); + // Add half length if it is the top semi-sphere, otherwise subtract half + if point.y > 0.0 { + point + Vec3::Y * self.half_length + } else { + point - Vec3::Y * self.half_length + } + } + } + + fn sample_surface(&self, rng: &mut impl Rng) -> Vec3 { + let cylinder_surface = TAU * self.radius * 2.0 * self.half_length; + let capsule_surface = cylinder_surface + 4.0 * PI * self.radius * self.radius; + if rng.gen_bool((cylinder_surface / capsule_surface) as f64) { + let Vec2 { x, y: z } = Circle::new(self.radius).sample_surface(rng); + let y = rng.gen_range(-self.half_length..=self.half_length); + Vec3::new(x, y, z) + } else { + let sphere = Sphere::new(self.radius); + let point = sphere.sample_surface(rng); + // Add half length if it is the top semi-sphere, otherwise subtract half + if point.y > 0.0 { + point + Vec3::Y * self.half_length + } else { + point - Vec3::Y * self.half_length + } + } + } +} From 118a6fccbcbd39142c8a9fbdec17c52524744786 Mon Sep 17 00:00:00 2001 From: William Rose Date: Thu, 14 Mar 2024 23:13:21 +0000 Subject: [PATCH 06/17] Added a ?Sized to all the rng arguments --- crates/bevy_math/src/shape_sampling.rs | 32 +++++++++++++------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/crates/bevy_math/src/shape_sampling.rs b/crates/bevy_math/src/shape_sampling.rs index 4298e28710779..a26f902c38763 100644 --- a/crates/bevy_math/src/shape_sampling.rs +++ b/crates/bevy_math/src/shape_sampling.rs @@ -18,7 +18,7 @@ pub trait ShapeSample { /// // Returns a Vec2 with both x and y between -1 and 1. /// println!("{:?}", square.sample_volume(&mut rand::thread_rng())); /// ``` - fn sample_volume(&self, rng: &mut impl Rng) -> Self::Output; + fn sample_volume(&self, rng: &mut R) -> Self::Output; /// Uniformly sample a point from the surface of this shape, centered on 0. /// @@ -31,13 +31,13 @@ pub trait ShapeSample { /// // and the other is somewhere between -1 and 1. /// println!("{:?}", square.sample_surface(&mut rand::thread_rng())); /// ``` - fn sample_surface(&self, rng: &mut impl Rng) -> Self::Output; + fn sample_surface(&self, rng: &mut R) -> Self::Output; } impl ShapeSample for Circle { type Output = Vec2; - fn sample_volume(&self, rng: &mut impl Rng) -> Vec2 { + fn sample_volume(&self, rng: &mut R) -> Vec2 { // https://mathworld.wolfram.com/DiskPointPicking.html let theta = rng.gen_range(0.0..TAU); let r_squared = rng.gen_range(0.0..=(self.radius * self.radius)); @@ -45,7 +45,7 @@ impl ShapeSample for Circle { Vec2::new(r * theta.cos(), r * theta.sin()) } - fn sample_surface(&self, rng: &mut impl Rng) -> Vec2 { + fn sample_surface(&self, rng: &mut R) -> Vec2 { let theta = rng.gen_range(0.0..TAU); Vec2::new(self.radius * theta.cos(), self.radius * theta.sin()) } @@ -54,7 +54,7 @@ impl ShapeSample for Circle { impl ShapeSample for Sphere { type Output = Vec3; - fn sample_volume(&self, rng: &mut impl Rng) -> Vec3 { + fn sample_volume(&self, rng: &mut R) -> Vec3 { // https://mathworld.wolfram.com/SpherePointPicking.html let theta = rng.gen_range(0.0..TAU); let phi = rng.gen_range(-1.0_f32..1.0).acos(); @@ -67,7 +67,7 @@ impl ShapeSample for Sphere { } } - fn sample_surface(&self, rng: &mut impl Rng) -> Vec3 { + fn sample_surface(&self, rng: &mut R) -> Vec3 { let theta = rng.gen_range(0.0..TAU); let phi = rng.gen_range(-1.0_f32..1.0).acos(); Vec3 { @@ -81,13 +81,13 @@ impl ShapeSample for Sphere { impl ShapeSample for Rectangle { type Output = Vec2; - fn sample_volume(&self, rng: &mut impl Rng) -> Vec2 { + fn sample_volume(&self, rng: &mut R) -> Vec2 { let x = rng.gen_range(-self.half_size.x..=self.half_size.x); let y = rng.gen_range(-self.half_size.y..=self.half_size.y); Vec2::new(x, y) } - fn sample_surface(&self, rng: &mut impl Rng) -> Vec2 { + fn sample_surface(&self, rng: &mut R) -> Vec2 { let side_distance = rng.gen_range(-1.0..1.0); let other_side = rng.gen_range(0..=1) as f32 * 2.0 - 1.0; @@ -102,14 +102,14 @@ impl ShapeSample for Rectangle { impl ShapeSample for Cuboid { type Output = Vec3; - fn sample_volume(&self, rng: &mut impl Rng) -> Vec3 { + fn sample_volume(&self, rng: &mut R) -> Vec3 { let x = rng.gen_range(-self.half_size.x..=self.half_size.x); let y = rng.gen_range(-self.half_size.y..=self.half_size.y); let z = rng.gen_range(-self.half_size.z..=self.half_size.z); Vec3::new(x, y, z) } - fn sample_surface(&self, rng: &mut impl Rng) -> Vec3 { + fn sample_surface(&self, rng: &mut R) -> Vec3 { let side_distance = rng.gen_range(-1.0..1.0); let sides = rng.gen_range(0..4); let other_side1 = (sides & 1) as f32 * 2.0 - 1.0; @@ -127,13 +127,13 @@ impl ShapeSample for Cuboid { impl ShapeSample for Cylinder { type Output = Vec3; - fn sample_volume(&self, rng: &mut impl Rng) -> Vec3 { + fn sample_volume(&self, rng: &mut R) -> Vec3 { let Vec2 { x, y: z } = self.base().sample_volume(rng); let y = rng.gen_range(-self.half_height..=self.half_height); Vec3::new(x, y, z) } - fn sample_surface(&self, rng: &mut impl Rng) -> Vec3 { + fn sample_surface(&self, rng: &mut R) -> Vec3 { // This uses the area of the ends divided by the overall surface area (optimised) // [2 (\pi r^2)]/[2 (\pi r^2) + 2 \pi r h] = r/(r + h) if rng.gen_bool((self.radius / (self.radius + 2.0 * self.half_height)) as f64) { @@ -154,7 +154,7 @@ impl ShapeSample for Cylinder { impl ShapeSample for Capsule2d { type Output = Vec2; - fn sample_volume(&self, rng: &mut impl Rng) -> Vec2 { + fn sample_volume(&self, rng: &mut R) -> Vec2 { let rectangle_area = self.half_length * self.radius * 4.0; let capsule_area = rectangle_area + PI * self.radius * self.radius; // Check if the random point should be inside the rectangle @@ -173,7 +173,7 @@ impl ShapeSample for Capsule2d { } } - fn sample_surface(&self, rng: &mut impl Rng) -> Vec2 { + fn sample_surface(&self, rng: &mut R) -> Vec2 { let rectangle_surface = 4.0 * self.half_length; let capsule_surface = rectangle_surface + TAU * self.radius; if rng.gen_bool((rectangle_surface / capsule_surface) as f64) { @@ -199,7 +199,7 @@ impl ShapeSample for Capsule2d { impl ShapeSample for Capsule3d { type Output = Vec3; - fn sample_volume(&self, rng: &mut impl Rng) -> Vec3 { + fn sample_volume(&self, rng: &mut R) -> Vec3 { let cylinder_vol = PI * self.radius * self.radius * 2.0 * self.half_length; // Add 4/3 pi r^3 let capsule_vol = cylinder_vol + 4.0 / 3.0 * PI * self.radius * self.radius * self.radius; @@ -218,7 +218,7 @@ impl ShapeSample for Capsule3d { } } - fn sample_surface(&self, rng: &mut impl Rng) -> Vec3 { + fn sample_surface(&self, rng: &mut R) -> Vec3 { let cylinder_surface = TAU * self.radius * 2.0 * self.half_length; let capsule_surface = cylinder_surface + 4.0 * PI * self.radius * self.radius; if rng.gen_bool((cylinder_surface / capsule_surface) as f64) { From 2f9b4ca78a13807dfa70bcbd9d56291beb0a10a7 Mon Sep 17 00:00:00 2001 From: William Rose Date: Fri, 15 Mar 2024 11:33:36 +0000 Subject: [PATCH 07/17] Changed from the weird `gen_range(0..=1) as f32 * 2.0 - 1.0` to `if gen_ratio(1, 2) { ...` --- crates/bevy_math/src/shape_sampling.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/crates/bevy_math/src/shape_sampling.rs b/crates/bevy_math/src/shape_sampling.rs index a26f902c38763..87391e889a647 100644 --- a/crates/bevy_math/src/shape_sampling.rs +++ b/crates/bevy_math/src/shape_sampling.rs @@ -89,7 +89,7 @@ impl ShapeSample for Rectangle { fn sample_surface(&self, rng: &mut R) -> Vec2 { let side_distance = rng.gen_range(-1.0..1.0); - let other_side = rng.gen_range(0..=1) as f32 * 2.0 - 1.0; + let other_side = if rng.gen_ratio(1, 2) { -1.0 } else { 1.0 }; if rng.gen_ratio(1, 2) { Vec2::new(side_distance, other_side) * self.half_size @@ -111,9 +111,8 @@ impl ShapeSample for Cuboid { fn sample_surface(&self, rng: &mut R) -> Vec3 { let side_distance = rng.gen_range(-1.0..1.0); - let sides = rng.gen_range(0..4); - let other_side1 = (sides & 1) as f32 * 2.0 - 1.0; - let other_side2 = (sides & 2) as f32 * 2.0 - 1.0; + let other_side1 = if rng.gen_ratio(1, 2) { -1.0 } else { 1.0 }; + let other_side2 = if rng.gen_ratio(1, 2) { -1.0 } else { 1.0 }; match rng.gen_range(0..=2) { 0 => Vec3::new(side_distance, other_side1, other_side2) * self.half_size, From ff447676cb2ad2b92cd28c89f6368456b308d8f0 Mon Sep 17 00:00:00 2001 From: William Rose Date: Fri, 15 Mar 2024 11:57:00 +0000 Subject: [PATCH 08/17] Change `gen_ratio(1, 2)` to `gen()` --- crates/bevy_math/src/shape_sampling.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/bevy_math/src/shape_sampling.rs b/crates/bevy_math/src/shape_sampling.rs index 87391e889a647..8439498ec25d7 100644 --- a/crates/bevy_math/src/shape_sampling.rs +++ b/crates/bevy_math/src/shape_sampling.rs @@ -89,9 +89,9 @@ impl ShapeSample for Rectangle { fn sample_surface(&self, rng: &mut R) -> Vec2 { let side_distance = rng.gen_range(-1.0..1.0); - let other_side = if rng.gen_ratio(1, 2) { -1.0 } else { 1.0 }; + let other_side = if rng.gen() { -1.0 } else { 1.0 }; - if rng.gen_ratio(1, 2) { + if rng.gen() { Vec2::new(side_distance, other_side) * self.half_size } else { Vec2::new(other_side, side_distance) * self.half_size @@ -111,8 +111,8 @@ impl ShapeSample for Cuboid { fn sample_surface(&self, rng: &mut R) -> Vec3 { let side_distance = rng.gen_range(-1.0..1.0); - let other_side1 = if rng.gen_ratio(1, 2) { -1.0 } else { 1.0 }; - let other_side2 = if rng.gen_ratio(1, 2) { -1.0 } else { 1.0 }; + let other_side1 = if rng.gen() { -1.0 } else { 1.0 }; + let other_side2 = if rng.gen() { -1.0 } else { 1.0 }; match rng.gen_range(0..=2) { 0 => Vec3::new(side_distance, other_side1, other_side2) * self.half_size, @@ -137,7 +137,7 @@ impl ShapeSample for Cylinder { // [2 (\pi r^2)]/[2 (\pi r^2) + 2 \pi r h] = r/(r + h) if rng.gen_bool((self.radius / (self.radius + 2.0 * self.half_height)) as f64) { let Vec2 { x, y: z } = self.base().sample_volume(rng); - if rng.gen_ratio(1, 2) { + if rng.gen() { Vec3::new(x, self.half_height, z) } else { Vec3::new(x, -self.half_height, z) From ac91a7aac3defc41a90b252633a3a2fea1dc7ff4 Mon Sep 17 00:00:00 2001 From: William Rose Date: Fri, 15 Mar 2024 12:37:41 +0000 Subject: [PATCH 09/17] Add the `alloc` feature to rand so that we get access to distributions --- crates/bevy_math/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_math/Cargo.toml b/crates/bevy_math/Cargo.toml index 76f714a71edf9..c7a2be52c0bf5 100644 --- a/crates/bevy_math/Cargo.toml +++ b/crates/bevy_math/Cargo.toml @@ -14,7 +14,7 @@ thiserror = "1.0" serde = { version = "1", features = ["derive"], optional = true } libm = { version = "0.2", optional = true } approx = { version = "0.5", optional = true } -rand = { version = "0.8", default-features = false, optional = true } +rand = { version = "0.8", features = ["alloc"], default-features = false, optional = true } [dev-dependencies] approx = "0.5" From 5c9537bf8cc55116832f7e3cda77ab4e1ceaccff Mon Sep 17 00:00:00 2001 From: William Rose Date: Fri, 15 Mar 2024 12:38:36 +0000 Subject: [PATCH 10/17] Fixed rectangle and cuboid sampling --- crates/bevy_math/src/shape_sampling.rs | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/crates/bevy_math/src/shape_sampling.rs b/crates/bevy_math/src/shape_sampling.rs index 8439498ec25d7..e5ef4c6bfe0d9 100644 --- a/crates/bevy_math/src/shape_sampling.rs +++ b/crates/bevy_math/src/shape_sampling.rs @@ -1,7 +1,10 @@ use std::f32::consts::{PI, TAU}; use crate::{primitives::*, Vec2, Vec3}; -use rand::Rng; +use rand::{ + distributions::{Distribution, WeightedIndex}, + Rng, +}; /// Exposes methods to uniformly sample a variety of primitive shapes. pub trait ShapeSample { @@ -88,13 +91,13 @@ impl ShapeSample for Rectangle { } fn sample_surface(&self, rng: &mut R) -> Vec2 { - let side_distance = rng.gen_range(-1.0..1.0); + let primary_side = rng.gen_range(-1.0..1.0); let other_side = if rng.gen() { -1.0 } else { 1.0 }; - if rng.gen() { - Vec2::new(side_distance, other_side) * self.half_size + if rng.gen_bool((self.half_size.x / (self.half_size.x + self.half_size.y)) as f64) { + Vec2::new(primary_side, other_side) * self.half_size } else { - Vec2::new(other_side, side_distance) * self.half_size + Vec2::new(other_side, primary_side) * self.half_size } } } @@ -110,14 +113,15 @@ impl ShapeSample for Cuboid { } fn sample_surface(&self, rng: &mut R) -> Vec3 { - let side_distance = rng.gen_range(-1.0..1.0); + let primary_side = rng.gen_range(-1.0..1.0); let other_side1 = if rng.gen() { -1.0 } else { 1.0 }; let other_side2 = if rng.gen() { -1.0 } else { 1.0 }; - match rng.gen_range(0..=2) { - 0 => Vec3::new(side_distance, other_side1, other_side2) * self.half_size, - 1 => Vec3::new(other_side1, side_distance, other_side2) * self.half_size, - 2 => Vec3::new(other_side1, other_side2, side_distance) * self.half_size, + let dist = WeightedIndex::new(self.half_size.to_array()).unwrap(); + match dist.sample(rng) { + 0 => Vec3::new(primary_side, other_side1, other_side2) * self.half_size, + 1 => Vec3::new(other_side1, primary_side, other_side2) * self.half_size, + 2 => Vec3::new(other_side1, other_side2, primary_side) * self.half_size, _ => unreachable!(), } } From 0ac8d0693c92b96c8081748e5132022f799af171 Mon Sep 17 00:00:00 2001 From: William Rose Date: Fri, 15 Mar 2024 12:48:17 +0000 Subject: [PATCH 11/17] Appease taplo (this seems considerably worse formatted to me?) --- crates/bevy_math/Cargo.toml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/bevy_math/Cargo.toml b/crates/bevy_math/Cargo.toml index c7a2be52c0bf5..d17a989c13fc9 100644 --- a/crates/bevy_math/Cargo.toml +++ b/crates/bevy_math/Cargo.toml @@ -14,7 +14,9 @@ thiserror = "1.0" serde = { version = "1", features = ["derive"], optional = true } libm = { version = "0.2", optional = true } approx = { version = "0.5", optional = true } -rand = { version = "0.8", features = ["alloc"], default-features = false, optional = true } +rand = { version = "0.8", features = [ + "alloc", +], default-features = false, optional = true } [dev-dependencies] approx = "0.5" From b314ca8b28c0a9214f5bc76aa0f38bfa8ef152ac Mon Sep 17 00:00:00 2001 From: William Rose Date: Fri, 15 Mar 2024 12:48:56 +0000 Subject: [PATCH 12/17] Added documentation specifying the direction ambiguous shapes point in --- crates/bevy_math/src/shape_sampling.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/bevy_math/src/shape_sampling.rs b/crates/bevy_math/src/shape_sampling.rs index e5ef4c6bfe0d9..d4bf3ab659449 100644 --- a/crates/bevy_math/src/shape_sampling.rs +++ b/crates/bevy_math/src/shape_sampling.rs @@ -13,6 +13,8 @@ pub trait ShapeSample { /// Uniformly sample a point from inside the area/volume of this shape, centered on 0. /// + /// Shapes like [`Cylinder`], [`Capsule2d`] and [`Capsule3d`] are oriented along the y-axis. + /// /// # Example /// ``` /// # use bevy_math::prelude::*; @@ -25,6 +27,8 @@ pub trait ShapeSample { /// Uniformly sample a point from the surface of this shape, centered on 0. /// + /// Shapes like [`Cylinder`], [`Capsule2d`] and [`Capsule3d`] are oriented along the y-axis. + /// /// # Example /// ``` /// # use bevy_math::prelude::*; From 4679fbe0915ddaa1a893d9101530c5d5697aa8ac Mon Sep 17 00:00:00 2001 From: William Rose Date: Fri, 15 Mar 2024 13:17:49 +0000 Subject: [PATCH 13/17] Rename `sample_volume` to `sample_interior` and `sample_surface` to `sample_boundary` --- crates/bevy_math/src/shape_sampling.rs | 56 +++++++++++++------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/crates/bevy_math/src/shape_sampling.rs b/crates/bevy_math/src/shape_sampling.rs index d4bf3ab659449..caa34d14da27a 100644 --- a/crates/bevy_math/src/shape_sampling.rs +++ b/crates/bevy_math/src/shape_sampling.rs @@ -21,9 +21,9 @@ pub trait ShapeSample { /// let square = Rectangle::new(2.0, 2.0); /// /// // Returns a Vec2 with both x and y between -1 and 1. - /// println!("{:?}", square.sample_volume(&mut rand::thread_rng())); + /// println!("{:?}", square.sample_interior(&mut rand::thread_rng())); /// ``` - fn sample_volume(&self, rng: &mut R) -> Self::Output; + fn sample_interior(&self, rng: &mut R) -> Self::Output; /// Uniformly sample a point from the surface of this shape, centered on 0. /// @@ -36,15 +36,15 @@ pub trait ShapeSample { /// /// // Returns a Vec2 where one of the coordinates is at ±1, /// // and the other is somewhere between -1 and 1. - /// println!("{:?}", square.sample_surface(&mut rand::thread_rng())); + /// println!("{:?}", square.sample_boundary(&mut rand::thread_rng())); /// ``` - fn sample_surface(&self, rng: &mut R) -> Self::Output; + fn sample_boundary(&self, rng: &mut R) -> Self::Output; } impl ShapeSample for Circle { type Output = Vec2; - fn sample_volume(&self, rng: &mut R) -> Vec2 { + fn sample_interior(&self, rng: &mut R) -> Vec2 { // https://mathworld.wolfram.com/DiskPointPicking.html let theta = rng.gen_range(0.0..TAU); let r_squared = rng.gen_range(0.0..=(self.radius * self.radius)); @@ -52,7 +52,7 @@ impl ShapeSample for Circle { Vec2::new(r * theta.cos(), r * theta.sin()) } - fn sample_surface(&self, rng: &mut R) -> Vec2 { + fn sample_boundary(&self, rng: &mut R) -> Vec2 { let theta = rng.gen_range(0.0..TAU); Vec2::new(self.radius * theta.cos(), self.radius * theta.sin()) } @@ -61,7 +61,7 @@ impl ShapeSample for Circle { impl ShapeSample for Sphere { type Output = Vec3; - fn sample_volume(&self, rng: &mut R) -> Vec3 { + fn sample_interior(&self, rng: &mut R) -> Vec3 { // https://mathworld.wolfram.com/SpherePointPicking.html let theta = rng.gen_range(0.0..TAU); let phi = rng.gen_range(-1.0_f32..1.0).acos(); @@ -74,7 +74,7 @@ impl ShapeSample for Sphere { } } - fn sample_surface(&self, rng: &mut R) -> Vec3 { + fn sample_boundary(&self, rng: &mut R) -> Vec3 { let theta = rng.gen_range(0.0..TAU); let phi = rng.gen_range(-1.0_f32..1.0).acos(); Vec3 { @@ -88,13 +88,13 @@ impl ShapeSample for Sphere { impl ShapeSample for Rectangle { type Output = Vec2; - fn sample_volume(&self, rng: &mut R) -> Vec2 { + fn sample_interior(&self, rng: &mut R) -> Vec2 { let x = rng.gen_range(-self.half_size.x..=self.half_size.x); let y = rng.gen_range(-self.half_size.y..=self.half_size.y); Vec2::new(x, y) } - fn sample_surface(&self, rng: &mut R) -> Vec2 { + fn sample_boundary(&self, rng: &mut R) -> Vec2 { let primary_side = rng.gen_range(-1.0..1.0); let other_side = if rng.gen() { -1.0 } else { 1.0 }; @@ -109,14 +109,14 @@ impl ShapeSample for Rectangle { impl ShapeSample for Cuboid { type Output = Vec3; - fn sample_volume(&self, rng: &mut R) -> Vec3 { + fn sample_interior(&self, rng: &mut R) -> Vec3 { let x = rng.gen_range(-self.half_size.x..=self.half_size.x); let y = rng.gen_range(-self.half_size.y..=self.half_size.y); let z = rng.gen_range(-self.half_size.z..=self.half_size.z); Vec3::new(x, y, z) } - fn sample_surface(&self, rng: &mut R) -> Vec3 { + fn sample_boundary(&self, rng: &mut R) -> Vec3 { let primary_side = rng.gen_range(-1.0..1.0); let other_side1 = if rng.gen() { -1.0 } else { 1.0 }; let other_side2 = if rng.gen() { -1.0 } else { 1.0 }; @@ -134,24 +134,24 @@ impl ShapeSample for Cuboid { impl ShapeSample for Cylinder { type Output = Vec3; - fn sample_volume(&self, rng: &mut R) -> Vec3 { - let Vec2 { x, y: z } = self.base().sample_volume(rng); + fn sample_interior(&self, rng: &mut R) -> Vec3 { + let Vec2 { x, y: z } = self.base().sample_interior(rng); let y = rng.gen_range(-self.half_height..=self.half_height); Vec3::new(x, y, z) } - fn sample_surface(&self, rng: &mut R) -> Vec3 { + fn sample_boundary(&self, rng: &mut R) -> Vec3 { // This uses the area of the ends divided by the overall surface area (optimised) // [2 (\pi r^2)]/[2 (\pi r^2) + 2 \pi r h] = r/(r + h) if rng.gen_bool((self.radius / (self.radius + 2.0 * self.half_height)) as f64) { - let Vec2 { x, y: z } = self.base().sample_volume(rng); + let Vec2 { x, y: z } = self.base().sample_interior(rng); if rng.gen() { Vec3::new(x, self.half_height, z) } else { Vec3::new(x, -self.half_height, z) } } else { - let Vec2 { x, y: z } = self.base().sample_surface(rng); + let Vec2 { x, y: z } = self.base().sample_boundary(rng); let y = rng.gen_range(-self.half_height..=self.half_height); Vec3::new(x, y, z) } @@ -161,16 +161,16 @@ impl ShapeSample for Cylinder { impl ShapeSample for Capsule2d { type Output = Vec2; - fn sample_volume(&self, rng: &mut R) -> Vec2 { + fn sample_interior(&self, rng: &mut R) -> Vec2 { let rectangle_area = self.half_length * self.radius * 4.0; let capsule_area = rectangle_area + PI * self.radius * self.radius; // Check if the random point should be inside the rectangle if rng.gen_bool((rectangle_area / capsule_area) as f64) { let rectangle = Rectangle::new(self.radius, self.half_length * 2.0); - rectangle.sample_volume(rng) + rectangle.sample_interior(rng) } else { let circle = Circle::new(self.radius); - let point = circle.sample_volume(rng); + let point = circle.sample_interior(rng); // Add half length if it is the top semi-circle, otherwise subtract half if point.y > 0.0 { point + Vec2::Y * self.half_length @@ -180,7 +180,7 @@ impl ShapeSample for Capsule2d { } } - fn sample_surface(&self, rng: &mut R) -> Vec2 { + fn sample_boundary(&self, rng: &mut R) -> Vec2 { let rectangle_surface = 4.0 * self.half_length; let capsule_surface = rectangle_surface + TAU * self.radius; if rng.gen_bool((rectangle_surface / capsule_surface) as f64) { @@ -192,7 +192,7 @@ impl ShapeSample for Capsule2d { } } else { let circle = Circle::new(self.radius); - let point = circle.sample_surface(rng); + let point = circle.sample_boundary(rng); // Add half length if it is the top semi-circle, otherwise subtract half if point.y > 0.0 { point + Vec2::Y * self.half_length @@ -206,16 +206,16 @@ impl ShapeSample for Capsule2d { impl ShapeSample for Capsule3d { type Output = Vec3; - fn sample_volume(&self, rng: &mut R) -> Vec3 { + fn sample_interior(&self, rng: &mut R) -> Vec3 { let cylinder_vol = PI * self.radius * self.radius * 2.0 * self.half_length; // Add 4/3 pi r^3 let capsule_vol = cylinder_vol + 4.0 / 3.0 * PI * self.radius * self.radius * self.radius; // Check if the random point should be inside the cylinder if rng.gen_bool((cylinder_vol / capsule_vol) as f64) { - self.to_cylinder().sample_volume(rng) + self.to_cylinder().sample_interior(rng) } else { let sphere = Sphere::new(self.radius); - let point = sphere.sample_volume(rng); + let point = sphere.sample_interior(rng); // Add half length if it is the top semi-sphere, otherwise subtract half if point.y > 0.0 { point + Vec3::Y * self.half_length @@ -225,16 +225,16 @@ impl ShapeSample for Capsule3d { } } - fn sample_surface(&self, rng: &mut R) -> Vec3 { + fn sample_boundary(&self, rng: &mut R) -> Vec3 { let cylinder_surface = TAU * self.radius * 2.0 * self.half_length; let capsule_surface = cylinder_surface + 4.0 * PI * self.radius * self.radius; if rng.gen_bool((cylinder_surface / capsule_surface) as f64) { - let Vec2 { x, y: z } = Circle::new(self.radius).sample_surface(rng); + let Vec2 { x, y: z } = Circle::new(self.radius).sample_boundary(rng); let y = rng.gen_range(-self.half_length..=self.half_length); Vec3::new(x, y, z) } else { let sphere = Sphere::new(self.radius); - let point = sphere.sample_surface(rng); + let point = sphere.sample_boundary(rng); // Add half length if it is the top semi-sphere, otherwise subtract half if point.y > 0.0 { point + Vec3::Y * self.half_length From 2f8a5a59f083eca68f99ec23d6312c2ba0b27bee Mon Sep 17 00:00:00 2001 From: William Rose Date: Fri, 15 Mar 2024 14:17:45 +0000 Subject: [PATCH 14/17] Added a test for how uniform Circle.sample_interior and Circle.sample_boundary are --- crates/bevy_math/src/shape_sampling.rs | 66 ++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/crates/bevy_math/src/shape_sampling.rs b/crates/bevy_math/src/shape_sampling.rs index caa34d14da27a..7bea38718cf50 100644 --- a/crates/bevy_math/src/shape_sampling.rs +++ b/crates/bevy_math/src/shape_sampling.rs @@ -244,3 +244,69 @@ impl ShapeSample for Capsule3d { } } } + +#[cfg(test)] +mod tests { + use super::*; + use rand::{rngs::StdRng, SeedableRng}; + + #[test] + fn circle_interior_sampling() { + let mut rng = StdRng::from_seed(Default::default()); + let circle = Circle::new(8.0); + + let boxes = [ + (-3.0, 3.0), + (1.0, 2.0), + (-1.0, -2.0), + (3.0, -2.0), + (1.0, -6.0), + (-3.0, -7.0), + (-7.0, -3.0), + (-6.0, 1.0), + ]; + let mut box_hits = [0; 8]; + + // Checks which boxes (if any) the sampled points are in + for _ in 0..5000 { + let point = circle.sample_interior(&mut rng); + + for (i, box_) in boxes.iter().enumerate() { + if (point.x > box_.0 && point.x < box_.0 + 4.0) + && (point.y > box_.1 && point.y < box_.1 + 4.0) + { + box_hits[i] += 1; + } + } + } + + assert_eq!( + box_hits, + [400, 367, 365, 394, 445, 417, 405, 382], + "samples will occur across all array items at statistically equal chance" + ); + } + + #[test] + fn circle_boundary_sampling() { + let mut rng = StdRng::from_seed(Default::default()); + let circle = Circle::new(1.0); + + let mut wedge_hits = [0; 8]; + + // Checks in which eighth of the circle each sampled point is in + for _ in 0..5000 { + let point = circle.sample_boundary(&mut rng); + + let angle = f32::atan(point.y / point.x) + PI / 2.0; + let wedge = (angle * 8.0 / PI).floor() as usize; + wedge_hits[wedge] += 1; + } + + assert_eq!( + wedge_hits, + [677, 638, 649, 625, 594, 624, 600, 593], + "samples will occur across all array items at statistically equal chance" + ); + } +} From 7b19264cebb53d20d1c6577fa57ca97140e8377b Mon Sep 17 00:00:00 2001 From: William Rose Date: Fri, 15 Mar 2024 14:28:25 +0000 Subject: [PATCH 15/17] Switch to `ChaCha8Rng` from `StdRng` for stability --- crates/bevy_math/Cargo.toml | 3 ++- crates/bevy_math/src/shape_sampling.rs | 11 ++++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/crates/bevy_math/Cargo.toml b/crates/bevy_math/Cargo.toml index d17a989c13fc9..3423759ebec8c 100644 --- a/crates/bevy_math/Cargo.toml +++ b/crates/bevy_math/Cargo.toml @@ -20,8 +20,9 @@ rand = { version = "0.8", features = [ [dev-dependencies] approx = "0.5" -# Enable the use of thread_rng in examples +# Supply rngs for examples and tests rand = "0.8" +rand_chacha = "0.3" [features] default = ["rand"] diff --git a/crates/bevy_math/src/shape_sampling.rs b/crates/bevy_math/src/shape_sampling.rs index 7bea38718cf50..31a50e102be3c 100644 --- a/crates/bevy_math/src/shape_sampling.rs +++ b/crates/bevy_math/src/shape_sampling.rs @@ -248,11 +248,12 @@ impl ShapeSample for Capsule3d { #[cfg(test)] mod tests { use super::*; - use rand::{rngs::StdRng, SeedableRng}; + use rand::SeedableRng; + use rand_chacha::ChaCha8Rng; #[test] fn circle_interior_sampling() { - let mut rng = StdRng::from_seed(Default::default()); + let mut rng = ChaCha8Rng::from_seed(Default::default()); let circle = Circle::new(8.0); let boxes = [ @@ -282,14 +283,14 @@ mod tests { assert_eq!( box_hits, - [400, 367, 365, 394, 445, 417, 405, 382], + [396, 377, 415, 404, 366, 408, 408, 430], "samples will occur across all array items at statistically equal chance" ); } #[test] fn circle_boundary_sampling() { - let mut rng = StdRng::from_seed(Default::default()); + let mut rng = ChaCha8Rng::from_seed(Default::default()); let circle = Circle::new(1.0); let mut wedge_hits = [0; 8]; @@ -305,7 +306,7 @@ mod tests { assert_eq!( wedge_hits, - [677, 638, 649, 625, 594, 624, 600, 593], + [636, 608, 639, 603, 614, 650, 640, 610], "samples will occur across all array items at statistically equal chance" ); } From 54890a05fe591993e4db0a450ea96e33060bb7b0 Mon Sep 17 00:00:00 2001 From: William Rose Date: Fri, 15 Mar 2024 15:16:35 +0000 Subject: [PATCH 16/17] Fixed the implementation for Cuboid::sample_boundary again --- crates/bevy_math/src/shape_sampling.rs | 27 ++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/crates/bevy_math/src/shape_sampling.rs b/crates/bevy_math/src/shape_sampling.rs index 31a50e102be3c..b74588a1c00e2 100644 --- a/crates/bevy_math/src/shape_sampling.rs +++ b/crates/bevy_math/src/shape_sampling.rs @@ -117,16 +117,23 @@ impl ShapeSample for Cuboid { } fn sample_boundary(&self, rng: &mut R) -> Vec3 { - let primary_side = rng.gen_range(-1.0..1.0); - let other_side1 = if rng.gen() { -1.0 } else { 1.0 }; - let other_side2 = if rng.gen() { -1.0 } else { 1.0 }; - - let dist = WeightedIndex::new(self.half_size.to_array()).unwrap(); - match dist.sample(rng) { - 0 => Vec3::new(primary_side, other_side1, other_side2) * self.half_size, - 1 => Vec3::new(other_side1, primary_side, other_side2) * self.half_size, - 2 => Vec3::new(other_side1, other_side2, primary_side) * self.half_size, - _ => unreachable!(), + let primary_side1 = rng.gen_range(-1.0..1.0); + let primary_side2 = rng.gen_range(-1.0..1.0); + let other_side = if rng.gen() { -1.0 } else { 1.0 }; + + if let Ok(dist) = WeightedIndex::new([ + self.half_size.y * self.half_size.z, + self.half_size.x * self.half_size.z, + self.half_size.x * self.half_size.y, + ]) { + match dist.sample(rng) { + 0 => Vec3::new(other_side, primary_side1, primary_side2) * self.half_size, + 1 => Vec3::new(primary_side1, other_side, primary_side2) * self.half_size, + 2 => Vec3::new(primary_side1, primary_side2, other_side) * self.half_size, + _ => unreachable!(), + } + } else { + Vec3::ZERO } } } From f74d0a4460d4521cc49a38d23f309882b4c2507f Mon Sep 17 00:00:00 2001 From: William Rose Date: Fri, 15 Mar 2024 15:22:07 +0000 Subject: [PATCH 17/17] Added checks to make it return VecN::ZERO rather than panicking with degenerate shapes --- crates/bevy_math/src/shape_sampling.rs | 137 +++++++++++++++---------- 1 file changed, 81 insertions(+), 56 deletions(-) diff --git a/crates/bevy_math/src/shape_sampling.rs b/crates/bevy_math/src/shape_sampling.rs index b74588a1c00e2..eff4a0898f37b 100644 --- a/crates/bevy_math/src/shape_sampling.rs +++ b/crates/bevy_math/src/shape_sampling.rs @@ -98,10 +98,14 @@ impl ShapeSample for Rectangle { let primary_side = rng.gen_range(-1.0..1.0); let other_side = if rng.gen() { -1.0 } else { 1.0 }; - if rng.gen_bool((self.half_size.x / (self.half_size.x + self.half_size.y)) as f64) { - Vec2::new(primary_side, other_side) * self.half_size + if self.half_size.x + self.half_size.y > 0.0 { + if rng.gen_bool((self.half_size.x / (self.half_size.x + self.half_size.y)) as f64) { + Vec2::new(primary_side, other_side) * self.half_size + } else { + Vec2::new(other_side, primary_side) * self.half_size + } } else { - Vec2::new(other_side, primary_side) * self.half_size + Vec2::ZERO } } } @@ -150,17 +154,21 @@ impl ShapeSample for Cylinder { fn sample_boundary(&self, rng: &mut R) -> Vec3 { // This uses the area of the ends divided by the overall surface area (optimised) // [2 (\pi r^2)]/[2 (\pi r^2) + 2 \pi r h] = r/(r + h) - if rng.gen_bool((self.radius / (self.radius + 2.0 * self.half_height)) as f64) { - let Vec2 { x, y: z } = self.base().sample_interior(rng); - if rng.gen() { - Vec3::new(x, self.half_height, z) + if self.radius + 2.0 * self.half_height > 0.0 { + if rng.gen_bool((self.radius / (self.radius + 2.0 * self.half_height)) as f64) { + let Vec2 { x, y: z } = self.base().sample_interior(rng); + if rng.gen() { + Vec3::new(x, self.half_height, z) + } else { + Vec3::new(x, -self.half_height, z) + } } else { - Vec3::new(x, -self.half_height, z) + let Vec2 { x, y: z } = self.base().sample_boundary(rng); + let y = rng.gen_range(-self.half_height..=self.half_height); + Vec3::new(x, y, z) } } else { - let Vec2 { x, y: z } = self.base().sample_boundary(rng); - let y = rng.gen_range(-self.half_height..=self.half_height); - Vec3::new(x, y, z) + Vec3::ZERO } } } @@ -171,41 +179,50 @@ impl ShapeSample for Capsule2d { fn sample_interior(&self, rng: &mut R) -> Vec2 { let rectangle_area = self.half_length * self.radius * 4.0; let capsule_area = rectangle_area + PI * self.radius * self.radius; - // Check if the random point should be inside the rectangle - if rng.gen_bool((rectangle_area / capsule_area) as f64) { - let rectangle = Rectangle::new(self.radius, self.half_length * 2.0); - rectangle.sample_interior(rng) - } else { - let circle = Circle::new(self.radius); - let point = circle.sample_interior(rng); - // Add half length if it is the top semi-circle, otherwise subtract half - if point.y > 0.0 { - point + Vec2::Y * self.half_length + if capsule_area > 0.0 { + // Check if the random point should be inside the rectangle + if rng.gen_bool((rectangle_area / capsule_area) as f64) { + let rectangle = Rectangle::new(self.radius, self.half_length * 2.0); + rectangle.sample_interior(rng) } else { - point - Vec2::Y * self.half_length + let circle = Circle::new(self.radius); + let point = circle.sample_interior(rng); + // Add half length if it is the top semi-circle, otherwise subtract half + if point.y > 0.0 { + point + Vec2::Y * self.half_length + } else { + point - Vec2::Y * self.half_length + } } + } else { + Vec2::ZERO } } fn sample_boundary(&self, rng: &mut R) -> Vec2 { let rectangle_surface = 4.0 * self.half_length; let capsule_surface = rectangle_surface + TAU * self.radius; - if rng.gen_bool((rectangle_surface / capsule_surface) as f64) { - let side_distance = rng.gen_range((-2.0 * self.half_length)..=(2.0 * self.half_length)); - if side_distance < 0.0 { - Vec2::new(self.radius, side_distance + self.half_length) + if capsule_surface > 0.0 { + if rng.gen_bool((rectangle_surface / capsule_surface) as f64) { + let side_distance = + rng.gen_range((-2.0 * self.half_length)..=(2.0 * self.half_length)); + if side_distance < 0.0 { + Vec2::new(self.radius, side_distance + self.half_length) + } else { + Vec2::new(-self.radius, side_distance - self.half_length) + } } else { - Vec2::new(-self.radius, side_distance - self.half_length) + let circle = Circle::new(self.radius); + let point = circle.sample_boundary(rng); + // Add half length if it is the top semi-circle, otherwise subtract half + if point.y > 0.0 { + point + Vec2::Y * self.half_length + } else { + point - Vec2::Y * self.half_length + } } } else { - let circle = Circle::new(self.radius); - let point = circle.sample_boundary(rng); - // Add half length if it is the top semi-circle, otherwise subtract half - if point.y > 0.0 { - point + Vec2::Y * self.half_length - } else { - point - Vec2::Y * self.half_length - } + Vec2::ZERO } } } @@ -217,37 +234,45 @@ impl ShapeSample for Capsule3d { let cylinder_vol = PI * self.radius * self.radius * 2.0 * self.half_length; // Add 4/3 pi r^3 let capsule_vol = cylinder_vol + 4.0 / 3.0 * PI * self.radius * self.radius * self.radius; - // Check if the random point should be inside the cylinder - if rng.gen_bool((cylinder_vol / capsule_vol) as f64) { - self.to_cylinder().sample_interior(rng) - } else { - let sphere = Sphere::new(self.radius); - let point = sphere.sample_interior(rng); - // Add half length if it is the top semi-sphere, otherwise subtract half - if point.y > 0.0 { - point + Vec3::Y * self.half_length + if capsule_vol > 0.0 { + // Check if the random point should be inside the cylinder + if rng.gen_bool((cylinder_vol / capsule_vol) as f64) { + self.to_cylinder().sample_interior(rng) } else { - point - Vec3::Y * self.half_length + let sphere = Sphere::new(self.radius); + let point = sphere.sample_interior(rng); + // Add half length if it is the top semi-sphere, otherwise subtract half + if point.y > 0.0 { + point + Vec3::Y * self.half_length + } else { + point - Vec3::Y * self.half_length + } } + } else { + Vec3::ZERO } } fn sample_boundary(&self, rng: &mut R) -> Vec3 { let cylinder_surface = TAU * self.radius * 2.0 * self.half_length; let capsule_surface = cylinder_surface + 4.0 * PI * self.radius * self.radius; - if rng.gen_bool((cylinder_surface / capsule_surface) as f64) { - let Vec2 { x, y: z } = Circle::new(self.radius).sample_boundary(rng); - let y = rng.gen_range(-self.half_length..=self.half_length); - Vec3::new(x, y, z) - } else { - let sphere = Sphere::new(self.radius); - let point = sphere.sample_boundary(rng); - // Add half length if it is the top semi-sphere, otherwise subtract half - if point.y > 0.0 { - point + Vec3::Y * self.half_length + if capsule_surface > 0.0 { + if rng.gen_bool((cylinder_surface / capsule_surface) as f64) { + let Vec2 { x, y: z } = Circle::new(self.radius).sample_boundary(rng); + let y = rng.gen_range(-self.half_length..=self.half_length); + Vec3::new(x, y, z) } else { - point - Vec3::Y * self.half_length + let sphere = Sphere::new(self.radius); + let point = sphere.sample_boundary(rng); + // Add half length if it is the top semi-sphere, otherwise subtract half + if point.y > 0.0 { + point + Vec3::Y * self.half_length + } else { + point - Vec3::Y * self.half_length + } } + } else { + Vec3::ZERO } } }