use std::f32::consts::{SQRT_2, TAU};
use bevy::math::{Quat, Rect, Vec2, Vec3};
use bevy::reflect::Reflect;
use bevy::transform::components::Transform;
use crate::rng::SeededRng;
const CANDIDATES: u32 = 30;
const TRUNK_STREAM: u32 = 0;
const GROVE_STREAM: u32 = 1;
const GROVE_SPECIES_STREAM: u32 = 2;
const TREE_STREAMS: u32 = 16;
const WARP_X_SALT: i32 = 0x3A5C_29E1;
const WARP_Y_SALT: i32 = 0x0F1E_7B4D;
const GROVE_RAGGEDNESS: f32 = 0.35;
const GROVE_BEND: f32 = 0.5;
#[derive(Clone, Debug, PartialEq, Reflect)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Scatter {
pub area: Rect,
pub spacing: f32,
pub seed: i32,
pub species: Vec<f32>,
pub grove_size: f32,
pub scale: (f32, f32),
}
impl Scatter {
pub fn new(area: Rect, spacing: f32) -> Self {
Self { area, spacing, seed: 0, species: Vec::new(), grove_size: 0.0, scale: (0.85, 1.15) }
}
pub fn points(&self) -> Vec<ScatterPoint> {
self.points_where(|_| 1.0)
}
pub fn points_where(&self, mut density: impl FnMut(Vec2) -> f32) -> Vec<ScatterPoint> {
let trunks = poisson_disc_stream(self.area, self.spacing, self.seed, TRUNK_STREAM);
let groves = Groves::new(self);
let (small, large) = self.scale;
trunks
.into_iter()
.enumerate()
.filter_map(|(i, position)| {
let mut rng = SeededRng::from_parts(self.seed, TREE_STREAMS.wrapping_add(i as u32));
let keep = rng.next_f32();
let yaw = rng.range(0.0, TAU);
let scale = rng.range(small, large);
let variant = (rng.next_u64() >> 32) as u32;
let pick = rng.next_f32();
if keep >= density(position).clamp(0.0, 1.0) {
return None;
}
let species = match &groves {
Some(groves) => groves.species_at(position),
None => weighted(&self.species, pick),
};
Some(ScatterPoint { position, yaw, scale, species, variant })
})
.collect()
}
}
#[derive(Clone, Copy, Debug, PartialEq, Reflect)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ScatterPoint {
pub position: Vec2,
pub yaw: f32,
pub scale: f32,
pub species: usize,
pub variant: u32,
}
impl ScatterPoint {
pub fn transform(&self, ground: f32) -> Transform {
Transform::from_xyz(self.position.x, ground, self.position.y)
.with_rotation(Quat::from_rotation_y(self.yaw))
.with_scale(Vec3::splat(self.scale))
}
}
pub fn poisson_disc(area: Rect, spacing: f32, seed: i32) -> Vec<Vec2> {
poisson_disc_stream(area, spacing, seed, TRUNK_STREAM)
}
fn poisson_disc_stream(area: Rect, spacing: f32, seed: i32, stream: u32) -> Vec<Vec2> {
let size = area.size();
if !(spacing > 0.0 && size.x > 0.0 && size.y > 0.0 && size.is_finite() && spacing.is_finite()) {
return Vec::new();
}
let mut rng = SeededRng::from_parts(seed, stream);
let mut grid = Grid::new(area, spacing / SQRT_2);
let mut points = Vec::new();
let mut active = Vec::new();
let first = area.min + Vec2::new(rng.next_f32(), rng.next_f32()) * size;
grid.insert(first, 0);
points.push(first);
active.push(0usize);
let spacing_sq = spacing * spacing;
while !active.is_empty() {
let slot = (rng.next_f32() * active.len() as f32) as usize % active.len();
let centre = points[active[slot]];
let mut placed = false;
for _ in 0..CANDIDATES {
let angle = rng.range(0.0, TAU);
let radius = spacing * (1.0 + 3.0 * rng.next_f32()).sqrt();
let candidate = centre + Vec2::from_angle(angle) * radius;
if !area.contains(candidate) {
continue;
}
let crowded = grid.around(candidate, 2).any(|i| points[i].distance_squared(candidate) < spacing_sq);
if !crowded {
grid.insert(candidate, points.len());
active.push(points.len());
points.push(candidate);
placed = true;
break;
}
}
if !placed {
active.swap_remove(slot);
}
}
points
}
struct Grid {
min: Vec2,
cell: f32,
width: usize,
height: usize,
cells: Vec<u32>,
}
impl Grid {
const EMPTY: u32 = u32::MAX;
fn new(area: Rect, cell: f32) -> Self {
let size = area.size();
let width = (size.x / cell).ceil().max(1.0) as usize;
let height = (size.y / cell).ceil().max(1.0) as usize;
let count = width.checked_mul(height).filter(|&n| n < Self::EMPTY as usize);
let count = count.expect("scatter: the area is too large for its spacing");
Self { min: area.min, cell, width, height, cells: vec![Self::EMPTY; count] }
}
fn cell_of(&self, p: Vec2) -> (usize, usize) {
let c = ((p - self.min) / self.cell).floor();
(
(c.x.max(0.0) as usize).min(self.width - 1),
(c.y.max(0.0) as usize).min(self.height - 1),
)
}
fn insert(&mut self, p: Vec2, index: usize) {
let (x, y) = self.cell_of(p);
self.cells[y * self.width + x] = index as u32;
}
fn around(&self, p: Vec2, reach: usize) -> impl Iterator<Item = usize> + '_ {
let (cx, cy) = self.cell_of(p);
let xs = cx.saturating_sub(reach)..=(cx + reach).min(self.width - 1);
let ys = cy.saturating_sub(reach)..=(cy + reach).min(self.height - 1);
ys.flat_map(move |y| xs.clone().map(move |x| self.cells[y * self.width + x]))
.filter(|&i| i != Self::EMPTY)
.map(|i| i as usize)
}
}
struct Groves {
centres: Vec<Vec2>,
species: Vec<usize>,
grid: Grid,
size: f32,
seed: i32,
}
impl Groves {
fn new(scatter: &Scatter) -> Option<Self> {
let size = scatter.grove_size;
if !(size > 0.0 && size.is_finite()) || scatter.species.len() < 2 {
return None;
}
let area = Rect::from_corners(scatter.area.min - size, scatter.area.max + size);
let centres = poisson_disc_stream(area, size, scatter.seed, GROVE_STREAM);
let mut rng = SeededRng::from_parts(scatter.seed, GROVE_SPECIES_STREAM);
let species = centres.iter().map(|_| weighted(&scatter.species, rng.next_f32())).collect();
let mut grid = Grid::new(area, size / SQRT_2);
for (i, ¢re) in centres.iter().enumerate() {
grid.insert(centre, i);
}
Some(Self { centres, species, grid, size, seed: scatter.seed })
}
fn species_at(&self, p: Vec2) -> usize {
let q = p / (GROVE_BEND * self.size);
let warp = Vec2::new(value_noise(q, self.seed ^ WARP_X_SALT), value_noise(q, self.seed ^ WARP_Y_SALT));
let p = p + warp * (GROVE_RAGGEDNESS * self.size);
let mut best = (f32::INFINITY, 0);
for reach in 1..=self.grid.width.max(self.grid.height) {
for i in self.grid.around(p, reach) {
let d = self.centres[i].distance(p);
if d < best.0 {
best = (d, self.species[i]);
}
}
if best.0 <= reach as f32 * self.grid.cell {
break;
}
}
best.1
}
}
fn value_noise(p: Vec2, seed: i32) -> f32 {
let cell = p.floor();
let f = p - cell;
let s = f * f * (Vec2::splat(3.0) - 2.0 * f);
let (x, y) = (cell.x as i32, cell.y as i32);
let at = |dx: i32, dy: i32| lattice(x.wrapping_add(dx), y.wrapping_add(dy), seed);
let bottom = at(0, 0) + (at(1, 0) - at(0, 0)) * s.x;
let top = at(0, 1) + (at(1, 1) - at(0, 1)) * s.x;
bottom + (top - bottom) * s.y
}
fn lattice(x: i32, y: i32, seed: i32) -> f32 {
let mut h = (x as u32).wrapping_mul(0x8DA6_B343)
^ (y as u32).wrapping_mul(0xD816_3841)
^ (seed as u32).wrapping_mul(0xCB1A_B31F);
h ^= h >> 15;
h = h.wrapping_mul(0x2C1B_3C6D);
h ^= h >> 12;
h = h.wrapping_mul(0x297A_2D39);
h ^= h >> 15;
(h >> 8) as f32 / (1u32 << 23) as f32 - 1.0
}
fn weighted(weights: &[f32], u: f32) -> usize {
let total: f32 = weights.iter().map(|w| w.max(0.0)).sum();
if total <= 0.0 {
return 0;
}
let mut left = u * total;
for (i, &w) in weights.iter().enumerate() {
let w = w.max(0.0);
if w > 0.0 && left < w {
return i;
}
left -= w;
}
weights.iter().rposition(|&w| w > 0.0).unwrap_or(0)
}
#[cfg(test)]
mod tests {
use super::*;
fn square(half: f32) -> Rect {
Rect::new(-half, -half, half, half)
}
#[test]
fn trunks_keep_their_spacing_stay_inside_and_leave_no_holes() {
let area = Rect::new(-60.0, -40.0, 60.0, 40.0);
let spacing = 5.0;
let points = poisson_disc(area, spacing, 7);
for (i, a) in points.iter().enumerate() {
assert!(area.contains(*a), "{a} outside");
for b in &points[i + 1..] {
assert!(a.distance(*b) >= spacing - 1e-4, "{a} and {b} closer than {spacing}");
}
}
let mut worst: f32 = 0.0;
for x in 0..=120 {
for y in 0..=80 {
let spot = Vec2::new(-60.0 + x as f32, -40.0 + y as f32);
let nearest = points.iter().map(|p| p.distance(spot)).fold(f32::INFINITY, f32::min);
worst = worst.max(nearest);
}
}
assert!(worst <= 2.0 * spacing, "a hole: a spot {worst} m from the nearest trunk");
let per_area = points.len() as f32 * spacing * spacing / (120.0 * 80.0);
assert!((0.55..0.8).contains(&per_area), "{per_area} trunks per spacing²");
}
#[test]
fn a_seed_is_a_forest() {
let a = Scatter::new(square(50.0), 6.0);
assert_eq!(a.points(), a.points());
assert_ne!(a.points(), Scatter { seed: 1, ..a.clone() }.points());
}
#[test]
fn nothing_to_cover_gives_no_trees() {
assert!(poisson_disc(Rect::new(0.0, 0.0, 0.0, 10.0), 1.0, 0).is_empty());
assert!(poisson_disc(square(10.0), 0.0, 0).is_empty());
assert!(poisson_disc(square(10.0), f32::NAN, 0).is_empty());
assert_eq!(poisson_disc(square(1.0), 5.0, 0).len(), 1);
}
#[test]
fn a_density_mask_only_removes_trees() {
let forest = Scatter { species: vec![1.0, 1.0], grove_size: 30.0, ..Scatter::new(square(80.0), 5.0) };
let all = forest.points();
let cleared = forest.points_where(|p| if p.x < 0.0 { 0.0 } else { 1.0 });
let east: Vec<_> = all.iter().filter(|t| t.position.x >= 0.0).copied().collect();
assert_eq!(cleared, east, "the trees east of the clearing are untouched, species and all");
let sparse = forest.points_where(|_| 0.25);
let share = sparse.len() as f32 / all.len() as f32;
assert!((0.18..0.32).contains(&share), "a density of 0.25 kept {share}");
assert!(sparse.iter().all(|t| all.contains(t)));
}
#[test]
fn species_follow_their_weights_and_grow_in_groves() {
let area = square(300.0);
let together = |trees: &[ScatterPoint]| {
let same = trees
.iter()
.filter(|a| {
let nearest = trees
.iter()
.filter(|b| b.position != a.position)
.min_by(|b, c| {
a.position.distance_squared(b.position).total_cmp(&a.position.distance_squared(c.position))
})
.unwrap();
nearest.species == a.species
})
.count();
same as f32 / trees.len() as f32
};
let share = |trees: &[ScatterPoint]| trees.iter().filter(|t| t.species == 1).count() as f32 / trees.len() as f32;
let mixed = Scatter { species: vec![3.0, 1.0, 0.0], ..Scatter::new(area, 8.0) }.points();
assert!((0.22..0.28).contains(&share(&mixed)), "{}", share(&mixed));
assert!(mixed.iter().all(|t| t.species != 2), "a species of weight 0 grew");
assert!((0.57..0.68).contains(&together(&mixed)), "{}", together(&mixed));
let groves = Scatter { species: vec![3.0, 1.0, 0.0], grove_size: 60.0, ..Scatter::new(area, 8.0) }.points();
assert!((0.12..0.38).contains(&share(&groves)), "{}", share(&groves));
assert!(groves.iter().all(|t| t.species != 2), "a species of weight 0 grew");
assert!(together(&groves) > 0.85, "groves: {}", together(&groves));
}
#[test]
fn a_tree_stands_where_it_was_scattered() {
let tree = ScatterPoint { position: Vec2::new(3.0, -4.0), yaw: 1.0, scale: 1.2, species: 0, variant: 9 };
let t = tree.transform(2.5);
assert_eq!(t.translation, Vec3::new(3.0, 2.5, -4.0));
assert_eq!(t.scale, Vec3::splat(1.2));
assert!((t.rotation.to_euler(bevy::math::EulerRot::YXZ).0 - 1.0).abs() < 1e-5);
}
}