Random sampling of directions and quaternions (#12857)

# Objective

Augment Bevy's random sampling capabilities by providing good tools for
producing random directions and rotations.

## Solution

The `rand` crate has a natural tool for providing `Distribution`s whose
output is a type that doesn't require any additional data to sample
values — namely,
[`Standard`](https://docs.rs/rand/latest/rand/distributions/struct.Standard.html).

Here, our existing `ShapeSample` implementations have been put to good
use in providing these, resulting in patterns like the following:
```rust
// Using thread-local rng
let random_direction1: Dir3 = random();

// Using an explicit rng
let random_direction2: Dir3 = rng.gen();

// Using an explicit rng coupled explicitly with Standard
let random_directions: Vec<Dir3> = rng.sample_iter(Standard).take(5).collect();
```

Furthermore, we have introduced a trait `FromRng` which provides sugar
for `rng.gen()` that is more namespace-friendly (in this author's
opinion):
```rust
let random_direction = Dir3::from_rng(rng);
```

The types this has been implemented for are `Dir2`, `Dir3`, `Dir3A`, and
`Quat`. Notably, `Quat` uses `glam`'s implementation rather than an
in-house one, and as a result, `bevy_math`'s "rand" feature now enables
that of `glam`.

---

## Changelog

- Created `standard` submodule in `sampling` to hold implementations and
other items related to the `Standard` distribution.
- "rand" feature of `bevy_math` now enables that of `glam`.

---

## Discussion

From a quick glance at `Quat`'s distribution implementation in `glam`, I
am a bit suspicious, since it is simple and doesn't match any algorithm
that I came across in my research. I will do a little more digging as a
follow-up to this and see if it's actually uniform (maybe even using
those tools I wrote — what a thrill).

As an aside, I'd also like to say that I think
[`Distribution`](https://docs.rs/rand/latest/rand/distributions/trait.Distribution.html)
is really, really good. It integrates with distributions provided
externally (e.g. in `rand` itself and its extensions) along with doing a
good job of isolating the source of randomness, so that output can be
reliably reproduced if need be. Finally, `Distribution::sample_iter` is
quite good for ergonomically acquiring lots of random values. At one
point I found myself writing traits to describe random sampling and
essentially reinvented this one. I just think it's good, and I think
it's worth centralizing around to a significant extent.
This commit is contained in:
Matty 2024-04-04 19:13:00 -04:00 committed by GitHub
parent 4a649d5030
commit 3a7923ea92
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 97 additions and 4 deletions

View file

@ -41,7 +41,7 @@ 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"]
rand = ["dep:rand", "glam/rand"]
[lints]
workspace = true

View file

@ -23,7 +23,7 @@ mod ray;
mod rects;
mod rotation2d;
#[cfg(feature = "rand")]
mod shape_sampling;
pub mod sampling;
pub use affine3::*;
pub use aspect_ratio::AspectRatio;
@ -34,13 +34,13 @@ pub use ray::{Ray2d, Ray3d};
pub use rects::*;
pub use rotation2d::Rotation2d;
#[cfg(feature = "rand")]
pub use shape_sampling::ShapeSample;
pub use sampling::{FromRng, ShapeSample};
/// The `bevy_math` prelude.
pub mod prelude {
#[doc(hidden)]
#[cfg(feature = "rand")]
pub use crate::shape_sampling::ShapeSample;
pub use crate::sampling::{FromRng, ShapeSample};
#[doc(hidden)]
pub use crate::{
cubic_splines::{

View file

@ -0,0 +1,9 @@
//! This module contains tools related to random sampling.
//!
//! To use this, the "rand" feature must be enabled.
mod shape_sampling;
pub mod standard;
pub use shape_sampling::*;
pub use standard::*;

View file

@ -0,0 +1,84 @@
//! This module holds local implementations of the [`Distribution`] trait for [`Standard`], which
//! allow certain Bevy math types (those whose values can be randomly generated without additional
//! input other than an [`Rng`]) to be produced using [`rand`]'s APIs. It also holds [`FromRng`],
//! an ergonomic extension to that functionality which permits the omission of type annotations.
//!
//! For instance:
//! ```
//! # use rand::{random, Rng, SeedableRng, rngs::StdRng, distributions::Standard};
//! # use bevy_math::{Dir3, sampling::FromRng};
//! let mut rng = StdRng::from_entropy();
//! // Random direction using thread-local rng
//! let random_direction1: Dir3 = random();
//!
//! // Random direction using the rng constructed above
//! let random_direction2: Dir3 = rng.gen();
//!
//! // The same as the previous but with different syntax
//! let random_direction3 = Dir3::from_rng(&mut rng);
//!
//! // Five random directions, using Standard explicitly
//! let many_random_directions: Vec<Dir3> = rng.sample_iter(Standard).take(5).collect();
//! ```
use crate::{
primitives::{Circle, Sphere},
Dir2, Dir3, Dir3A, Quat, ShapeSample, Vec3A,
};
use rand::{
distributions::{Distribution, Standard},
Rng,
};
/// Ergonomics trait for a type with a [`Standard`] distribution, allowing values to be generated
/// uniformly from an [`Rng`] by a method in its own namespace.
///
/// Example
/// ```
/// # use rand::{Rng, SeedableRng, rngs::StdRng};
/// # use bevy_math::{Dir3, sampling::FromRng};
/// let mut rng = StdRng::from_entropy();
/// let random_dir = Dir3::from_rng(&mut rng);
/// ```
pub trait FromRng
where
Self: Sized,
Standard: Distribution<Self>,
{
/// Construct a value of this type uniformly at random using `rng` as the source of randomness.
fn from_rng<R: Rng + ?Sized>(rng: &mut R) -> Self {
rng.gen()
}
}
impl Distribution<Dir2> for Standard {
fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> Dir2 {
let circle = Circle::new(1.0);
let vector = circle.sample_boundary(rng);
Dir2::new_unchecked(vector)
}
}
impl FromRng for Dir2 {}
impl Distribution<Dir3> for Standard {
fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> Dir3 {
let sphere = Sphere::new(1.0);
let vector = sphere.sample_boundary(rng);
Dir3::new_unchecked(vector)
}
}
impl FromRng for Dir3 {}
impl Distribution<Dir3A> for Standard {
fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> Dir3A {
let sphere = Sphere::new(1.0);
let vector: Vec3A = sphere.sample_boundary(rng).into();
Dir3A::new_unchecked(vector)
}
}
impl FromRng for Dir3A {}
impl FromRng for Quat {}