
# Objective - Fixes #17960 ## Solution - Followed the [edition upgrade guide](https://doc.rust-lang.org/edition-guide/editions/transitioning-an-existing-project-to-a-new-edition.html) ## Testing - CI --- ## Summary of Changes ### Documentation Indentation When using lists in documentation, proper indentation is now linted for. This means subsequent lines within the same list item must start at the same indentation level as the item. ```rust /* Valid */ /// - Item 1 /// Run-on sentence. /// - Item 2 struct Foo; /* Invalid */ /// - Item 1 /// Run-on sentence. /// - Item 2 struct Foo; ``` ### Implicit `!` to `()` Conversion `!` (the never return type, returned by `panic!`, etc.) no longer implicitly converts to `()`. This is particularly painful for systems with `todo!` or `panic!` statements, as they will no longer be functions returning `()` (or `Result<()>`), making them invalid systems for functions like `add_systems`. The ideal fix would be to accept functions returning `!` (or rather, _not_ returning), but this is blocked on the [stabilisation of the `!` type itself](https://doc.rust-lang.org/std/primitive.never.html), which is not done. The "simple" fix would be to add an explicit `-> ()` to system signatures (e.g., `|| { todo!() }` becomes `|| -> () { todo!() }`). However, this is _also_ banned, as there is an existing lint which (IMO, incorrectly) marks this as an unnecessary annotation. So, the "fix" (read: workaround) is to put these kinds of `|| -> ! { ... }` closuers into variables and give the variable an explicit type (e.g., `fn()`). ```rust // Valid let system: fn() = || todo!("Not implemented yet!"); app.add_systems(..., system); // Invalid app.add_systems(..., || todo!("Not implemented yet!")); ``` ### Temporary Variable Lifetimes The order in which temporary variables are dropped has changed. The simple fix here is _usually_ to just assign temporaries to a named variable before use. ### `gen` is a keyword We can no longer use the name `gen` as it is reserved for a future generator syntax. This involved replacing uses of the name `gen` with `r#gen` (the raw-identifier syntax). ### Formatting has changed Use statements have had the order of imports changed, causing a substantial +/-3,000 diff when applied. For now, I have opted-out of this change by amending `rustfmt.toml` ```toml style_edition = "2021" ``` This preserves the original formatting for now, reducing the size of this PR. It would be a simple followup to update this to 2024 and run `cargo fmt`. ### New `use<>` Opt-Out Syntax Lifetimes are now implicitly included in RPIT types. There was a handful of instances where it needed to be added to satisfy the borrow checker, but there may be more cases where it _should_ be added to avoid breakages in user code. ### `MyUnitStruct { .. }` is an invalid pattern Previously, you could match against unit structs (and unit enum variants) with a `{ .. }` destructuring. This is no longer valid. ### Pretty much every use of `ref` and `mut` are gone Pattern binding has changed to the point where these terms are largely unused now. They still serve a purpose, but it is far more niche now. ### `iter::repeat(...).take(...)` is bad New lint recommends using the more explicit `iter::repeat_n(..., ...)` instead. ## Migration Guide The lifetimes of functions using return-position impl-trait (RPIT) are likely _more_ conservative than they had been previously. If you encounter lifetime issues with such a function, please create an issue to investigate the addition of `+ use<...>`. ## Notes - Check the individual commits for a clearer breakdown for what _actually_ changed. --------- Co-authored-by: François Mockers <francois.mockers@vleue.com>
641 lines
22 KiB
Rust
641 lines
22 KiB
Rust
//! The [`ShapeSample`] trait, allowing random sampling from geometric shapes.
|
|
//!
|
|
//! At the most basic level, this allows sampling random points from the interior and boundary of
|
|
//! geometric primitives. For example:
|
|
//! ```
|
|
//! # use bevy_math::primitives::*;
|
|
//! # use bevy_math::ShapeSample;
|
|
//! # use rand::SeedableRng;
|
|
//! # use rand::rngs::StdRng;
|
|
//! // Get some `Rng`:
|
|
//! let rng = &mut StdRng::from_entropy();
|
|
//! // Make a circle of radius 2:
|
|
//! let circle = Circle::new(2.0);
|
|
//! // Get a point inside this circle uniformly at random:
|
|
//! let interior_pt = circle.sample_interior(rng);
|
|
//! // Get a point on the circle's boundary uniformly at random:
|
|
//! let boundary_pt = circle.sample_boundary(rng);
|
|
//! ```
|
|
//!
|
|
//! For repeated sampling, `ShapeSample` also includes methods for accessing a [`Distribution`]:
|
|
//! ```
|
|
//! # use bevy_math::primitives::*;
|
|
//! # use bevy_math::{Vec2, ShapeSample};
|
|
//! # use rand::SeedableRng;
|
|
//! # use rand::rngs::StdRng;
|
|
//! # use rand::distributions::Distribution;
|
|
//! # let rng1 = StdRng::from_entropy();
|
|
//! # let rng2 = StdRng::from_entropy();
|
|
//! // Use a rectangle this time:
|
|
//! let rectangle = Rectangle::new(1.0, 2.0);
|
|
//! // Get an iterator that spits out random interior points:
|
|
//! let interior_iter = rectangle.interior_dist().sample_iter(rng1);
|
|
//! // Collect random interior points from the iterator:
|
|
//! let interior_pts: Vec<Vec2> = interior_iter.take(1000).collect();
|
|
//! // Similarly, get an iterator over many random boundary points and collect them:
|
|
//! let boundary_pts: Vec<Vec2> = rectangle.boundary_dist().sample_iter(rng2).take(1000).collect();
|
|
//! ```
|
|
//!
|
|
//! In any case, the [`Rng`] used as the source of randomness must be provided explicitly.
|
|
|
|
use core::f32::consts::{PI, TAU};
|
|
|
|
use crate::{ops, primitives::*, NormedVectorSpace, Vec2, Vec3};
|
|
use rand::{
|
|
distributions::{Distribution, WeightedIndex},
|
|
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.
|
|
///
|
|
/// Shapes like [`Cylinder`], [`Capsule2d`] and [`Capsule3d`] are oriented along the y-axis.
|
|
///
|
|
/// # 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_interior(&mut rand::thread_rng()));
|
|
/// ```
|
|
fn sample_interior<R: Rng + ?Sized>(&self, rng: &mut R) -> Self::Output;
|
|
|
|
/// 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::*;
|
|
/// 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_boundary(&mut rand::thread_rng()));
|
|
/// ```
|
|
fn sample_boundary<R: Rng + ?Sized>(&self, rng: &mut R) -> Self::Output;
|
|
|
|
/// Extract a [`Distribution`] whose samples are points of this shape's interior, taken uniformly.
|
|
///
|
|
/// # Example
|
|
///
|
|
/// ```
|
|
/// # use bevy_math::prelude::*;
|
|
/// # use rand::distributions::Distribution;
|
|
/// let square = Rectangle::new(2.0, 2.0);
|
|
/// let rng = rand::thread_rng();
|
|
///
|
|
/// // Iterate over points randomly drawn from `square`'s interior:
|
|
/// for random_val in square.interior_dist().sample_iter(rng).take(5) {
|
|
/// println!("{}", random_val);
|
|
/// }
|
|
/// ```
|
|
fn interior_dist(self) -> impl Distribution<Self::Output>
|
|
where
|
|
Self: Sized,
|
|
{
|
|
InteriorOf(self)
|
|
}
|
|
|
|
/// Extract a [`Distribution`] whose samples are points of this shape's boundary, taken uniformly.
|
|
///
|
|
/// # Example
|
|
///
|
|
/// ```
|
|
/// # use bevy_math::prelude::*;
|
|
/// # use rand::distributions::Distribution;
|
|
/// let square = Rectangle::new(2.0, 2.0);
|
|
/// let rng = rand::thread_rng();
|
|
///
|
|
/// // Iterate over points randomly drawn from `square`'s boundary:
|
|
/// for random_val in square.boundary_dist().sample_iter(rng).take(5) {
|
|
/// println!("{}", random_val);
|
|
/// }
|
|
/// ```
|
|
fn boundary_dist(self) -> impl Distribution<Self::Output>
|
|
where
|
|
Self: Sized,
|
|
{
|
|
BoundaryOf(self)
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy)]
|
|
/// A wrapper struct that allows interior sampling from a [`ShapeSample`] type directly as
|
|
/// a [`Distribution`].
|
|
pub struct InteriorOf<T: ShapeSample>(pub T);
|
|
|
|
#[derive(Clone, Copy)]
|
|
/// A wrapper struct that allows boundary sampling from a [`ShapeSample`] type directly as
|
|
/// a [`Distribution`].
|
|
pub struct BoundaryOf<T: ShapeSample>(pub T);
|
|
|
|
impl<T: ShapeSample> Distribution<<T as ShapeSample>::Output> for InteriorOf<T> {
|
|
fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> <T as ShapeSample>::Output {
|
|
self.0.sample_interior(rng)
|
|
}
|
|
}
|
|
|
|
impl<T: ShapeSample> Distribution<<T as ShapeSample>::Output> for BoundaryOf<T> {
|
|
fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> <T as ShapeSample>::Output {
|
|
self.0.sample_boundary(rng)
|
|
}
|
|
}
|
|
|
|
impl ShapeSample for Circle {
|
|
type Output = Vec2;
|
|
|
|
fn sample_interior<R: Rng + ?Sized>(&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));
|
|
let r = ops::sqrt(r_squared);
|
|
let (sin, cos) = ops::sin_cos(theta);
|
|
Vec2::new(r * cos, r * sin)
|
|
}
|
|
|
|
fn sample_boundary<R: Rng + ?Sized>(&self, rng: &mut R) -> Vec2 {
|
|
let theta = rng.gen_range(0.0..TAU);
|
|
let (sin, cos) = ops::sin_cos(theta);
|
|
Vec2::new(self.radius * cos, self.radius * sin)
|
|
}
|
|
}
|
|
|
|
/// Boundary sampling for unit-spheres
|
|
#[inline]
|
|
fn sample_unit_sphere_boundary<R: Rng + ?Sized>(rng: &mut R) -> Vec3 {
|
|
let z = rng.gen_range(-1f32..=1f32);
|
|
let (a_sin, a_cos) = ops::sin_cos(rng.gen_range(-PI..=PI));
|
|
let c = ops::sqrt(1f32 - z * z);
|
|
let x = a_sin * c;
|
|
let y = a_cos * c;
|
|
|
|
Vec3::new(x, y, z)
|
|
}
|
|
|
|
impl ShapeSample for Sphere {
|
|
type Output = Vec3;
|
|
|
|
fn sample_interior<R: Rng + ?Sized>(&self, rng: &mut R) -> Vec3 {
|
|
let r_cubed = rng.gen_range(0.0..=(self.radius * self.radius * self.radius));
|
|
let r = ops::cbrt(r_cubed);
|
|
|
|
r * sample_unit_sphere_boundary(rng)
|
|
}
|
|
|
|
fn sample_boundary<R: Rng + ?Sized>(&self, rng: &mut R) -> Vec3 {
|
|
self.radius * sample_unit_sphere_boundary(rng)
|
|
}
|
|
}
|
|
|
|
impl ShapeSample for Annulus {
|
|
type Output = Vec2;
|
|
|
|
fn sample_interior<R: Rng + ?Sized>(&self, rng: &mut R) -> Self::Output {
|
|
let inner_radius = self.inner_circle.radius;
|
|
let outer_radius = self.outer_circle.radius;
|
|
|
|
// Like random sampling for a circle, radius is weighted by the square.
|
|
let r_squared = rng.gen_range((inner_radius * inner_radius)..(outer_radius * outer_radius));
|
|
let r = ops::sqrt(r_squared);
|
|
let theta = rng.gen_range(0.0..TAU);
|
|
let (sin, cos) = ops::sin_cos(theta);
|
|
|
|
Vec2::new(r * cos, r * sin)
|
|
}
|
|
|
|
fn sample_boundary<R: Rng + ?Sized>(&self, rng: &mut R) -> Self::Output {
|
|
let total_perimeter = self.inner_circle.perimeter() + self.outer_circle.perimeter();
|
|
let inner_prob = (self.inner_circle.perimeter() / total_perimeter) as f64;
|
|
|
|
// Sample from boundary circles, choosing which one by weighting by perimeter:
|
|
let inner = rng.gen_bool(inner_prob);
|
|
if inner {
|
|
self.inner_circle.sample_boundary(rng)
|
|
} else {
|
|
self.outer_circle.sample_boundary(rng)
|
|
}
|
|
}
|
|
}
|
|
|
|
impl ShapeSample for Rectangle {
|
|
type Output = Vec2;
|
|
|
|
fn sample_interior<R: Rng + ?Sized>(&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_boundary<R: Rng + ?Sized>(&self, rng: &mut R) -> Vec2 {
|
|
let primary_side = rng.gen_range(-1.0..1.0);
|
|
let other_side = if rng.r#gen() { -1.0 } else { 1.0 };
|
|
|
|
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::ZERO
|
|
}
|
|
}
|
|
}
|
|
|
|
impl ShapeSample for Cuboid {
|
|
type Output = Vec3;
|
|
|
|
fn sample_interior<R: Rng + ?Sized>(&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_boundary<R: Rng + ?Sized>(&self, rng: &mut R) -> Vec3 {
|
|
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.r#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
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Interior sampling for triangles which doesn't depend on the ambient dimension.
|
|
fn sample_triangle_interior<P: NormedVectorSpace, R: Rng + ?Sized>(
|
|
vertices: [P; 3],
|
|
rng: &mut R,
|
|
) -> P {
|
|
let [a, b, c] = vertices;
|
|
let ab = b - a;
|
|
let ac = c - a;
|
|
|
|
// Generate random points on a parallelepiped and reflect so that
|
|
// we can use the points that lie outside the triangle
|
|
let u = rng.gen_range(0.0..=1.0);
|
|
let v = rng.gen_range(0.0..=1.0);
|
|
|
|
if u + v > 1. {
|
|
let u1 = 1. - v;
|
|
let v1 = 1. - u;
|
|
a + (ab * u1 + ac * v1)
|
|
} else {
|
|
a + (ab * u + ac * v)
|
|
}
|
|
}
|
|
|
|
/// Boundary sampling for triangles which doesn't depend on the ambient dimension.
|
|
fn sample_triangle_boundary<P: NormedVectorSpace, R: Rng + ?Sized>(
|
|
vertices: [P; 3],
|
|
rng: &mut R,
|
|
) -> P {
|
|
let [a, b, c] = vertices;
|
|
let ab = b - a;
|
|
let ac = c - a;
|
|
let bc = c - b;
|
|
|
|
let t = rng.gen_range(0.0..=1.0);
|
|
|
|
if let Ok(dist) = WeightedIndex::new([ab.norm(), ac.norm(), bc.norm()]) {
|
|
match dist.sample(rng) {
|
|
0 => a.lerp(b, t),
|
|
1 => a.lerp(c, t),
|
|
2 => b.lerp(c, t),
|
|
_ => unreachable!(),
|
|
}
|
|
} else {
|
|
// This should only occur when the triangle is 0-dimensional degenerate
|
|
// so this is actually the correct result.
|
|
a
|
|
}
|
|
}
|
|
|
|
impl ShapeSample for Triangle2d {
|
|
type Output = Vec2;
|
|
|
|
fn sample_interior<R: Rng + ?Sized>(&self, rng: &mut R) -> Self::Output {
|
|
sample_triangle_interior(self.vertices, rng)
|
|
}
|
|
|
|
fn sample_boundary<R: Rng + ?Sized>(&self, rng: &mut R) -> Self::Output {
|
|
sample_triangle_boundary(self.vertices, rng)
|
|
}
|
|
}
|
|
|
|
impl ShapeSample for Triangle3d {
|
|
type Output = Vec3;
|
|
|
|
fn sample_interior<R: Rng + ?Sized>(&self, rng: &mut R) -> Self::Output {
|
|
sample_triangle_interior(self.vertices, rng)
|
|
}
|
|
|
|
fn sample_boundary<R: Rng + ?Sized>(&self, rng: &mut R) -> Self::Output {
|
|
sample_triangle_boundary(self.vertices, rng)
|
|
}
|
|
}
|
|
|
|
impl ShapeSample for Tetrahedron {
|
|
type Output = Vec3;
|
|
|
|
fn sample_interior<R: Rng + ?Sized>(&self, rng: &mut R) -> Self::Output {
|
|
let [v0, v1, v2, v3] = self.vertices;
|
|
|
|
// Generate a random point in a cube:
|
|
let mut coords: [f32; 3] = [
|
|
rng.gen_range(0.0..1.0),
|
|
rng.gen_range(0.0..1.0),
|
|
rng.gen_range(0.0..1.0),
|
|
];
|
|
|
|
// The cube is broken into six tetrahedra of the form 0 <= c_0 <= c_1 <= c_2 <= 1,
|
|
// where c_i are the three euclidean coordinates in some permutation. (Since 3! = 6,
|
|
// there are six of them). Sorting the coordinates folds these six tetrahedra into the
|
|
// tetrahedron 0 <= x <= y <= z <= 1 (i.e. a fundamental domain of the permutation action).
|
|
coords.sort_by(|x, y| x.partial_cmp(y).unwrap());
|
|
|
|
// Now, convert a point from the fundamental tetrahedron into barycentric coordinates by
|
|
// taking the four successive differences of coordinates; note that these telescope to sum
|
|
// to 1, and this transformation is linear, hence preserves the probability density, since
|
|
// the latter comes from the Lebesgue measure.
|
|
//
|
|
// (See https://en.wikipedia.org/wiki/Lebesgue_measure#Properties — specifically, that
|
|
// Lebesgue measure of a linearly transformed set is its original measure times the
|
|
// determinant.)
|
|
let (a, b, c, d) = (
|
|
coords[0],
|
|
coords[1] - coords[0],
|
|
coords[2] - coords[1],
|
|
1. - coords[2],
|
|
);
|
|
|
|
// This is also a linear mapping, so probability density is still preserved.
|
|
v0 * a + v1 * b + v2 * c + v3 * d
|
|
}
|
|
|
|
fn sample_boundary<R: Rng + ?Sized>(&self, rng: &mut R) -> Self::Output {
|
|
let triangles = self.faces();
|
|
let areas = triangles.iter().map(Measured2d::area);
|
|
|
|
if areas.clone().sum::<f32>() > 0.0 {
|
|
// There is at least one triangle with nonzero area, so this unwrap succeeds.
|
|
let dist = WeightedIndex::new(areas).unwrap();
|
|
|
|
// Get a random index, then sample the interior of the associated triangle.
|
|
let idx = dist.sample(rng);
|
|
triangles[idx].sample_interior(rng)
|
|
} else {
|
|
// In this branch the tetrahedron has zero surface area; just return a point that's on
|
|
// the tetrahedron.
|
|
self.vertices[0]
|
|
}
|
|
}
|
|
}
|
|
|
|
impl ShapeSample for Cylinder {
|
|
type Output = Vec3;
|
|
|
|
fn sample_interior<R: Rng + ?Sized>(&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_boundary<R: Rng + ?Sized>(&self, rng: &mut R) -> Vec3 {
|
|
// This uses the area of the ends divided by the overall surface area (optimized)
|
|
// [2 (\pi r^2)]/[2 (\pi r^2) + 2 \pi r h] = r/(r + h)
|
|
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.r#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_boundary(rng);
|
|
let y = rng.gen_range(-self.half_height..=self.half_height);
|
|
Vec3::new(x, y, z)
|
|
}
|
|
} else {
|
|
Vec3::ZERO
|
|
}
|
|
}
|
|
}
|
|
|
|
impl ShapeSample for Capsule2d {
|
|
type Output = Vec2;
|
|
|
|
fn sample_interior<R: Rng + ?Sized>(&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;
|
|
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) {
|
|
self.to_inner_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
|
|
} else {
|
|
point - Vec2::Y * self.half_length
|
|
}
|
|
}
|
|
} else {
|
|
Vec2::ZERO
|
|
}
|
|
}
|
|
|
|
fn sample_boundary<R: Rng + ?Sized>(&self, rng: &mut R) -> Vec2 {
|
|
let rectangle_surface = 4.0 * self.half_length;
|
|
let capsule_surface = rectangle_surface + TAU * self.radius;
|
|
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 {
|
|
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 {
|
|
Vec2::ZERO
|
|
}
|
|
}
|
|
}
|
|
|
|
impl ShapeSample for Capsule3d {
|
|
type Output = Vec3;
|
|
|
|
fn sample_interior<R: Rng + ?Sized>(&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;
|
|
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 {
|
|
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<R: Rng + ?Sized>(&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 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 {
|
|
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
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<P: Primitive2d + Measured2d + ShapeSample<Output = Vec2>> ShapeSample for Extrusion<P> {
|
|
type Output = Vec3;
|
|
|
|
fn sample_interior<R: Rng + ?Sized>(&self, rng: &mut R) -> Self::Output {
|
|
let base_point = self.base_shape.sample_interior(rng);
|
|
let depth = rng.gen_range(-self.half_depth..self.half_depth);
|
|
base_point.extend(depth)
|
|
}
|
|
|
|
fn sample_boundary<R: Rng + ?Sized>(&self, rng: &mut R) -> Self::Output {
|
|
let base_area = self.base_shape.area();
|
|
let total_area = self.area();
|
|
|
|
let random = rng.gen_range(0.0..total_area);
|
|
match random {
|
|
x if x < base_area => self.base_shape.sample_interior(rng).extend(self.half_depth),
|
|
x if x < 2. * base_area => self
|
|
.base_shape
|
|
.sample_interior(rng)
|
|
.extend(-self.half_depth),
|
|
_ => self
|
|
.base_shape
|
|
.sample_boundary(rng)
|
|
.extend(rng.gen_range(-self.half_depth..self.half_depth)),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use rand::SeedableRng;
|
|
use rand_chacha::ChaCha8Rng;
|
|
|
|
#[test]
|
|
fn circle_interior_sampling() {
|
|
let mut rng = ChaCha8Rng::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,
|
|
[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 = ChaCha8Rng::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 = ops::atan(point.y / point.x) + PI / 2.0;
|
|
let wedge = ops::floor(angle * 8.0 / PI) as usize;
|
|
wedge_hits[wedge] += 1;
|
|
}
|
|
|
|
assert_eq!(
|
|
wedge_hits,
|
|
[636, 608, 639, 603, 614, 650, 640, 610],
|
|
"samples will occur across all array items at statistically equal chance"
|
|
);
|
|
}
|
|
}
|