use std::collections::VecDeque;
use bevy::asset::Assets;
use bevy::camera::Camera3d;
use bevy::ecs::prelude::*;
use bevy::math::{Vec2, Vec3};
use bevy::platform::collections::HashMap;
use bevy::reflect::Reflect;
use bevy::render::storage::ShaderBuffer;
use bevy::time::Time;
use bevy::transform::components::GlobalTransform;
use crate::gpu::GrowthMaterial;
use crate::see_through::{SEE_THROUGH_SLOT, SEE_THROUGH_VECTORS};
use crate::shading::{FloraMaterial, FloraWindBuffer};
use crate::units::FloraUnits;
#[derive(Component, Clone, Copy, Debug, PartialEq, Reflect)]
#[reflect(Component)]
pub struct FoliagePusher {
pub radius: f32,
pub height: f32,
pub strength: f32,
}
impl Default for FoliagePusher {
fn default() -> Self {
Self { radius: 0.45, height: 1.8, strength: 1.0 }
}
}
pub const MAX_PUSHERS: usize = 4;
const TRAIL: usize = 4;
const TRAIL_STEP: f32 = 0.15;
const TRAIL_WEIGHTS: [f32; TRAIL] = [1.0, 0.6, 0.3, 0.1];
pub(crate) const PUSH_SLOT: usize = SEE_THROUGH_SLOT + SEE_THROUGH_VECTORS;
pub(crate) const PUSH_VECTORS: usize = 1 + 2 * MAX_PUSHERS * TRAIL;
#[derive(Resource, Clone, Debug, PartialEq)]
pub(crate) struct PushSlots(pub [[f32; 4]; PUSH_VECTORS]);
impl Default for PushSlots {
fn default() -> Self {
Self([[0.0; 4]; PUSH_VECTORS])
}
}
#[derive(Default)]
pub(crate) struct Trails(HashMap<Entity, VecDeque<(f32, Vec3)>>);
impl Trails {
fn at(trail: &VecDeque<(f32, Vec3)>, now: f32, age: f32) -> Vec3 {
let wanted = now - age;
trail.iter().find(|(time, _)| *time <= wanted).or_else(|| trail.back()).map_or(Vec3::ZERO, |(_, at)| *at)
}
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn sync_pushers(
pushers: Query<(Entity, &FoliagePusher, &GlobalTransform)>,
units: Res<FloraUnits>,
cameras: Query<&GlobalTransform, With<Camera3d>>,
time: Res<Time>,
mut trails: Local<Trails>,
mut slots: ResMut<PushSlots>,
buffer: Option<Res<FloraWindBuffer>>,
buffers: Option<ResMut<Assets<ShaderBuffer>>>,
) {
let now = time.elapsed_secs();
let keep = TRAIL_STEP * TRAIL as f32 + 0.1;
trails.0.retain(|entity, _| pushers.contains(*entity));
for (entity, _, at) in &pushers {
let trail = trails.0.entry(entity).or_default();
trail.push_front((now, at.translation()));
while trail.len() > 1 && trail.back().is_some_and(|(time, _)| now - time > keep) {
trail.pop_back();
}
}
let eye = cameras.iter().next().map_or(Vec3::ZERO, GlobalTransform::translation);
let mut nearest: Vec<(Entity, &FoliagePusher, f32)> =
pushers.iter().map(|(entity, pusher, at)| (entity, pusher, at.translation().distance_squared(eye))).collect();
nearest.sort_by(|a, b| a.2.total_cmp(&b.2));
let mut packed = PushSlots::default();
let mut count = 0;
let metre = units.units_per_metre;
for (entity, pusher, _) in nearest.into_iter().take(MAX_PUSHERS) {
let Some(trail) = trails.0.get(&entity) else { continue };
for (k, weight) in TRAIL_WEIGHTS.iter().enumerate() {
let at = Trails::at(trail, now, k as f32 * TRAIL_STEP);
packed.0[1 + 2 * count] = [at.x, at.y, at.z, pusher.radius.max(0.0) * metre];
packed.0[2 + 2 * count] =
[0.5 * pusher.height.max(0.0) * metre, pusher.strength.clamp(0.0, 1.0) * weight, 0.0, 0.0];
count += 1;
}
}
let samples: Vec<[f32; 4]> = (0..count).map(|i| packed.0[1 + 2 * i]).collect();
let centre = samples.iter().fold(Vec2::ZERO, |sum, s| sum + Vec2::new(s[0], s[2])) / count.max(1) as f32;
let reach = samples.iter().map(|s| centre.distance(Vec2::new(s[0], s[2])) + 1.5 * s[3]).fold(0.0, f32::max);
packed.0[0] = [count as f32, centre.x, centre.y, reach];
if *slots == packed {
return;
}
*slots = packed;
let (Some(buffer), Some(mut buffers)) = (buffer, buffers) else { return };
let Some(mut shader_buffer) = buffers.get_mut(&buffer.0) else { return };
if let Some(slice) = shader_buffer.cast_slice_mut::<[f32; 4]>()
&& slice.len() >= PUSH_SLOT + PUSH_VECTORS
{
slice[PUSH_SLOT..PUSH_SLOT + PUSH_VECTORS].copy_from_slice(&slots.0);
}
}
pub(crate) fn arm_push(
pushers: Query<(), With<FoliagePusher>>,
flora: Option<ResMut<Assets<FloraMaterial>>>,
growth: Option<ResMut<Assets<GrowthMaterial>>>,
) {
let on = !pushers.is_empty();
if let Some(mut assets) = flora {
crate::see_through::arm(&mut assets, on, |m| m.extension.push, |m, on| m.extension.push = on);
}
if let Some(mut assets) = growth {
crate::see_through::arm(&mut assets, on, |m| m.extension.push, |m, on| m.extension.push = on);
}
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use bevy::app::App;
use bevy::asset::{AssetApp, AssetPlugin};
use bevy::mesh::Mesh;
use bevy::pbr::StandardMaterial;
use bevy::time::TimeUpdateStrategy;
use super::*;
use crate::shading::FloraMaterialSupport;
use crate::{FloraPlugin, ProceduralTree, TreePreset};
fn app() -> App {
let mut app = App::new();
app.add_plugins((bevy::MinimalPlugins, AssetPlugin::default()));
app.init_asset::<Mesh>();
app.init_asset::<StandardMaterial>();
app.add_plugins(FloraPlugin);
app.insert_resource(FloraMaterialSupport);
app.insert_resource(TimeUpdateStrategy::ManualDuration(Duration::from_millis(50)));
app
}
fn armed(app: &App) -> Vec<bool> {
app.world().resource::<Assets<FloraMaterial>>().iter().map(|(_, m)| m.extension.push).collect()
}
fn slots(app: &App) -> Vec<[f32; 4]> {
let buffer = app.world().resource::<FloraWindBuffer>();
let data = app.world().resource::<Assets<ShaderBuffer>>().get(&buffer.0).unwrap().cast_slice::<[f32; 4]>().unwrap();
data[PUSH_SLOT..PUSH_SLOT + PUSH_VECTORS].to_vec()
}
#[test]
fn a_pusher_bends_foliage_from_where_it_is_and_was_until_it_goes() {
let mut app = app();
app.world_mut().spawn(ProceduralTree(TreePreset::Shrub.params()));
app.update();
assert!(!armed(&app).is_empty() && armed(&app).iter().all(|on| !on), "off without a pusher");
assert_eq!(slots(&app)[0][0], 0.0);
let pusher = FoliagePusher { radius: 0.5, height: 2.0, strength: 0.8 };
let body = app.world_mut().spawn((pusher, GlobalTransform::from_translation(Vec3::new(1.0, 1.0, 0.0)))).id();
app.update();
assert!(armed(&app).iter().all(|on| *on));
let s = slots(&app);
assert_eq!(s[0], [4.0, 1.0, 0.0, 0.75]);
assert_eq!((s[1], s[2]), ([1.0, 1.0, 0.0, 0.5], [1.0, 0.8, 0.0, 0.0]));
for step in 1..=12 {
*app.world_mut().get_mut::<GlobalTransform>(body).unwrap() =
GlobalTransform::from_translation(Vec3::new(1.0 + 0.1 * step as f32, 1.0, 0.0));
app.update();
}
let s = slots(&app);
for (k, behind) in [0.0, 0.3, 0.6, 0.9].into_iter().enumerate() {
let x = s[1 + 2 * k][0];
assert!((x - (2.2 - behind)).abs() < 0.11, "sample {k} at x {x}, not {} m behind", behind);
}
assert!((1..4).all(|k| s[2 + 2 * k][1] < s[2 * k][1]), "older samples not weaker: {s:?}");
app.world_mut().spawn((bevy::camera::Camera3d::default(), GlobalTransform::default()));
for x in [3.0, 6.0, 4.0, 5.0, 7.0] {
app.world_mut().spawn((pusher, GlobalTransform::from_translation(Vec3::new(x, 1.0, 0.0))));
}
app.update();
let s = slots(&app);
assert_eq!(s[0][0], 16.0);
let nearest: Vec<f32> = (0..MAX_PUSHERS).map(|p| s[1 + 2 * TRAIL * p][0]).collect();
assert_eq!(nearest, vec![2.2, 3.0, 4.0, 5.0]);
let bodies: Vec<Entity> =
app.world_mut().query_filtered::<Entity, With<FoliagePusher>>().iter(app.world()).collect();
for entity in bodies {
app.world_mut().despawn(entity);
}
app.update();
assert!(armed(&app).iter().all(|on| !on), "on with no pusher left");
assert_eq!(slots(&app)[0][0], 0.0);
}
#[test]
fn the_default_pusher_covers_the_same_share_of_a_shrub_in_a_world_in_feet() {
const FEET: f32 = 3.2808;
let shrub = crate::build_tree(&TreePreset::Shrub.params(), f32::INFINITY);
let points: Vec<Vec3> = shrub.trunk.positions.iter().chain(&shrub.foliage.positions).copied().collect();
let share = |world: f32, units: f32| {
let mut app = app();
app.insert_resource(FloraUnits { units_per_metre: units });
let at = Vec3::new(0.4 * world, 0.9 * world, 0.0);
app.world_mut().spawn((FoliagePusher::default(), GlobalTransform::from_translation(at)));
app.update();
let s = slots(&app);
let (body, half_height) = (s[1], s[2][0]);
let covered = points
.iter()
.map(|p| *p * world)
.filter(|p| {
let across = (Vec2::new(p.x, p.z) - Vec2::new(body[0], body[2])).length();
across < 1.5 * body[3] && (p.y - body[1]).abs() - half_height < 0.5 * body[3]
})
.count();
covered as f32 / points.len() as f32
};
let (metres, feet) = (share(1.0, 1.0), share(FEET, FEET));
assert!(metres > 0.02 && metres < 0.98, "the body covers {metres} of the shrub: not a test of anything");
assert!((metres - feet).abs() < 1.0e-3, "{metres} of the shrub in metres, {feet} in feet");
let unaware = share(FEET, 1.0);
assert!(unaware < metres * 0.8, "an unaware body covers {unaware} of the shrub, {metres} in metres");
}
}