use bevy::camera::{Camera, Camera3d, Projection};
use bevy::ecs::prelude::*;
use bevy::light::NotShadowCaster;
use bevy::math::{Vec2, Vec3Swizzles};
use bevy::platform::collections::HashSet;
use bevy::reflect::Reflect;
use bevy::transform::components::GlobalTransform;
use crate::felling::Felled;
use crate::ground::FloraGround;
use crate::index::FloraIndex;
use crate::plugin::ProceduralTree;
use crate::pool::{PooledTree, ground_distance};
use crate::units::FloraUnits;
#[derive(Resource, Clone, Copy, Debug, PartialEq, Reflect)]
pub struct FloraShadows {
pub radius: f32,
}
#[derive(Component)]
pub(crate) struct ShadowCut;
#[derive(Default)]
pub(crate) struct Applied {
centre: Option<Vec2>,
radius: f32,
}
const STEP: f32 = 0.01;
#[allow(clippy::type_complexity, clippy::too_many_arguments)]
pub(crate) fn cut_far_shadows(
mut commands: Commands,
shadows: Option<Res<FloraShadows>>,
index: Option<Res<FloraIndex>>,
ground: Option<Res<FloraGround>>,
cameras: Query<(&Camera, &Projection, &GlobalTransform), With<Camera3d>>,
casting: Query<Entity, (Or<(With<PooledTree>, With<ProceduralTree>)>, Without<NotShadowCaster>, Without<Felled>)>,
cut: Query<Entity, With<ShadowCut>>,
felled: Query<Entity, (With<ShadowCut>, Added<Felled>)>,
units: Res<FloraUnits>,
mut applied: Local<Applied>,
) {
for tree in &felled {
commands.entity(tree).try_remove::<(NotShadowCaster, ShadowCut)>();
}
let (Some(shadows), Some(index)) = (shadows, index) else {
if applied.centre.take().is_some() {
for tree in &cut {
commands.entity(tree).try_remove::<(NotShadowCaster, ShadowCut)>();
}
}
return;
};
let main = cameras.iter().filter(|(camera, ..)| camera.is_active).min_by_key(|(camera, ..)| camera.order);
let Some((_, projection, at)) = main else { return };
let eye = at.translation();
let centre = match projection {
Projection::Orthographic(_) => ground_distance(at, ground.as_deref()).map_or(eye, |d| eye + at.forward().as_vec3() * d),
_ => eye,
}
.xz();
let radius = shadows.radius.max(0.0) * units.units_per_metre;
let moved = applied.centre.is_none_or(|was| was.distance(centre) > radius * STEP);
if !moved && radius == applied.radius && !index.is_changed() {
return;
}
if moved {
applied.centre = Some(centre);
}
applied.radius = radius;
let centre = applied.centre.unwrap_or(centre);
let side = (2.0 * radius / FloraIndex::CELL).ceil() + 1.0;
let near: HashSet<Entity> = if side * side > index.len() as f32 {
index.iter().filter(|tree| tree.distance_to(centre) <= radius).map(|tree| tree.entity).collect()
} else {
index.within(centre, radius).map(|tree| tree.entity).collect()
};
for &tree in &near {
if cut.contains(tree) {
commands.entity(tree).try_remove::<(NotShadowCaster, ShadowCut)>();
}
}
for tree in &casting {
if !near.contains(&tree) {
commands.entity(tree).try_insert((NotShadowCaster, ShadowCut));
}
}
}
#[cfg(test)]
mod tests {
use bevy::app::App;
use bevy::asset::{AssetApp, AssetPlugin, Assets, Handle};
use bevy::camera::OrthographicProjection;
use bevy::math::Vec3;
use bevy::mesh::{Mesh, Mesh3d};
use bevy::pbr::StandardMaterial;
use bevy::transform::components::Transform;
use super::*;
use crate::lod::{LodDistances, LodSettings};
use crate::plugin::FloraPlugin;
use crate::pool::{TreePool, build_pool};
use crate::presets::TreePreset;
fn app() -> (App, Handle<TreePool>) {
let mut app = App::new();
app.add_plugins((bevy::MinimalPlugins, AssetPlugin::default(), bevy::transform::TransformPlugin));
app.init_asset::<Mesh>();
app.init_asset::<StandardMaterial>();
app.add_plugins(FloraPlugin);
let lod = LodSettings { distances: LodDistances { impostor: f32::INFINITY, ..Default::default() }, ..Default::default() };
let pool = {
let mut meshes = app.world_mut().resource_mut::<Assets<Mesh>>();
build_pool(&TreePreset::Oak.params(), 1, f32::INFINITY, &lod, &mut meshes)
};
let pool = app.world_mut().resource_mut::<Assets<TreePool>>().add(pool);
(app, pool)
}
fn casting(app: &App, trees: &[Entity]) -> Vec<bool> {
trees
.iter()
.map(|&tree| {
let world = app.world();
let meshes: Vec<Entity> =
world.get::<Children>(tree).into_iter().flat_map(|c| c.iter()).filter(|c| world.get::<Mesh3d>(*c).is_some()).collect();
assert!(!meshes.is_empty(), "the tree has its meshes");
let quiet = meshes.iter().filter(|m| world.get::<NotShadowCaster>(**m).is_some()).count();
assert!(quiet == 0 || quiet == meshes.len(), "its meshes agree: {quiet} of {}", meshes.len());
world.get::<NotShadowCaster>(tree).is_none() && quiet == 0
})
.collect()
}
fn settle(app: &mut App) {
for _ in 0..3 {
app.update();
}
}
#[test]
fn only_trees_near_the_camera_cast_and_the_range_follows_it() {
let (mut app, pool) = app();
let trees: Vec<Entity> = (0..5)
.map(|i| app.world_mut().spawn((PooledTree { pool: pool.clone(), variant: 0 }, Transform::from_xyz(i as f32 * 20.0, 0.0, 0.0))).id())
.collect();
let own = app.world_mut().spawn((PooledTree { pool: pool.clone(), variant: 0 }, Transform::from_xyz(10.0, 0.0, 0.0), NotShadowCaster)).id();
let camera = app.world_mut().spawn((Camera3d::default(), Transform::from_xyz(0.0, 2.0, -5.0).looking_at(Vec3::new(0.0, 1.0, 0.0), Vec3::Y))).id();
settle(&mut app);
assert_eq!(casting(&app, &trees), [true; 5], "no range, every tree casts");
app.insert_resource(FloraShadows { radius: 30.0 });
settle(&mut app);
assert_eq!(casting(&app, &trees), [true, true, false, false, false], "within 30 m of the camera at x = 0");
app.world_mut().entity_mut(camera).insert(Transform::from_xyz(80.0, 2.0, -5.0).looking_at(Vec3::new(80.0, 1.0, 0.0), Vec3::Y));
settle(&mut app);
assert_eq!(casting(&app, &trees), [false, false, false, true, true], "within 30 m of x = 80");
assert_eq!(casting(&app, &[own]), [false], "the game's own quiet tree stays quiet");
let ortho = Projection::Orthographic(OrthographicProjection::default_3d());
app.world_mut()
.entity_mut(camera)
.insert((ortho, Transform::from_xyz(20.0, 50.0, -50.0).looking_at(Vec3::new(20.0, 0.0, 0.0), Vec3::Y)));
settle(&mut app);
assert_eq!(casting(&app, &trees), [true, true, true, false, false], "within 30 m of x = 20, under the orthographic camera");
app.world_mut().remove_resource::<FloraShadows>();
settle(&mut app);
assert_eq!(casting(&app, &trees), [true; 5]);
assert_eq!(casting(&app, &[own]), [false]);
}
#[test]
fn a_tree_planted_far_away_is_quieted_and_one_planted_near_casts() {
let (mut app, pool) = app();
app.world_mut().spawn((Camera3d::default(), Transform::from_xyz(0.0, 2.0, 0.0)));
app.insert_resource(FloraShadows { radius: 30.0 });
settle(&mut app);
let near = app.world_mut().spawn((PooledTree { pool: pool.clone(), variant: 0 }, Transform::from_xyz(5.0, 0.0, 5.0))).id();
let far = app.world_mut().spawn((PooledTree { pool, variant: 0 }, Transform::from_xyz(-200.0, 0.0, 40.0))).id();
settle(&mut app);
assert_eq!(casting(&app, &[near, far]), [true, false]);
}
}