use std::ops::Range;
use std::sync::RwLock;
use bevy::asset::{AssetEvent, AssetId, Assets, Handle, RenderAssetUsages};
use bevy::camera::Camera3d;
use bevy::camera::primitives::Aabb;
use bevy::camera::visibility::Visibility;
use bevy::ecs::prelude::*;
use bevy::math::{Vec2, Vec3, Vec3Swizzles};
use bevy::mesh::{
Indices, Mesh, Mesh3d, MeshVertexAttribute, MeshVertexBufferLayoutRef, PrimitiveTopology, VertexAttributeValues,
VertexFormat,
};
use bevy::pbr::MeshMaterial3d;
use bevy::platform::collections::HashMap;
use bevy::reflect::Reflect;
use bevy::render::render_resource::{RenderPipelineDescriptor, SpecializedMeshPipelineError};
use bevy::shader::{Shader, ShaderDefVal};
use bevy::time::Time;
use bevy::transform::components::{GlobalTransform, Transform};
use crate::felling::{Felled, TreeLanded};
use crate::fire::{FloraFire, burnt_front};
use crate::ground::FloraGround;
use crate::mesh::ATTRIBUTE_LEAF_SWAY;
use crate::plugin::{Paint, ProceduralTree, TreeRender};
use crate::pool::{PoolMaterials, PooledTree, TreePool};
use crate::rng::SeededRng;
use crate::shading::{FloraExtension, FloraMaterial, FloraSeason, FloraWind, TreeShake, TreeShakes};
#[derive(Resource, Clone, Copy, Debug, PartialEq, Reflect)]
#[reflect(Resource)]
pub struct FallingLeaves {
pub per_shake: f32,
pub per_landing: u32,
pub autumn_distance: f32,
pub rest: f32,
pub max_leaves: u32,
}
impl Default for FallingLeaves {
fn default() -> Self {
Self { per_shake: 12.0, per_landing: 80, autumn_distance: 50.0, rest: 6.0, max_leaves: 5000 }
}
}
#[derive(Message, Clone, Copy, Debug, PartialEq, Eq)]
pub struct DropLeaves {
pub tree: Entity,
pub count: u32,
}
#[derive(Component, Clone, Copy, Debug, PartialEq)]
pub struct LeafBurst {
pub tree: Option<Entity>,
pub leaves: u32,
pub gone_at: f32,
}
pub(crate) const ATTRIBUTE_FALL_ANCHOR: MeshVertexAttribute =
MeshVertexAttribute::new("FallAnchor", 0x666C_6F72_6120, VertexFormat::Float32x4);
pub(crate) const ATTRIBUTE_FALL_PATH: MeshVertexAttribute =
MeshVertexAttribute::new("FallPath", 0x666C_6F72_6121, VertexFormat::Float32x4);
pub(crate) const ATTRIBUTE_FALL_FACE: MeshVertexAttribute =
MeshVertexAttribute::new("FallFace", 0x666C_6F72_6122, VertexFormat::Float32x4);
static SHADERS: RwLock<Option<(Handle<Shader>, Handle<Shader>)>> = RwLock::new(None);
pub(crate) fn set_shaders(main: Handle<Shader>, prepass: Handle<Shader>) {
if let Ok(mut slot) = SHADERS.write() {
*slot = Some((main, prepass));
}
}
pub(crate) fn specialize_falling(
descriptor: &mut RenderPipelineDescriptor,
layout: &MeshVertexBufferLayoutRef,
) -> Result<(), SpecializedMeshPipelineError> {
let Some((main, prepass)) = SHADERS.read().ok().and_then(|shaders| shaders.clone()) else { return Ok(()) };
let in_prepass = descriptor.vertex.shader_defs.contains(&ShaderDefVal::from("PREPASS_PIPELINE"));
descriptor.vertex.shader = if in_prepass { prepass } else { main };
let fall = layout.0.get_layout(&[
ATTRIBUTE_FALL_ANCHOR.at_shader_location(12),
ATTRIBUTE_FALL_PATH.at_shader_location(13),
ATTRIBUTE_FALL_FACE.at_shader_location(14),
])?;
descriptor.vertex.buffers[0].attributes.extend(fall.attributes);
Ok(())
}
const PICK_UP: f32 = 0.35;
const DRIFT: f32 = 1.5;
const SHRINK: f32 = 1.0;
const SWING: f32 = 0.4;
const LET_GO: f32 = 0.5;
const SPEED: Range<f32> = 0.9..1.5;
const LOWEST: f32 = 0.05;
const CACHED_MESHES: usize = 64;
const SHED_EVERY: f32 = 0.1;
const SEASON_JUMP: f32 = 0.1;
const WRAP: f32 = 3600.0;
#[derive(Resource, Default)]
pub(crate) struct FallCaches {
materials: HashMap<AssetId<FloraMaterial>, Handle<FloraMaterial>>,
cards: HashMap<AssetId<Mesh>, MeshCards>,
}
impl FallCaches {
fn cards(&mut self, id: AssetId<Mesh>, source: &Mesh) -> &MeshCards {
if self.cards.len() >= CACHED_MESHES && !self.cards.contains_key(&id) {
self.cards.clear();
}
let size = (source.count_vertices(), source.indices().map_or(0, Indices::len));
let read = self.cards.entry(id).or_insert_with(|| MeshCards::read(source, size));
if read.size != size {
*read = MeshCards::read(source, size);
}
read
}
fn material(&mut self, leaves: &Handle<FloraMaterial>, flora: &mut Assets<FloraMaterial>) -> Option<Handle<FloraMaterial>> {
let source = flora.get(leaves)?;
let look = FloraMaterial {
base: source.base.clone(),
extension: FloraExtension { falling: true, ..source.extension.clone() },
};
if let Some(made) = self.materials.get(&leaves.id())
&& flora.get(made).is_some_and(|made| made.extension == look.extension)
{
return Some(made.clone());
}
let made = flora.add(look);
self.materials.insert(leaves.id(), made.clone());
Some(made)
}
}
struct MeshCards {
size: (usize, usize),
cards: Vec<Card>,
by_moment: Vec<u32>,
}
impl MeshCards {
fn read(mesh: &Mesh, size: (usize, usize)) -> Self {
let cards = leaf_cards(mesh);
let mut by_moment: Vec<u32> = (0..cards.len() as u32).filter(|&k| !cards[k as usize].flower).collect();
by_moment.sort_by(|a, b| cards[*a as usize].moment.total_cmp(&cards[*b as usize].moment));
Self { size, cards, by_moment }
}
fn shed_between(&self, from: f32, to: f32) -> impl Iterator<Item = &Card> {
let moment = |k: &u32| self.cards[*k as usize].moment;
let start = self.by_moment.partition_point(|k| moment(k) < from);
let end = self.by_moment.partition_point(|k| moment(k) < to);
self.by_moment[start..end].iter().map(|&k| &self.cards[k as usize])
}
}
pub(crate) fn landing_time(height: f32, speed: f32) -> f32 {
if height <= 0.0 {
return 0.0;
}
let mut t = height / speed + PICK_UP;
for _ in 0..4 {
let e = (-t / PICK_UP).exp();
let f = speed * (t - PICK_UP * (1.0 - e)) - height;
t -= f / (speed * (1.0 - e)).max(1.0e-3);
}
t.max(0.0)
}
pub(crate) fn leaf_season_moment(phase: f32) -> f32 {
let x = phase * 7.31 + 0.137;
x - x.floor()
}
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct Card {
pub vertices: Range<usize>,
pub indices: Range<usize>,
pub moment: f32,
pub flower: bool,
}
impl Card {
pub fn on_tree(&self, shed: Option<f32>, burnt: f32) -> bool {
!self.flower && self.moment >= burnt && shed.is_none_or(|shed| self.moment >= shed)
}
}
pub(crate) fn leaf_cards(mesh: &Mesh) -> Vec<Card> {
let Some(VertexAttributeValues::Float32x4(sway)) = mesh.attribute(ATTRIBUTE_LEAF_SWAY) else {
return Vec::new();
};
let Some(indices) = mesh.indices() else { return Vec::new() };
let colors = match mesh.attribute(Mesh::ATTRIBUTE_COLOR) {
Some(VertexAttributeValues::Float32x4(colors)) => Some(colors),
_ => None,
};
let mut cards: Vec<Card> = Vec::new();
let mut card_of = Vec::with_capacity(sway.len());
for (v, s) in sway.iter().enumerate() {
match cards.last_mut() {
Some(card) if sway[card.vertices.start][3].to_bits() == s[3].to_bits() => card.vertices.end = v + 1,
_ => cards.push(Card {
vertices: v..v + 1,
indices: 0..0,
moment: leaf_season_moment(s[3]),
flower: colors.is_some_and(|colors| colors[v][3] < 0.5),
}),
}
card_of.push(cards.len() - 1);
}
let indices: Vec<usize> = indices.iter().collect();
for (k, triangle) in indices.as_chunks::<3>().0.iter().enumerate() {
let card = &mut cards[card_of[triangle[0]]];
if card.indices.is_empty() {
card.indices = 3 * k..3 * k;
}
card.indices.end = 3 * k + 3;
}
cards.retain(|card| !card.indices.is_empty());
cards
}
#[derive(Clone, Copy, Debug)]
pub(crate) struct Fall {
pub now: f32,
pub rest: f32,
pub drift: Vec2,
}
impl Fall {
fn now(time: &Time, settings: &FallingLeaves, wind: &FloraWind) -> Self {
Self {
now: time.elapsed_secs_wrapped(),
rest: settings.rest,
drift: wind.direction.normalize_or_zero() * (wind.strength.max(0.0) * DRIFT),
}
}
}
pub(crate) struct Burst {
pub mesh: Mesh,
pub aabb: Aabb,
pub leaves: u32,
pub life: f32,
}
pub(crate) struct BurstBuilder {
pub origin: Vec3,
out: BurstBuffers,
lo: Vec3,
hi: Vec3,
life: f32,
leaves: u32,
}
impl BurstBuilder {
pub fn new(origin: Vec3) -> Self {
Self {
origin,
out: BurstBuffers::default(),
lo: Vec3::splat(f32::INFINITY),
hi: Vec3::splat(f32::NEG_INFINITY),
life: 0.0,
leaves: 0,
}
}
#[allow(clippy::too_many_arguments)]
pub fn add<'a>(
&mut self,
source: &Mesh,
cards: impl IntoIterator<Item = &'a Card>,
tree: &GlobalTransform,
ground: impl Fn(Vec2) -> f32,
fall: Fall,
rng: &mut SeededRng,
mut release: impl FnMut(&Card, &mut SeededRng) -> f32,
) -> u32 {
let floats3 = |id| match source.attribute(id) {
Some(VertexAttributeValues::Float32x3(v)) => Some(v),
_ => None,
};
let floats4 = |id| match source.attribute(id) {
Some(VertexAttributeValues::Float32x4(v)) => Some(v),
_ => None,
};
let floats2 = |id| match source.attribute(id) {
Some(VertexAttributeValues::Float32x2(v)) => Some(v),
_ => None,
};
let (Some(positions), Some(normals), Some(uv0), Some(sway), Some(source_indices)) = (
floats3(Mesh::ATTRIBUTE_POSITION),
floats3(Mesh::ATTRIBUTE_NORMAL),
floats2(Mesh::ATTRIBUTE_UV_0),
floats4(ATTRIBUTE_LEAF_SWAY),
source.indices(),
) else {
return 0;
};
let uv1 = floats2(Mesh::ATTRIBUTE_UV_1);
let colors = floats4(Mesh::ATTRIBUTE_COLOR);
let source_indices: Vec<usize> = source_indices.iter().collect();
let origin = self.origin;
let affine = tree.affine();
let out = &mut self.out;
let mut added = 0;
for card in cards {
let world: Vec<Vec3> =
card.vertices.clone().map(|v| affine.transform_point3(Vec3::from_array(positions[v])) - origin).collect();
let anchor = world.iter().sum::<Vec3>() / world.len() as f32;
let reach = world.iter().map(|p| p.distance(anchor)).fold(0.0, f32::max);
let speed = rng.range(SPEED.start, SPEED.end);
let at = anchor + origin;
let mut landing = at.xz();
let mut floor = ground(landing);
for _ in 0..2 {
landing = at.xz() + fall.drift * landing_time(at.y - floor, speed);
floor = ground(landing);
}
let height = at.y - floor;
if height < LOWEST {
continue;
}
let land = landing_time(height, speed);
let delay = release(card, rng);
let rest = fall.rest.max(0.0) * rng.range(0.75, 1.25);
let seed = rng.next_f32();
self.life = self.life.max(delay + land + rest + SHRINK);
added += 1;
let first = &source_indices[card.indices.start..card.indices.start + 3];
let corner = |k: usize| affine.transform_point3(Vec3::from_array(positions[first[k]]));
let face = (corner(1) - corner(0)).cross(corner(2) - corner(0)).normalize_or(Vec3::Y);
let base = out.positions.len() as u32;
let local_floor = floor - origin.y;
for (k, v) in card.vertices.clone().enumerate() {
out.positions.push(world[k].to_array());
out.normals.push(affine.transform_vector3(Vec3::from_array(normals[v])).normalize_or(Vec3::Y).to_array());
out.uv0.push(uv0[v]);
if let Some(uv1) = uv1 {
out.uv1.push(uv1[v]);
}
if let Some(colors) = colors {
out.colors.push(colors[v]);
}
out.sway.push(sway[v]);
out.anchor.push(anchor.extend(fall.now + delay).to_array());
out.path.push([local_floor, seed, rest, speed]);
out.face.push(face.extend(0.0).to_array());
}
let start = card.vertices.start;
out.indices.extend(source_indices[card.indices.clone()].iter().map(|&i| base + (i - start) as u32));
let landed = Vec3::new(landing.x - origin.x, local_floor, landing.y - origin.z);
let margin = Vec3::splat(reach + SWING);
self.lo = self.lo.min(anchor - margin).min(landed - margin);
self.hi = self.hi.max(anchor + margin).max(landed + margin);
}
self.leaves += added;
added
}
pub fn finish(self) -> Option<Burst> {
(self.leaves > 0).then(|| Burst {
mesh: self.out.into_mesh(),
aabb: Aabb::from_min_max(self.lo, self.hi),
leaves: self.leaves,
life: self.life,
})
}
}
#[derive(Default)]
struct BurstBuffers {
positions: Vec<[f32; 3]>,
normals: Vec<[f32; 3]>,
uv0: Vec<[f32; 2]>,
uv1: Vec<[f32; 2]>,
colors: Vec<[f32; 4]>,
sway: Vec<[f32; 4]>,
anchor: Vec<[f32; 4]>,
path: Vec<[f32; 4]>,
face: Vec<[f32; 4]>,
indices: Vec<u32>,
}
impl BurstBuffers {
fn into_mesh(self) -> Mesh {
let mut mesh = Mesh::new(PrimitiveTopology::TriangleList, RenderAssetUsages::RENDER_WORLD)
.with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, self.positions)
.with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, self.normals)
.with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, self.uv0)
.with_inserted_attribute(ATTRIBUTE_LEAF_SWAY, self.sway)
.with_inserted_attribute(ATTRIBUTE_FALL_ANCHOR, self.anchor)
.with_inserted_attribute(ATTRIBUTE_FALL_PATH, self.path)
.with_inserted_attribute(ATTRIBUTE_FALL_FACE, self.face)
.with_inserted_indices(Indices::U32(self.indices));
if !self.uv1.is_empty() {
mesh.insert_attribute(Mesh::ATTRIBUTE_UV_1, self.uv1);
}
if !self.colors.is_empty() {
mesh.insert_attribute(Mesh::ATTRIBUTE_COLOR, self.colors);
}
mesh
}
}
fn smoothstep(edge: f32, x: f32) -> f32 {
let t = (x / edge.max(1.0e-4)).clamp(0.0, 1.0);
t * t * (3.0 - 2.0 * t)
}
#[allow(clippy::type_complexity)]
pub(crate) fn shake_leaves_loose(
shakes: Res<TreeShakes>,
settings: Res<FallingLeaves>,
mut seen: Local<Vec<TreeShake>>,
trees: Query<(Entity, &GlobalTransform), (Or<(With<TreeRender>, With<PooledTree>)>, Without<Felled>)>,
mut drops: MessageWriter<DropLeaves>,
) {
if !shakes.is_changed() {
return;
}
let new: Vec<TreeShake> = shakes.iter().filter(|shake| !seen.contains(shake)).copied().collect();
seen.clear();
seen.extend(shakes.iter().copied());
if settings.per_shake <= 0.0 {
return;
}
for shake in new {
let radius = shake.radius.max(0.01);
for (tree, at) in &trees {
let near = 1.0 - smoothstep(radius, at.translation().xz().distance(shake.at));
let count = (settings.per_shake * shake.strength.max(0.0) * near).round() as u32;
if count > 0 {
drops.write(DropLeaves { tree, count });
}
}
}
}
pub(crate) fn shed_landed_leaves(
mut landed: MessageReader<TreeLanded>,
settings: Res<FallingLeaves>,
mut drops: MessageWriter<DropLeaves>,
) {
for landing in landed.read() {
if settings.per_landing > 0 {
drops.write(DropLeaves { tree: landing.tree, count: settings.per_landing });
}
}
}
struct LeafSource {
mesh: Handle<Mesh>,
material: Handle<FloraMaterial>,
deciduous: bool,
}
fn leaf_source(
render: Option<&TreeRender>,
tree: Option<&ProceduralTree>,
pooled: Option<&PooledTree>,
pools: Option<&Assets<TreePool>>,
) -> Option<LeafSource> {
match (render, tree, pooled) {
(Some(render), Some(tree), _) => {
let (Paint::Flora { foliage, .. }, Some((_, mesh))) = (&render.paint, &render.foliage) else { return None };
Some(LeafSource { mesh: mesh.clone(), material: foliage.clone(), deciduous: tree.0.foliage.autumn.is_some() })
}
(_, _, Some(pooled)) => {
let pool = pools?.get(&pooled.pool)?;
let Some(PoolMaterials::Flora { leaves, .. }) = &pool.materials else { return None };
let variant = &pool.variants[pooled.variant as usize % pool.variants.len()];
Some(LeafSource {
mesh: variant.leaves[0].clone()?,
material: leaves.clone(),
deciduous: pool.params.foliage.autumn.is_some(),
})
}
_ => None,
}
}
fn spawn_burst(
commands: &mut Commands,
meshes: &mut Assets<Mesh>,
burst: Burst,
origin: Vec3,
material: Handle<FloraMaterial>,
tree: Option<Entity>,
time: &Time,
) {
commands.spawn((
Mesh3d(meshes.add(burst.mesh)),
MeshMaterial3d(material),
Transform::from_translation(origin),
GlobalTransform::from_translation(origin),
Visibility::default(),
burst.aabb,
LeafBurst { tree, leaves: burst.leaves, gone_at: time.elapsed_secs() + burst.life },
));
}
pub(crate) fn forget_changed_meshes(mut caches: ResMut<FallCaches>, mut changed: MessageReader<AssetEvent<Mesh>>) {
for event in changed.read() {
if let AssetEvent::Modified { id } | AssetEvent::Removed { id } | AssetEvent::Unused { id } = event
&& caches.cards.contains_key(id)
{
caches.cards.remove(id);
}
}
}
#[allow(clippy::too_many_arguments, clippy::type_complexity)]
pub(crate) fn drop_leaves(
mut commands: Commands,
mut drops: MessageReader<DropLeaves>,
settings: Res<FallingLeaves>,
time: Res<Time>,
wind: Res<FloraWind>,
season: Res<FloraSeason>,
ground: Option<Res<FloraGround>>,
mut meshes: ResMut<Assets<Mesh>>,
mut flora: ResMut<Assets<FloraMaterial>>,
pools: Option<Res<Assets<TreePool>>>,
trees: Query<(&GlobalTransform, Option<&TreeRender>, Option<&ProceduralTree>, Option<&PooledTree>, Option<&Felled>)>,
stumps: Query<&GlobalTransform>,
bursts: Query<&LeafBurst>,
mut caches: ResMut<FallCaches>,
fire: Option<Res<FloraFire>>,
) {
let mut live: u32 = bursts.iter().map(|burst| burst.leaves).sum();
let fall = Fall::now(&time, &settings, &wind);
for (k, drop) in drops.read().enumerate() {
let budget = settings.max_leaves.saturating_sub(live).min(drop.count) as usize;
if budget == 0 {
continue;
}
let Ok((at, render, tree, pooled, felled)) = trees.get(drop.tree) else { continue };
let Some(leaves) = leaf_source(render, tree, pooled, pools.as_deref()) else { continue };
let Some(source) = meshes.get(&leaves.mesh) else { continue };
let shed = leaves.deciduous.then_some(season.leaf_fall);
let burnt = burnt_front(fire.as_deref(), at.translation());
let mut cards: Vec<&Card> =
caches.cards(leaves.mesh.id(), source).cards.iter().filter(|card| card.on_tree(shed, burnt)).collect();
if cards.is_empty() {
continue;
}
let mut rng = SeededRng::from_parts(drop.tree.to_bits() as i32 ^ fall.now.to_bits() as i32, k as u32);
let (chosen, last) = (budget.min(cards.len()), cards.len() - 1);
for i in 0..chosen {
let j = i + (rng.next_f32() * (cards.len() - i) as f32) as usize;
cards.swap(i, j.min(last));
}
let stood = felled.and_then(|felled| stumps.get(felled.stump).ok()).unwrap_or(at).translation().y;
let height = |p: Vec2| ground.as_deref().map_or(stood, |ground| ground.height(p));
let mut burst = BurstBuilder::new(at.translation());
burst.add(source, cards[..chosen].iter().copied(), at, height, fall, &mut rng, |_, rng| rng.range(0.0, LET_GO));
let Some(burst) = burst.finish() else { continue };
let Some(material) = caches.material(&leaves.material, &mut flora) else { continue };
live += burst.leaves;
spawn_burst(&mut commands, &mut meshes, burst, at.translation(), material, Some(drop.tree), &time);
}
}
#[derive(Resource, Default)]
pub(crate) struct SeasonShed {
shed: Option<(f32, f32)>,
last: f32,
}
pub(crate) fn season_sheds(season: Res<FloraSeason>, shed: Res<SeasonShed>) -> bool {
season.is_changed() || shed.shed.is_some_and(|(from, _)| from < shed.last)
}
#[allow(clippy::too_many_arguments, clippy::type_complexity)]
pub(crate) fn shed_with_season(
mut commands: Commands,
settings: Res<FallingLeaves>,
season: Res<FloraSeason>,
time: Res<Time>,
wind: Res<FloraWind>,
ground: Option<Res<FloraGround>>,
mut meshes: ResMut<Assets<Mesh>>,
mut flora: ResMut<Assets<FloraMaterial>>,
pools: Option<Res<Assets<TreePool>>>,
mut caches: ResMut<FallCaches>,
mut state: ResMut<SeasonShed>,
cameras: Query<&GlobalTransform, With<Camera3d>>,
trees: Query<(&GlobalTransform, Option<&TreeRender>, Option<&ProceduralTree>, Option<&PooledTree>), Without<Felled>>,
bursts: Query<&LeafBurst>,
fire: Option<Res<FloraFire>>,
) {
let to = season.leaf_fall.clamp(0.0, 1.0);
let now = time.elapsed_secs_wrapped();
let last = std::mem::replace(&mut state.last, to);
let Some((from, since)) = state.shed else {
state.shed = Some((to, now));
return;
};
if to < from || to - last > SEASON_JUMP {
state.shed = Some((to, now));
return;
}
let mut elapsed = now - since;
if elapsed < -0.5 * WRAP {
elapsed += WRAP;
}
if to == from {
state.shed = Some((to, now));
return;
}
if elapsed < SHED_EVERY {
return;
}
state.shed = Some((to, now));
let eyes: Vec<Vec3> = cameras.iter().map(GlobalTransform::translation).collect();
let reach = settings.autumn_distance * settings.autumn_distance;
if eyes.is_empty() || reach <= 0.0 {
return;
}
let mut live: u32 = bursts.iter().map(|burst| burst.leaves).sum();
let fall = Fall::now(&time, &settings, &wind);
let mut rng = SeededRng::from_parts(now.to_bits() as i32, 1);
let span = to - from;
let mut looks: HashMap<AssetId<FloraMaterial>, (Handle<FloraMaterial>, BurstBuilder)> = HashMap::default();
for (at, render, tree, pooled) in &trees {
let budget = settings.max_leaves.saturating_sub(live) as usize;
if budget == 0 {
break;
}
let position = at.translation();
if !eyes.iter().any(|eye| eye.distance_squared(position) <= reach) {
continue;
}
let Some(leaves) = leaf_source(render, tree, pooled, pools.as_deref()).filter(|leaves| leaves.deciduous) else {
continue;
};
let Some(source) = meshes.get(&leaves.mesh) else { continue };
let burnt = burnt_front(fire.as_deref(), position);
let falling: Vec<&Card> = caches
.cards(leaves.mesh.id(), source)
.shed_between(from, to)
.filter(|card| card.moment >= burnt)
.take(budget)
.collect();
if falling.is_empty() {
continue;
}
let height = |p: Vec2| ground.as_deref().map_or(position.y, |ground| ground.height(p));
let (_, burst) = looks
.entry(leaves.material.id())
.or_insert_with(|| (leaves.material.clone(), BurstBuilder::new(position)));
live += burst.add(source, falling, at, height, fall, &mut rng, |card, _| {
((card.moment - from) / span - 1.0) * elapsed
});
}
for (_, (leaf_material, burst)) in looks {
let origin = burst.origin;
let (Some(burst), Some(material)) = (burst.finish(), caches.material(&leaf_material, &mut flora)) else { continue };
spawn_burst(&mut commands, &mut meshes, burst, origin, material, None, &time);
}
}
pub(crate) fn expire_bursts(mut commands: Commands, time: Res<Time>, bursts: Query<(Entity, &LeafBurst)>) {
let now = time.elapsed_secs();
for (entity, burst) in &bursts {
if now >= burst.gone_at {
commands.entity(entity).despawn();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::plugin::TreeAge;
use bevy::pbr::StandardMaterial;
use crate::pipeline::build_tree;
use crate::presets::TreePreset;
fn leaf_mesh(preset: TreePreset) -> Mesh {
build_tree(&preset.params(), f32::INFINITY).foliage.into_bevy_mesh()
}
fn fallen(t: f32, speed: f32) -> f32 {
speed * (t - PICK_UP * (1.0 - (-t / PICK_UP).exp()))
}
#[test]
fn a_leaf_lands_when_it_has_fallen_its_height() {
for (height, speed) in [(0.3, 0.9), (2.0, 1.2), (8.0, 1.5), (25.0, 1.0)] {
let t = landing_time(height, speed);
assert!((fallen(t, speed) - height).abs() < 1.0e-3, "{height} m at {speed} m/s: {t} s");
}
assert_eq!(landing_time(-1.0, 1.0), 0.0);
}
#[test]
fn every_leaf_is_one_card_and_a_card_holds_only_its_own_triangles() {
for preset in [TreePreset::Oak, TreePreset::Pine, TreePreset::Maple, TreePreset::Willow] {
let params = preset.params();
let built = build_tree(¶ms, f32::INFINITY);
let points = crate::foliage::collect_leaf_points(
&built.skeleton,
¶ms.foliage,
¶ms.growth,
¶ms.trunk,
params.seed,
);
let mesh = built.foliage.into_bevy_mesh();
let cards = leaf_cards(&mesh);
assert_eq!(cards.len(), points.len(), "{preset:?}");
let indices: Vec<usize> = mesh.indices().unwrap().iter().collect();
for card in &cards {
assert!(indices[card.indices.clone()].iter().all(|i| card.vertices.contains(i)), "{preset:?}");
}
}
}
#[test]
fn a_tree_drops_neither_flowers_fruit_nor_the_leaves_it_has_shed() {
let built = build_tree(&TreePreset::Cherry.params(), f32::INFINITY);
let leaves = on_tree(&built.foliage.clone().into_bevy_mesh(), None);
let mut merged = built.foliage;
merged.extend(&built.flowers);
assert_eq!(on_tree(&merged.into_bevy_mesh(), None), leaves);
let mut apple = TreePreset::Apple.params();
let fruiting = build_tree(&apple, f32::INFINITY).foliage.into_bevy_mesh();
apple.foliage.fruit = None;
let bare = build_tree(&apple, f32::INFINITY).foliage.into_bevy_mesh();
assert_eq!(on_tree(&fruiting, None), on_tree(&bare, None));
assert!(leaf_cards(&fruiting).len() > leaf_cards(&bare).len(), "the fruit went into its leaves' cards");
let oak = leaf_mesh(TreePreset::Oak);
let all = on_tree(&oak, None);
let half = on_tree(&oak, Some(0.5));
assert!((all as f32 * 0.4..all as f32 * 0.6).contains(&(half as f32)), "{half} of {all} left at half the leaf fall");
assert_eq!(on_tree(&oak, Some(1.0)), 0);
}
fn on_tree(mesh: &Mesh, shed: Option<f32>) -> usize {
leaf_cards(mesh).iter().filter(|card| card.on_tree(shed, f32::NEG_INFINITY)).count()
}
#[test]
fn a_burst_holds_the_chosen_cards_falling_to_the_ground_from_where_they_hang() {
let oak = leaf_mesh(TreePreset::Oak);
let cards = leaf_cards(&oak);
let chosen: Vec<&Card> = cards.iter().step_by(97).collect();
let tree = GlobalTransform::from(Transform::from_xyz(4.0, 1.0, -3.0).with_scale(Vec3::splat(1.5)));
let fall = Fall { now: 10.0, rest: 6.0, drift: Vec2::new(0.5, 0.0) };
let mut rng = SeededRng::new(3);
let mut burst = BurstBuilder::new(tree.translation());
burst.add(&oak, chosen.iter().copied(), &tree, |_| 1.0, fall, &mut rng, |_, rng| rng.range(0.0, LET_GO));
let burst = burst.finish().expect("leaves above the ground");
assert_eq!(burst.leaves as usize, chosen.len());
let vertices: usize = chosen.iter().map(|card| card.vertices.len()).sum();
assert_eq!(burst.mesh.count_vertices(), vertices);
let Some(VertexAttributeValues::Float32x4(anchor)) = burst.mesh.attribute(ATTRIBUTE_FALL_ANCHOR) else {
panic!("no anchors")
};
let Some(VertexAttributeValues::Float32x4(path)) = burst.mesh.attribute(ATTRIBUTE_FALL_PATH) else {
panic!("no paths")
};
let Some(VertexAttributeValues::Float32x3(positions)) = burst.mesh.attribute(Mesh::ATTRIBUTE_POSITION) else {
panic!("no positions")
};
let first = Vec3::from_array(positions[0]);
let source = match oak.attribute(Mesh::ATTRIBUTE_POSITION) {
Some(VertexAttributeValues::Float32x3(p)) => Vec3::from_array(p[chosen[0].vertices.start]),
_ => unreachable!(),
};
assert!((first - (tree.transform_point(source) - tree.translation())).length() < 1.0e-4);
for (a, p) in anchor.iter().zip(path) {
assert!((10.0..10.0 + LET_GO).contains(&a[3]), "let go at {}", a[3]);
assert_eq!(p[0], 0.0);
assert!(SPEED.contains(&p[3]));
let hangs = Vec3::new(a[0], a[1], a[2]);
assert!(burst.aabb.min().cmple(hangs.into()).all() && burst.aabb.max().cmpge(hangs.into()).all());
}
assert!(burst.aabb.min().y <= 0.0, "the bounds reach the ground");
let tallest = anchor.iter().map(|a| a[1]).fold(0.0, f32::max);
assert!(burst.life > landing_time(tallest, SPEED.end) + 6.0 * 0.75);
assert!(burst.life < LET_GO + landing_time(tallest, SPEED.start) + 6.0 * 1.25 + SHRINK + 1.0e-3);
}
fn app() -> bevy::app::App {
use bevy::asset::{AssetApp, AssetPlugin};
use bevy::time::TimeUpdateStrategy;
let mut app = bevy::app::App::new();
app.add_plugins((bevy::MinimalPlugins, AssetPlugin::default(), bevy::transform::TransformPlugin));
app.init_asset::<Mesh>();
app.init_asset::<StandardMaterial>();
app.add_plugins(crate::FloraPlugin);
app.insert_resource(crate::FloraMaterialSupport);
app.insert_resource(TimeUpdateStrategy::ManualDuration(std::time::Duration::from_millis(100)));
app
}
fn bursts(app: &mut bevy::app::App) -> Vec<LeafBurst> {
app.world_mut().query::<&LeafBurst>().iter(app.world()).copied().collect()
}
#[test]
fn a_shake_or_a_drop_makes_a_burst_from_the_trees_it_reaches_until_its_leaves_are_gone() {
let mut app = app();
let oak = TreePreset::Oak.params();
let near = app.world_mut().spawn((ProceduralTree(oak.clone()), Transform::default())).id();
let far = app.world_mut().spawn((ProceduralTree(oak), TreeAge(10.0), Transform::from_xyz(30.0, 0.0, 0.0))).id();
app.update();
app.update();
assert!(bursts(&mut app).is_empty());
let time = *app.world().resource::<Time>();
app.world_mut().resource_mut::<TreeShakes>().shake(TreeShake { at: Vec2::ZERO, ..Default::default() }, &time);
app.update();
app.update();
let shaken = bursts(&mut app);
assert_eq!(shaken.len(), 1);
assert_eq!((shaken[0].tree, shaken[0].leaves), (Some(near), 12));
app.world_mut().resource_mut::<FallingLeaves>().max_leaves = 20;
app.world_mut().resource_mut::<Messages<DropLeaves>>().write(DropLeaves { tree: far, count: 50 });
app.update();
let dropped: Vec<LeafBurst> = bursts(&mut app).into_iter().filter(|burst| burst.tree == Some(far)).collect();
assert_eq!(dropped.len(), 1);
assert_eq!(dropped[0].leaves, 8);
app.world_mut().resource_mut::<FallingLeaves>().max_leaves = 5000;
app.world_mut().get_mut::<TreeAge>(far).unwrap().0 = 5.0;
app.world_mut().resource_mut::<Messages<DropLeaves>>().write(DropLeaves { tree: far, count: 100_000 });
app.update();
let young = {
let world = app.world();
let render = world.get::<TreeRender>(far).unwrap();
let mesh = world.resource::<Assets<Mesh>>().get(&render.foliage.as_ref().unwrap().1).unwrap();
on_tree(mesh, None)
};
let regrown: Vec<LeafBurst> = bursts(&mut app).into_iter().filter(|burst| burst.tree == Some(far)).collect();
assert_eq!(regrown.len(), 2);
assert_eq!(regrown.iter().map(|burst| burst.leaves).max(), Some(young as u32));
let gone_at = bursts(&mut app).iter().map(|burst| burst.gone_at).fold(0.0, f32::max);
let now = app.world().resource::<Time>().elapsed_secs();
assert!(gone_at - now > 6.0 * 0.75, "gone {:.1} s after the drop", gone_at - now);
while app.world().resource::<Time>().elapsed_secs() < gone_at + 0.2 {
app.update();
}
assert!(bursts(&mut app).is_empty());
}
#[test]
fn a_burnt_tree_drops_none_of_the_leaves_the_fire_took() {
use crate::fire::{FloraFire, burnt_front};
let mut app = app();
let oak = app.world_mut().spawn((ProceduralTree(TreePreset::Oak.params()), Transform::default())).id();
let mut fire = FloraFire::with_cells(Vec2::ZERO, 32.0, 16);
fire.scorch(Vec3::ZERO, 3.0, 0.8);
app.insert_resource(fire);
app.update();
app.update();
let (all, left) = {
let world = app.world();
let render = world.get::<TreeRender>(oak).unwrap();
let mesh = world.resource::<Assets<Mesh>>().get(&render.foliage.as_ref().unwrap().1).unwrap();
let burnt = burnt_front(Some(world.resource::<FloraFire>()), Vec3::ZERO);
let cards = leaf_cards(mesh);
(cards.iter().filter(|card| !card.flower).count(), cards.iter().filter(|card| card.on_tree(None, burnt)).count())
};
assert!(left > 0 && left < all / 2, "{left} of {all} leaves left at char 0.8");
let drop_all = |app: &mut bevy::app::App| {
app.world_mut().resource_mut::<Messages<DropLeaves>>().write(DropLeaves { tree: oak, count: 100_000 });
app.update();
bursts(app).iter().map(|burst| burst.leaves).sum::<u32>()
};
assert_eq!(drop_all(&mut app), left as u32, "the leaves still on the tree, and only those");
app.world_mut().resource_mut::<FloraFire>().scorch(Vec3::ZERO, 3.0, 1.0);
assert_eq!(drop_all(&mut app), left as u32);
}
#[test]
fn a_pooled_tree_drops_leaves_from_its_variant() {
use crate::lod::LodSettings;
use crate::pool::build_pool;
let mut app = app();
let pool = {
let world = app.world_mut();
let built = world.resource_scope(|world, mut meshes: Mut<Assets<Mesh>>| {
let _ = world;
build_pool(&TreePreset::Pine.params(), 2, f32::INFINITY, &LodSettings::default(), &mut meshes)
});
world.resource_mut::<Assets<TreePool>>().add(built)
};
let tree = app.world_mut().spawn((PooledTree { pool, variant: 1 }, Transform::from_xyz(5.0, 0.0, 0.0))).id();
for _ in 0..5 {
app.update();
}
app.world_mut().resource_mut::<Messages<DropLeaves>>().write(DropLeaves { tree, count: 15 });
app.update();
let dropped = bursts(&mut app);
assert_eq!(dropped.len(), 1);
assert_eq!((dropped[0].tree, dropped[0].leaves), (Some(tree), 15));
}
#[test]
fn the_leaves_the_season_sheds_near_a_camera_fall_and_none_when_it_is_set_at_once() {
let mut app = app();
let oak = TreePreset::Oak.params();
let near = app.world_mut().spawn((ProceduralTree(oak.clone()), Transform::default())).id();
app.world_mut().spawn((ProceduralTree(oak), Transform::from_xyz(200.0, 0.0, 0.0)));
app.world_mut().spawn((Camera3d::default(), Transform::from_xyz(0.0, 2.0, 12.0)));
*app.world_mut().resource_mut::<FloraSeason>() = FloraSeason { autumn: 1.0, leaf_fall: 0.3, ..FloraSeason::SUMMER };
app.update();
app.update();
let fallen = |app: &mut bevy::app::App| bursts(app).iter().map(|burst| burst.leaves).sum::<u32>();
assert_eq!(fallen(&mut app), 0);
for step in 1..=10 {
app.world_mut().resource_mut::<FloraSeason>().leaf_fall = 0.3 + 0.01 * step as f32;
app.update();
}
app.update();
let expected = {
let world = app.world();
let render = world.get::<TreeRender>(near).unwrap();
let mesh = world.resource::<Assets<Mesh>>().get(&render.foliage.as_ref().unwrap().1).unwrap();
let to = app.world().resource::<FloraSeason>().leaf_fall;
leaf_cards(mesh).iter().filter(|card| !card.flower && (0.3..to).contains(&card.moment)).count() as u32
};
assert!(expected > 20, "only {expected} leaves in a tenth of the season");
assert_eq!(fallen(&mut app), expected);
assert!(bursts(&mut app).iter().all(|burst| burst.tree.is_none()));
for leaf_fall in [0.9, 0.2] {
app.world_mut().resource_mut::<FloraSeason>().leaf_fall = leaf_fall;
app.update();
app.update();
assert_eq!(fallen(&mut app), expected, "at {leaf_fall}");
}
}
}