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::ground::FloraGround;
use crate::params::TreeParams;
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, density: impl FnMut(Vec2) -> f32) -> Vec<ScatterPoint> {
self.points_with(density, |_, _| 1.0)
}
pub fn points_with(
&self,
mut density: impl FnMut(Vec2) -> f32,
mut weight: impl FnMut(Vec2, usize) -> 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;
let global = if self.species.is_empty() { vec![1.0] } else { self.species.clone() };
let mut shares = vec![0.0; global.len()];
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;
}
for (species, share) in shares.iter_mut().enumerate() {
*share = weight(position, species).max(0.0);
}
let grove = groves.as_ref().and_then(|groves| groves.species_at(position, &shares));
let species = grove.unwrap_or_else(|| {
for (share, global) in shares.iter_mut().zip(&global) {
*share *= global;
}
weighted(&shares, 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 transform_on(&self, ground: &FloraGround, params: &TreeParams) -> Transform {
self.transform(ground.footing(self.position, params.base_radius() * 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, shares: &[f32]) -> Option<usize> {
let most = shares.iter().copied().fold(0.0, f32::max);
if most <= 0.0 {
return None;
}
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, None);
for reach in 1..=self.grid.width.max(self.grid.height) {
for i in self.grid.around(p, reach) {
let share = shares.get(self.species[i]).copied().unwrap_or(0.0);
if share <= 0.0 {
continue;
}
let d = self.centres[i].distance(p) / share;
if d < best.0 {
best = (d, Some(self.species[i]));
}
}
if best.0 <= reach as f32 * self.grid.cell / most {
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_stand_picks_its_species_inside_and_leaves_the_forest_outside_as_it_was() {
let centre = Vec2::new(20.0, -10.0);
let inside = |p: Vec2| p.distance(centre) < 50.0;
let stand = |p: Vec2, species: usize| if inside(p) { (species == 3) as u8 as f32 } else { 1.0 };
let density = |p: Vec2| if p.x > 100.0 { 0.0 } else { 1.0 };
let groves = Scatter { species: vec![1.0, 2.0, 1.0, 1.0], grove_size: 30.0, ..Scatter::new(square(150.0), 5.0) };
for forest in [groves.clone(), Scatter { grove_size: 0.0, ..groves }] {
let plain = forest.points_where(density);
let stood = forest.points_with(density, stand);
assert_eq!(stood, forest.points_with(density, stand), "a seed is a forest");
assert_eq!(plain.len(), stood.len(), "the weights move no tree");
let mut within = 0;
for (before, after) in plain.iter().zip(&stood) {
if inside(after.position) {
assert_eq!((after.position, after.species), (before.position, 3));
within += 1;
} else {
assert_eq!(after, before, "a tree outside the stand changed");
}
}
assert!(within > 200, "{within} trees in the stand");
}
let forest = Scatter { species: vec![1.0, 2.0, 1.0, 1.0], grove_size: 30.0, ..Scatter::new(square(150.0), 4.0) };
let rim = |p: Vec2, species: usize| {
let out = ((p.distance(centre) - 30.0) / 30.0).clamp(0.0, 1.0);
if species == 3 { 1.0 - out } else { out }
};
let trees = forest.points_with(|_| 1.0, rim);
let at = |t: &ScatterPoint| t.position.distance(centre);
assert!(trees.iter().filter(|t| at(t) < 30.0).all(|t| t.species == 3));
assert!(trees.iter().filter(|t| at(t) > 60.0).all(|t| t.species != 3));
let stand_reaches = trees.iter().filter(|t| t.species == 3).map(at).fold(0.0, f32::max);
let forest_reaches = trees.iter().filter(|t| t.species != 3).map(at).fold(f32::INFINITY, f32::min);
assert!(stand_reaches - forest_reaches > 8.0, "a circular edge: the stand to {stand_reaches} m, the forest from {forest_reaches} m");
}
#[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);
}
}