use glam::DVec3;
use crate::vector::basis_for;
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct Rng(u64);
fn mix64(mut z: u64) -> u64 {
z = z.wrapping_add(0x9E37_79B9_7F4A_7C15);
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
z ^ (z >> 31)
}
impl Rng {
pub fn new(seed: u64) -> Self {
Rng(if seed == 0 {
0x9E37_79B9_7F4A_7C15
} else {
seed
})
}
pub fn for_index(seed: u64, index: u64) -> Rng {
Rng::new(mix64(seed ^ mix64(index)))
}
pub fn split(&mut self) -> Rng {
let drawn = self.next_u64();
Rng::new(mix64(drawn))
}
fn next_u64(&mut self) -> u64 {
let mut x = self.0;
x ^= x >> 12;
x ^= x << 25;
x ^= x >> 27;
self.0 = x;
x.wrapping_mul(0x2545_F491_4F6C_DD1D)
}
pub fn unit(&mut self) -> f64 {
(self.next_u64() >> 11) as f64 / (1u64 << 53) as f64
}
pub fn range(&mut self, lo: f64, hi: f64) -> f64 {
lo + (hi - lo) * self.unit()
}
pub fn in_disc(&mut self, radius: f64) -> (f64, f64) {
let r = radius * self.unit().sqrt();
let phi = std::f64::consts::TAU * self.unit();
(r * phi.cos(), r * phi.sin())
}
pub fn on_sphere(&mut self) -> DVec3 {
let z = self.range(-1.0, 1.0);
let phi = std::f64::consts::TAU * self.unit();
let r = (1.0 - z * z).max(0.0).sqrt();
DVec3::new(r * phi.cos(), r * phi.sin(), z)
}
pub fn on_hemisphere(&mut self, normal: DVec3) -> DVec3 {
let d = self.on_sphere();
if d.dot(normal) < 0.0 {
-d
} else {
d
}
}
pub fn cosine_hemisphere(&mut self, normal: DVec3) -> DVec3 {
let n = normal.normalize();
let (x, y) = self.in_disc(1.0);
let z = (1.0 - x * x - y * y).max(0.0).sqrt();
let (u, v) = basis_for(n);
(u * x + v * y + n * z).normalize()
}
pub fn gaussian(&mut self) -> f64 {
let u1 = self.unit().max(f64::MIN_POSITIVE);
let u2 = self.unit();
(-2.0 * u1.ln()).sqrt() * (std::f64::consts::TAU * u2).cos()
}
pub fn normal(&mut self, mean: f64, std_dev: f64) -> f64 {
mean + std_dev * self.gaussian()
}
pub fn poisson(&mut self, mean: f64) -> u64 {
if mean.is_nan() || mean <= 0.0 {
return 0;
}
if mean < 30.0 {
let u = self.unit();
let mut p = (-mean).exp();
let mut cumulative = p;
let mut k = 0u64;
let cap = (mean * 20.0) as u64 + 100;
while u > cumulative && k < cap {
k += 1;
p *= mean / k as f64;
cumulative += p;
}
k
} else {
let drawn = mean + self.gaussian() * mean.sqrt();
drawn.round().max(0.0) as u64
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn indexed_streams_are_order_free() {
let forward: Vec<f64> = (0..64)
.map(|i| Rng::for_index(0xD0A1_15EE, i).unit())
.collect();
let backward: Vec<f64> = (0..64)
.rev()
.map(|i| Rng::for_index(0xD0A1_15EE, i).unit())
.collect();
let mut backward_reordered = backward;
backward_reordered.reverse();
assert_eq!(forward, backward_reordered);
}
#[test]
fn adjacent_indices_are_decorrelated() {
let draws: Vec<Vec<f64>> = (0..16)
.map(|i| {
let mut r = Rng::for_index(7, i);
(0..8).map(|_| r.unit()).collect()
})
.collect();
for i in 0..draws.len() {
for j in (i + 1)..draws.len() {
assert_ne!(draws[i], draws[j], "streams {i} and {j} collided");
}
}
for i in 1..draws.len() {
assert!(
(draws[i][0] - draws[i - 1][0]).abs() > 1e-6,
"streams {} and {i} start too close",
i - 1
);
}
}
#[test]
fn seed_and_index_are_not_interchangeable() {
assert_ne!(
Rng::for_index(3, 9).unit(),
Rng::for_index(9, 3).unit(),
"the pair must be ordered"
);
}
#[test]
fn the_degenerate_seed_is_handled() {
let mut zero = Rng::new(0);
let draws: Vec<f64> = (0..4).map(|_| zero.unit()).collect();
assert!(draws.iter().all(|&v| v > 0.0 && v < 1.0), "{draws:?}");
assert!(draws[0] != draws[1]);
}
#[test]
fn a_split_stream_diverges_from_its_parent() {
let mut parent = Rng::new(42);
let mut child = parent.split();
let p: Vec<f64> = (0..8).map(|_| parent.unit()).collect();
let c: Vec<f64> = (0..8).map(|_| child.unit()).collect();
assert_ne!(p, c);
let mut a = Rng::new(42);
let mut b = a.clone();
assert_eq!(a.unit(), b.unit());
}
#[test]
fn the_stream_is_pinned() {
let mut r = Rng::new(0x5A17_7E3D);
let mut hash = 0u64;
for _ in 0..10_000 {
hash = hash.rotate_left(7).wrapping_mul(0x1000_0000_01B3)
^ (r.unit() * (1u64 << 53) as f64) as u64;
}
assert_eq!(hash, PINNED_DIGEST, "the generator's output has changed");
}
const PINNED_DIGEST: u64 = 6_777_642_030_472_145_829;
#[test]
fn gaussians_are_standard_normal() {
let mut r = Rng::new(1234);
const N: usize = 100_000;
let draws: Vec<f64> = (0..N).map(|_| r.gaussian()).collect();
let mean = draws.iter().sum::<f64>() / N as f64;
let variance = draws.iter().map(|d| (d - mean).powi(2)).sum::<f64>() / N as f64;
assert!(mean.abs() < 0.02, "mean {mean}");
assert!((variance - 1.0).abs() < 0.03, "variance {variance}");
let tail = draws.iter().filter(|d| d.abs() > 3.0).count() as f64 / N as f64;
assert!((tail - 0.0027).abs() < 0.002, "three-sigma tail {tail}");
assert!(draws.iter().all(|d| d.is_finite()));
}
#[test]
fn cosine_sampling_leans_towards_the_normal() {
let mut r = Rng::new(99);
let n = DVec3::new(1.0, 2.0, -0.5).normalize();
const N: usize = 50_000;
let mut cos_sum = 0.0;
for _ in 0..N {
let d = r.cosine_hemisphere(n);
let c = d.dot(n);
assert!(c > -1e-9, "sample left the hemisphere: {c}");
assert!((d.length() - 1.0).abs() < 1e-9);
cos_sum += c;
}
let mean_cos = cos_sum / N as f64;
assert!(
(mean_cos - 2.0 / 3.0).abs() < 0.01,
"Lambertian mean cosine should be 2/3, got {mean_cos}"
);
}
#[test]
fn poisson_variance_equals_its_mean() {
const N: usize = 200_000;
for mean in [0.5f64, 3.0, 12.0, 29.0, 31.0, 200.0, 5000.0] {
let mut r = Rng::new(0xC0FFEE);
let draws: Vec<f64> = (0..N).map(|_| r.poisson(mean) as f64).collect();
let measured_mean = draws.iter().sum::<f64>() / N as f64;
let variance = draws
.iter()
.map(|d| (d - measured_mean).powi(2))
.sum::<f64>()
/ N as f64;
assert!(
(measured_mean / mean - 1.0).abs() < 0.02,
"mean {mean}: got {measured_mean}"
);
assert!(
(variance / mean - 1.0).abs() < 0.05,
"mean {mean}: variance {variance} should equal the mean"
);
}
}
#[test]
fn a_small_poisson_matches_its_exact_probabilities() {
const N: usize = 400_000;
let mean = 2.5f64;
let mut r = Rng::new(7);
let mut counts = [0usize; 12];
for _ in 0..N {
let k = r.poisson(mean) as usize;
if k < counts.len() {
counts[k] += 1;
}
}
let mut factorial = 1.0;
for (k, count) in counts.iter().enumerate() {
if k > 0 {
factorial *= k as f64;
}
let exact = (-mean).exp() * mean.powi(k as i32) / factorial;
let measured = *count as f64 / N as f64;
assert!(
(measured - exact).abs() < 3e-3,
"P({k}): measured {measured:.5}, exact {exact:.5}"
);
}
}
#[test]
fn a_degenerate_poisson_counts_nothing() {
let mut r = Rng::new(1);
assert_eq!(r.poisson(0.0), 0);
assert_eq!(r.poisson(-5.0), 0);
assert_eq!(r.poisson(f64::NAN), 0);
assert!(r.poisson(1e12) > 0);
}
#[test]
fn uniform_hemisphere_is_not_cosine_weighted() {
let mut r = Rng::new(5);
let n = DVec3::Z;
const N: usize = 50_000;
let mean_cos: f64 = (0..N).map(|_| r.on_hemisphere(n).dot(n)).sum::<f64>() / N as f64;
assert!((mean_cos - 0.5).abs() < 0.01, "got {mean_cos}");
}
}