use bevy::math::Vec2;
use crate::foliage::LEAF_OUTLINE;
use crate::rng::SeededRng;
pub const BIRTH_DISTANCE: f32 = 0.06;
pub const STEP: f32 = 0.02;
pub const KILL_DISTANCE: f32 = 0.04;
pub const ATTRACTION_DISTANCE: f32 = 0.4;
pub const MAX_ITERATIONS: u32 = 400;
pub const TIP_WIDTH: f32 = 0.008;
pub const IMAGE_WIDTH: u32 = 128;
pub const IMAGE_HEIGHT: u32 = 256;
const PETIOLE: Vec2 = Vec2::new(0.0, -1.0);
const MAX_MISSES: u32 = 4_000;
const BOX: (Vec2, Vec2) = (Vec2::new(-0.8, -1.0), Vec2::new(0.8, 1.0));
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct VeinNode {
pub position: Vec2,
pub parent: Option<u32>,
pub width: f32,
}
#[derive(Clone, Debug, PartialEq)]
pub struct Venation {
pub nodes: Vec<VeinNode>,
pub sources: usize,
pub reached: usize,
}
pub fn inside_leaf(p: Vec2) -> bool {
let mut inside = false;
let n = LEAF_OUTLINE.len();
for i in 0..n {
let (a, b) = (LEAF_OUTLINE[i], LEAF_OUTLINE[(i + 1) % n]);
if (a.y > p.y) != (b.y > p.y) && p.x < (b.x - a.x) * (p.y - a.y) / (b.y - a.y) + a.x {
inside = !inside;
}
}
inside
}
fn throw_sources(rng: &mut SeededRng) -> Vec<Vec2> {
let mut sources: Vec<Vec2> = Vec::new();
let mut misses = 0;
while misses < MAX_MISSES {
let p = Vec2::new(rng.range(BOX.0.x, BOX.1.x), rng.range(BOX.0.y, BOX.1.y));
if inside_leaf(p) && sources.iter().all(|s| s.distance_squared(p) >= BIRTH_DISTANCE * BIRTH_DISTANCE) {
sources.push(p);
misses = 0;
} else {
misses += 1;
}
}
sources
}
pub fn grow(seed: i32) -> Venation {
let mut rng = SeededRng::from_parts(seed, 0x7E1A);
let sources = throw_sources(&mut rng);
let mut alive = vec![true; sources.len()];
let mut nodes = vec![VeinNode { position: PETIOLE, parent: None, width: 0.0 }];
for _ in 0..MAX_ITERATIONS {
if !alive.iter().any(|&a| a) {
break;
}
let mut pull = vec![(Vec2::ZERO, 0u32); nodes.len()];
let mut any = false;
for (source, _) in sources.iter().zip(&alive).filter(|(_, alive)| **alive) {
let closest = nodes
.iter()
.enumerate()
.map(|(i, n)| (i, n.position.distance_squared(*source)))
.filter(|&(_, d)| d <= ATTRACTION_DISTANCE * ATTRACTION_DISTANCE)
.min_by(|a, b| a.1.total_cmp(&b.1));
if let Some((i, d)) = closest {
pull[i].0 += (*source - nodes[i].position) / d.sqrt().max(1.0e-6);
pull[i].1 += 1;
any = true;
}
}
if !any {
break;
}
let grown = nodes.len();
for (i, &(direction, count)) in pull.iter().enumerate().take(grown) {
if count == 0 {
continue;
}
let direction = direction.normalize_or_zero();
if direction == Vec2::ZERO {
continue;
}
let position = nodes[i].position + direction * STEP;
nodes.push(VeinNode { position, parent: Some(i as u32), width: 0.0 });
}
for (source, alive) in sources.iter().zip(&mut alive) {
if *alive
&& nodes[grown..]
.iter()
.any(|n| n.position.distance_squared(*source) <= KILL_DISTANCE * KILL_DISTANCE)
{
*alive = false;
}
}
}
let mut cubes = vec![0.0f32; nodes.len()];
let mut has_child = vec![false; nodes.len()];
for i in (0..nodes.len()).rev() {
let width = if has_child[i] { cubes[i].cbrt() } else { TIP_WIDTH };
nodes[i].width = width;
if let Some(parent) = nodes[i].parent {
cubes[parent as usize] += width * width * width;
has_child[parent as usize] = true;
}
}
let reached = alive.iter().filter(|&&a| !a).count();
Venation { nodes, sources: sources.len(), reached }
}
pub fn rasterize(veins: &Venation, width: u32, height: u32) -> Vec<u8> {
let (w, h) = (width as usize, height as usize);
let pixel = Vec2::new(2.0 / width as f32, 2.0 / height as f32);
let half_pixel = 0.5 * 0.5 * (pixel.x + pixel.y);
let mut image = vec![0u8; w * h];
for node in &veins.nodes {
let Some(parent) = node.parent else { continue };
let (a, b) = (veins.nodes[parent as usize].position, node.position);
let radius = (0.5 * node.width).max(half_pixel);
let reach = radius + 2.0 * half_pixel;
let to_pixel = |p: Vec2| Vec2::new((p.x + 1.0) / pixel.x, (p.y + 1.0) / pixel.y);
let (lo, hi) = (to_pixel(a.min(b) - Vec2::splat(reach)), to_pixel(a.max(b) + Vec2::splat(reach)));
let (x0, x1) = (lo.x.floor().max(0.0) as usize, (hi.x.ceil() as usize).min(w - 1));
let (y0, y1) = (lo.y.floor().max(0.0) as usize, (hi.y.ceil() as usize).min(h - 1));
let ab = b - a;
let length_squared = ab.length_squared().max(1.0e-12);
for y in y0..=y1 {
for x in x0..=x1 {
let p = Vec2::new(-1.0 + (x as f32 + 0.5) * pixel.x, -1.0 + (y as f32 + 0.5) * pixel.y);
let t = ((p - a).dot(ab) / length_squared).clamp(0.0, 1.0);
let distance = p.distance(a + ab * t);
let coverage = ((radius + half_pixel - distance) / (2.0 * half_pixel)).clamp(0.0, 1.0);
let texel = &mut image[y * w + x];
*texel = (*texel).max((coverage * 255.0).round() as u8);
}
}
}
image
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_same_seed_grows_the_same_veins_and_another_grows_others() {
let (a, b) = (grow(5), grow(5));
assert_eq!(a, b);
assert_ne!(a.nodes, grow(6).nodes);
assert_eq!(rasterize(&a, 32, 64), rasterize(&b, 32, 64));
}
#[test]
fn veins_reach_nearly_every_source_and_stay_in_the_leaf() {
for seed in [0, 1, 7, 42, -3] {
let veins = grow(seed);
assert!(veins.sources > 200, "seed {seed}: only {} sources", veins.sources);
assert!(
veins.reached * 10 >= veins.sources * 9,
"seed {seed}: reached {} of {} sources",
veins.reached,
veins.sources
);
for (i, node) in veins.nodes.iter().enumerate() {
if let Some(parent) = node.parent {
assert!((parent as usize) < i, "seed {seed}: node {i} grew from a later node");
let inside = inside_leaf(node.position)
|| (0..8).any(|k| {
let angle = k as f32 * std::f32::consts::FRAC_PI_4;
inside_leaf(node.position - Vec2::from_angle(angle) * STEP)
});
assert!(inside, "seed {seed}: node {i} at {:?} is outside the leaf", node.position);
}
}
}
}
#[test]
fn widths_follow_murrays_law() {
let veins = grow(3);
let mut cubes = vec![0.0f32; veins.nodes.len()];
for node in &veins.nodes {
if let Some(parent) = node.parent {
cubes[parent as usize] += node.width.powi(3);
}
}
let mut forks = 0;
for (node, cube) in veins.nodes.iter().zip(&cubes) {
if *cube > 0.0 {
assert!((node.width.powi(3) - cube).abs() <= 1.0e-6 + 1.0e-4 * cube, "{node:?}: {cube}");
forks += 1;
} else {
assert_eq!(node.width, TIP_WIDTH);
}
}
assert!(forks > 10, "only {forks} nodes carry veins");
assert!(veins.nodes[0].width >= veins.nodes.iter().map(|n| n.width).fold(0.0, f32::max) - 1e-6);
assert!(veins.nodes[0].width > 4.0 * TIP_WIDTH);
}
#[test]
fn every_drawn_vein_pixel_lies_inside_the_leaf() {
for seed in [1, 2, 3] {
let veins = grow(seed);
let image = rasterize(&veins, IMAGE_WIDTH, IMAGE_HEIGHT);
let (w, h) = (IMAGE_WIDTH as usize, IMAGE_HEIGHT as usize);
let mut drawn = 0;
for y in 0..h {
for x in 0..w {
if image[y * w + x] < 128 {
continue;
}
drawn += 1;
let p = Vec2::new(-1.0 + (x as f32 + 0.5) * 2.0 / w as f32, -1.0 + (y as f32 + 0.5) * 2.0 / h as f32);
let near_inside = inside_leaf(p)
|| (0..8).any(|k| inside_leaf(p + Vec2::from_angle(k as f32 * std::f32::consts::FRAC_PI_4) * 0.04));
assert!(near_inside, "seed {seed}: vein pixel ({x}, {y}) at {p:?} is outside the leaf");
}
}
assert!(drawn > 500, "seed {seed}: only {drawn} vein pixels");
let column: Vec<u8> =
(0..h).map(|y| (w / 2 - 6..w / 2 + 6).map(|x| image[y * w + x]).max().unwrap_or(0)).collect();
assert!(column.iter().filter(|&&v| v >= 128).count() > h / 2, "seed {seed}: no midrib");
}
}
}