Skip to content

Commit

Permalink
Add Distribution access methods for ShapeSample trait (#13315)
Browse files Browse the repository at this point in the history
Stolen from #12835. 

# Objective

Sometimes you want to sample a whole bunch of points from a shape
instead of just one. You can write your own loop to do this, but it's
really more idiomatic to use a `rand`
[`Distribution`](https://docs.rs/rand/latest/rand/distributions/trait.Distribution.html)
with the `sample_iter` method. Distributions also support other useful
things like mapping, and they are suitable as generic items for
consumption by other APIs.

## Solution

`ShapeSample` has been given two new automatic trait methods,
`interior_dist` and `boundary_dist`. They both have similar signatures
(recall that `Output` is the output type for `ShapeSample`):
```rust
fn interior_dist(self) -> impl Distribution<Self::Output>
where Self: Sized { //... }
```

These have default implementations which are powered by wrapper structs
`InteriorOf` and `BoundaryOf` that actually implement `Distribution` —
the implementations effectively just call `ShapeSample::sample_interior`
and `ShapeSample::sample_boundary` on the contained type.

The upshot is that this allows iteration as follows:
```rust
// Get an iterator over boundary points of a rectangle:
let rectangle = Rectangle::new(1.0, 2.0);
let boundary_iter = rectangle.boundary_dist().sample_iter(rng);
// Collect a bunch of boundary points at once:
let boundary_pts: Vec<Vec2> = boundary_iter.take(1000).collect();
```

Alternatively, you can use `InteriorOf`/`BoundaryOf` explicitly to
similar effect:
```rust
let boundary_pts: Vec<Vec2> = BoundaryOf(rectangle).sample_iter(rng).take(1000).collect();
```

---

## Changelog

- Added `InteriorOf` and `BoundaryOf` distribution wrapper structs in
`bevy_math::sampling::shape_sampling`.
- Added `interior_dist` and `boundary_dist` automatic trait methods to
`ShapeSample`.
- Made `shape_sampling` module public with explanatory documentation.

---

## Discussion

### Design choices

The main point of interest here is just the choice of `impl
Distribution` instead of explicitly using `InteriorOf`/`BoundaryOf`
return types for `interior_dist` and `boundary_dist`. The reason for
this choice is that it allows future optimizations for repeated sampling
— for example, instead of just wrapping the base type,
`interior_dist`/`boundary_dist` could construct auxiliary data that is
held over between sampling operations.
  • Loading branch information
mweatherley authored May 22, 2024
1 parent c7f7d90 commit d2ef88f
Show file tree
Hide file tree
Showing 2 changed files with 107 additions and 1 deletion.
2 changes: 1 addition & 1 deletion crates/bevy_math/src/sampling/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
//!
//! To use this, the "rand" feature must be enabled.

mod shape_sampling;
pub mod shape_sampling;
pub mod standard;

pub use shape_sampling::*;
Expand Down
106 changes: 106 additions & 0 deletions crates/bevy_math/src/sampling/shape_sampling.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,43 @@
//! 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 std::f32::consts::{PI, TAU};

use crate::{primitives::*, NormedVectorSpace, Vec2, Vec3};
Expand Down Expand Up @@ -39,6 +79,72 @@ pub trait ShapeSample {
/// 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 {
Expand Down

0 comments on commit d2ef88f

Please sign in to comment.