use bevy::asset::{AssetEvent, Assets, Handle};
use bevy::ecs::prelude::*;
use bevy::ecs::system::EntityCommands;
use bevy::mesh::{Mesh, Mesh3d};
use bevy::pbr::{MeshMaterial3d, StandardMaterial};
use bevy::reflect::Reflect;
use bevy::render::render_resource::Face;
use bevy::camera::visibility::{Visibility, VisibilityRange};
use bevy::tasks::{ComputeTaskPool, TaskPool};
use bevy::time::Time;
use bevy::transform::components::Transform;
use crate::background::{BuildInBackground, PendingBuild};
use crate::falling::{
DropLeaves, FallCaches, FallingLeaves, LeafBurst, SeasonShed, drop_leaves, expire_bursts, forget_changed_meshes,
season_sheds, shake_leaves_loose, shed_landed_leaves, shed_with_season,
};
use crate::gpu::render::{GrowthPaint, detect_gpu_growth_support, rebuild_gpu_trees, sync_growth_mode};
use crate::gpu::{GpuGrowth, GpuGrowthSupport};
use crate::impostor::{ImpostorSupport, drive_impostor_bakes, start_impostor_bakes};
use crate::params::TreeParams;
use crate::pipeline::{aged_skeleton, build_aged};
use crate::see_through::{SeeThroughTrees, arm_see_through, sync_see_through};
use crate::fire::{FireRebind, arm_fire, burn, fire_burns, fire_changed, fire_rebinds, rebind_fire, sync_fire};
use crate::shape::TreeShape;
use crate::lod::{LodDistances, LodSettings, TreeLod, coarse_levels};
use crate::pool::{band, fill_pooled_shapes, spawn_pooled_trees};
use crate::shading::{
FloraMaterial, FloraMaterialSupport, FloraSeason, FloraWind, FloraWindBuffer, SharedFlora, TextureLibrary,
TreeShakes,
detect_flora_support,
expire_shakes, flora_decided, sync_wind,
};
use crate::skeleton::{Skeleton, generate_skeleton};
use crate::felling::{Felled, TreeLanded, buck_trees, drive_felling, start_felling};
use crate::tree_asset::sync_tree_assets;
#[derive(Component, Clone, Debug, PartialEq, Reflect)]
#[reflect(Component)]
#[require(TreeAge, Transform, Visibility)]
pub struct ProceduralTree(pub TreeParams);
#[derive(Component, Clone, Copy, Debug, PartialEq, Reflect)]
#[reflect(Component)]
pub struct TreeAge(pub f32);
impl Default for TreeAge {
fn default() -> Self {
Self(f32::INFINITY)
}
}
#[derive(Component, Clone, Copy, Debug, PartialEq, Reflect)]
#[reflect(Component)]
pub struct Growing {
pub rate: f32,
}
impl Default for Growing {
fn default() -> Self {
Self { rate: 1.0 }
}
}
#[derive(Message, Clone, Copy, Debug)]
pub struct TreeGenerated {
pub entity: Entity,
pub height: f32,
pub radius: f32,
}
#[derive(Message, Clone, Copy, Debug, PartialEq, Eq)]
pub struct TreeMature {
pub entity: Entity,
}
#[derive(Component)]
pub struct TreeRender {
pub(crate) trunk: Entity,
pub(crate) trunk_mesh: Option<Handle<Mesh>>,
pub(crate) paint: Paint,
pub(crate) foliage: Option<(Entity, Handle<Mesh>)>,
pub(crate) flowers: Option<(Entity, Handle<Mesh>)>,
pub(crate) full: Skeleton,
pub mature_at: f32,
pub(crate) age: f32,
pub(crate) lod: Option<(LodSettings, [CoarseLevel; 2])>,
}
pub(crate) enum Paint {
Standard {
trunk: Handle<StandardMaterial>,
foliage: Handle<StandardMaterial>,
},
Flora {
trunk: Handle<FloraMaterial>,
foliage: Handle<FloraMaterial>,
},
Growth(GrowthPaint),
}
impl Paint {
pub(crate) fn cloned_cpu(&self) -> Option<Paint> {
match self {
Paint::Standard { trunk, foliage } => Some(Paint::Standard {
trunk: trunk.clone(),
foliage: foliage.clone(),
}),
Paint::Flora { trunk, foliage } => Some(Paint::Flora {
trunk: trunk.clone(),
foliage: foliage.clone(),
}),
Paint::Growth(_) => None,
}
}
}
impl TreeRender {
pub fn trunk_entity(&self) -> Entity {
self.trunk
}
pub fn foliage_entity(&self) -> Option<Entity> {
self.foliage.as_ref().map(|(e, _)| *e)
}
pub fn flower_entity(&self) -> Option<Entity> {
self.flowers.as_ref().map(|(e, _)| *e)
}
}
#[derive(Component)]
pub struct TreeTrunkMesh;
#[derive(Component)]
pub struct TreeFoliageMesh;
#[derive(Component)]
pub struct TreeFlowerMesh;
pub(crate) const FLOWERS_SPAWN: (TreeFlowerMesh, Visibility) = (TreeFlowerMesh, Visibility::Hidden);
pub struct FloraPlugin;
impl bevy::app::Plugin for FloraPlugin {
fn build(&self, app: &mut bevy::app::App) {
app.add_message::<TreeGenerated>()
.add_message::<TreeMature>()
.add_message::<DropLeaves>()
.init_resource::<FallingLeaves>()
.init_resource::<FallCaches>()
.init_resource::<SeasonShed>()
.init_resource::<FireRebind>()
.register_type::<ProceduralTree>()
.register_type::<TreeAge>()
.register_type::<Growing>()
.register_type::<BuildInBackground>()
.register_type::<SeeThroughTrees>()
.register_type::<TreeLod>()
.register_type::<FallingLeaves>()
.add_systems(
bevy::app::Update,
(
detect_gpu_growth_support,
detect_flora_support,
expire_shakes,
sync_wind.run_if(
|wind: Res<FloraWind>, season: Res<FloraSeason>, shakes: Res<TreeShakes>| {
wind.is_changed() || season.is_changed() || shakes.is_changed()
},
),
sync_tree_assets.run_if(resource_exists::<Assets<TreeParams>>),
grow_trees,
sync_growth_mode,
rebuild_trees.run_if(flora_decided),
rebuild_gpu_trees,
update_tree_shapes,
start_felling,
drive_felling,
buck_trees,
show_flowers,
)
.chain(),
)
.add_systems(
bevy::app::Update,
(
(start_impostor_bakes, drive_impostor_bakes)
.chain()
.run_if(resource_exists::<ImpostorSupport>),
spawn_pooled_trees.run_if(flora_decided),
fill_pooled_shapes,
)
.chain(),
)
.add_systems(
bevy::app::PostUpdate,
(arm_see_through, sync_see_through).after(bevy::transform::TransformSystems::Propagate),
)
.add_systems(
bevy::app::PostUpdate,
(
rebind_fire.run_if(fire_rebinds),
burn.run_if(fire_burns),
sync_fire.run_if(fire_changed),
arm_fire,
)
.chain()
.after(bevy::transform::TransformSystems::Propagate)
.before(bevy::asset::AssetEventSystems),
)
.add_systems(
bevy::app::PostUpdate,
(
(
forget_changed_meshes.run_if(on_message::<AssetEvent<Mesh>>),
shake_leaves_loose.run_if(resource_changed::<TreeShakes>),
shed_landed_leaves.run_if(on_message::<TreeLanded>),
drop_leaves.run_if(on_message::<DropLeaves>),
shed_with_season.run_if(season_sheds),
)
.chain(),
expire_bursts.run_if(any_with_component::<LeafBurst>),
)
.after(bevy::transform::TransformSystems::Propagate)
.after(bevy::asset::AssetEventSystems),
);
crate::shading::build(app);
crate::gpu::build(app);
crate::impostor::build(app);
crate::pool::build(app);
crate::tree_asset::build(app);
crate::felling::build(app);
}
fn finish(&self, app: &mut bevy::app::App) {
crate::shading::finish(app);
crate::gpu::finish(app);
crate::impostor::finish(app);
crate::pool::finish(app);
crate::tree_asset::finish(app);
}
}
const LEAF_TRANSLUCENCY: f32 = 0.5;
pub(crate) fn material(base_color: bevy::color::Color) -> StandardMaterial {
StandardMaterial {
base_color,
perceptual_roughness: 1.0,
cull_mode: Some(Face::Back),
..Default::default()
}
}
pub(crate) fn leaf_material(base_color: bevy::color::Color) -> StandardMaterial {
StandardMaterial {
diffuse_transmission: LEAF_TRANSLUCENCY,
..material(base_color)
}
}
pub fn grow_trees(
time: Res<Time>,
mut trees: Query<(Entity, &mut TreeAge, &Growing, &TreeRender), Without<Felled>>,
mut mature: MessageWriter<TreeMature>,
) {
let dt = time.delta_secs();
for (entity, mut age, growing, render) in &mut trees {
let next = (age.0 + growing.rate * dt).min(render.mature_at);
if next != age.0 {
if next >= render.mature_at && age.0 < render.mature_at {
mature.write(TreeMature { entity });
}
age.0 = next;
}
}
}
pub fn update_tree_shapes(
mut generated: MessageReader<TreeGenerated>,
mut trees: Query<(&ProceduralTree, &TreeAge, &TreeRender, &mut TreeShape)>,
) {
fn update(tree: &ProceduralTree, age: &TreeAge, render: &TreeRender, shape: &mut Mut<TreeShape>) {
let skeleton = aged_skeleton(&render.full, &tree.0, age.0);
let new = TreeShape::from_skeleton(&skeleton, &tree.0, shape.limb_radius);
shape.set_if_neq(new);
}
for message in generated.read() {
if let Ok((tree, age, render, mut shape)) = trees.get_mut(message.entity) {
update(tree, age, render, &mut shape);
}
}
for (tree, age, render, mut shape) in &mut trees {
if shape.is_added() {
update(tree, age, render, &mut shape);
}
}
}
#[allow(clippy::type_complexity, clippy::too_many_arguments, private_interfaces)]
pub fn rebuild_trees(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
mut flora: ResMut<Assets<FloraMaterial>>,
mut shared: ResMut<SharedFlora>,
mut textures: TextureLibrary,
flora_support: Option<Res<FloraMaterialSupport>>,
wind: Option<Res<FloraWindBuffer>>,
support: Res<GpuGrowthSupport>,
mut generated: MessageWriter<TreeGenerated>,
mut trees: Query<
(
Entity,
Ref<ProceduralTree>,
Ref<TreeAge>,
Option<&mut TreeRender>,
Has<GpuGrowth>,
Has<BuildInBackground>,
Option<&TreeLod>,
),
Without<Felled>,
>,
mut pending: Query<(Entity, &mut PendingBuild<RebuiltTree>)>,
) {
let wind = wind.as_deref().filter(|_| flora_support.is_some());
let mut finished = Vec::new();
for (entity, mut build) in &mut pending {
if let Some(out) = build.poll() {
commands.entity(entity).remove::<PendingBuild<RebuiltTree>>();
finished.push(out);
}
}
let mut jobs = Vec::new();
for (entity, tree, age, mut render, gpu, background, lod) in &mut trees {
if gpu && support.0 {
continue;
}
if render.as_deref().is_some_and(|render| render.paint.cloned_cpu().is_none()) {
continue;
}
let lod = lod.map(|lod| lod.0);
let levels_stale = render.as_deref().is_some_and(|render| render.lod.as_ref().map(|(s, _)| *s) != lod);
if background {
let building = pending.contains(entity);
let behind = render.as_deref().is_none_or(|render| render.age != age.0) || levels_stale;
if !(tree.is_changed() || (behind && !building)) {
continue;
}
let kept = render.as_deref().filter(|_| !tree.is_changed()).map(|render| render.full.clone());
let job = RebuildJob { entity, params: tree.0.clone(), age: age.0, kept, background: true, lod };
commands.entity(entity).insert(PendingBuild::spawn(move || build_job(job)));
continue;
}
if !(tree.is_changed() || age.is_changed() || render.is_none() || levels_stale) {
continue;
}
let kept = match render.as_mut() {
Some(render) if !tree.is_changed() => Some(std::mem::take(&mut render.full)),
_ => None,
};
jobs.push(RebuildJob { entity, params: tree.0.clone(), age: age.0, kept, background: false, lod });
}
let mut apply = |out: RebuiltTree| {
let (entity, full, trunk_progress) = (out.entity, out.full, out.trunk_progress);
let Ok((_, tree, _, render, gpu, _, _)) = trees.get_mut(entity) else { return };
if out.background && (tree.is_changed() || (gpu && support.0)) {
return;
}
let params = &tree.0;
let existing = render.as_deref().and_then(|render| render.paint.cloned_cpu());
match (render, existing) {
(None, _) => {
let paint = if let Some(wind) = wind {
Paint::Flora {
trunk: shared.resolve(&mut flora, &textures.wood(params), wind),
foliage: shared.resolve(&mut flora, &textures.leaves(params), wind),
}
} else {
Paint::Standard {
trunk: materials.add(material(params.trunk_color)),
foliage: materials.add(leaf_material(params.foliage.color)),
}
};
let mut trunk_child = commands.spawn((TreeTrunkMesh, Visibility::Hidden, ChildOf(entity)));
paint_child(&mut trunk_child, &paint, false);
let trunk = trunk_child.id();
let mut trunk_mesh = None;
sync_trunk(&mut commands, &mut meshes, trunk, &mut trunk_mesh, out.trunk);
let mut foliage = None;
sync_leaf_child(&mut commands, &mut meshes, &mut foliage, out.foliage, |commands, mesh| {
spawn_mesh_child(commands, mesh, TreeFoliageMesh, entity, &paint, true)
});
let mut flowers = None;
sync_leaf_child(&mut commands, &mut meshes, &mut flowers, out.flowers, |commands, mesh| {
spawn_mesh_child(commands, mesh, FLOWERS_SPAWN, entity, &paint, true)
});
let mut lod = None;
let level0 = [Some(trunk), foliage.as_ref().map(|c| c.0), flowers.as_ref().map(|c| c.0)];
sync_levels(&mut commands, &mut meshes, entity, &mut lod, out.lod, &paint, level0);
commands.entity(entity).insert(TreeRender {
trunk,
trunk_mesh,
paint,
foliage,
flowers,
full,
mature_at: out.mature_at,
age: out.age,
lod,
});
}
(Some(mut render), Some(paint)) => {
let render = &mut *render;
render.full = full;
render.mature_at = out.mature_at;
render.age = out.age;
let trunk_entity = render.trunk;
sync_trunk(
&mut commands,
&mut meshes,
trunk_entity,
&mut render.trunk_mesh,
out.trunk,
);
match &paint {
Paint::Standard { trunk, foliage } => {
set_color(&mut materials, trunk, params.trunk_color);
set_color(&mut materials, foliage, params.foliage.color);
}
Paint::Flora { .. } if out.regenerated => {
let wind = wind.expect("flora paint implies the wind buffer");
let wood = shared.resolve(&mut flora, &textures.wood(params), wind);
let leaves = shared.resolve(&mut flora, &textures.leaves(params), wind);
if let Paint::Flora { trunk, foliage } = &mut render.paint {
let coarse = render.lod.iter().flat_map(|(_, levels)| levels);
if *trunk != wood {
let woods = coarse.clone().flat_map(|level| level.wood.iter().map(|c| c.0));
for child in std::iter::once(render.trunk).chain(woods) {
commands.entity(child).insert(MeshMaterial3d(wood.clone()));
}
*trunk = wood;
}
if *foliage != leaves {
let coarse_leaves = coarse.flat_map(|level| level.leaves.iter().map(|c| c.0));
let level0 = render.foliage.iter().chain(&render.flowers).map(|c| c.0);
for child in level0.chain(coarse_leaves) {
commands.entity(child).insert(MeshMaterial3d(leaves.clone()));
}
*foliage = leaves;
}
}
}
Paint::Flora { .. } => {}
Paint::Growth(_) => unreachable!("trees grown on the GPU were skipped above"),
}
let paint = &render.paint;
sync_leaf_child(&mut commands, &mut meshes, &mut render.foliage, out.foliage, |commands, mesh| {
spawn_mesh_child(commands, mesh, TreeFoliageMesh, entity, paint, true)
});
sync_leaf_child(&mut commands, &mut meshes, &mut render.flowers, out.flowers, |commands, mesh| {
spawn_mesh_child(commands, mesh, FLOWERS_SPAWN, entity, paint, true)
});
let level0 = [Some(render.trunk), render.foliage.as_ref().map(|c| c.0), render.flowers.as_ref().map(|c| c.0)];
sync_levels(&mut commands, &mut meshes, entity, &mut render.lod, out.lod, &render.paint, level0);
}
(Some(_), None) => {}
}
generated.write(TreeGenerated {
entity,
height: params.trunk.height * trunk_progress,
radius: params.trunk.radius,
});
};
for out in finished {
apply(out);
}
let parallel = jobs.len() > 1000 || jobs.iter().filter(|job| job.kept.is_none()).count() >= 2;
build_in_batches(jobs, parallel, build_job, &mut apply);
}
pub(crate) fn build_in_batches<J: Send, R: Send + 'static>(
jobs: Vec<J>,
parallel: bool,
build: impl Fn(J) -> R + Sync,
mut apply: impl FnMut(R),
) {
if !parallel || jobs.len() < 2 {
jobs.into_iter().for_each(|job| apply(build(job)));
return;
}
let pool = ComputeTaskPool::get_or_init(TaskPool::default);
let batch = pool.thread_num() * 4;
let build = &build;
let mut jobs = jobs.into_iter();
loop {
let results = pool.scope(|scope| {
for job in jobs.by_ref().take(batch) {
scope.spawn(async move { build(job) });
}
});
if results.is_empty() {
return;
}
results.into_iter().for_each(&mut apply);
}
}
pub(crate) fn paint_child(child: &mut EntityCommands, paint: &Paint, leaf: bool) {
match (paint, leaf) {
(Paint::Standard { trunk, .. }, false) => child.insert(MeshMaterial3d(trunk.clone())),
(Paint::Standard { foliage, .. }, true) => child.insert(MeshMaterial3d(foliage.clone())),
(Paint::Flora { trunk, .. }, false) => child.insert(MeshMaterial3d(trunk.clone())),
(Paint::Flora { foliage, .. }, true) => child.insert(MeshMaterial3d(foliage.clone())),
(Paint::Growth(_), _) => unreachable!("trees grown on the GPU are spawned by rebuild_gpu_trees"),
};
}
fn set_color(materials: &mut Assets<StandardMaterial>, handle: &Handle<StandardMaterial>, color: bevy::color::Color) {
if let Some(mut material) = materials.get_mut(handle)
&& material.base_color != color
{
material.base_color = color;
}
}
fn sync_trunk(
commands: &mut Commands,
meshes: &mut Assets<Mesh>,
child: Entity,
handle: &mut Option<Handle<Mesh>>,
mesh: Option<Mesh>,
) {
let Some(mesh) = mesh else {
commands.entity(child).insert(Visibility::Hidden);
return;
};
commands.entity(child).insert(Visibility::Inherited);
match handle {
Some(handle) => replace_mesh(commands, meshes, child, handle, mesh),
None => {
let new = meshes.add(mesh);
commands.entity(child).insert(Mesh3d(new.clone()));
*handle = Some(new);
}
}
}
pub(crate) struct RebuiltTree {
entity: Entity,
age: f32,
regenerated: bool,
background: bool,
full: Skeleton,
trunk: Option<Mesh>,
foliage: Option<Mesh>,
flowers: Option<Mesh>,
mature_at: f32,
trunk_progress: f32,
lod: Option<(LodSettings, [CoarseMeshes; 2])>,
}
struct RebuildJob {
entity: Entity,
params: TreeParams,
age: f32,
kept: Option<Skeleton>,
background: bool,
lod: Option<LodSettings>,
}
fn build_job(job: RebuildJob) -> RebuiltTree {
let regenerated = job.kept.is_none();
let full = job.kept.unwrap_or_else(|| generate_skeleton(&job.params));
let out = build_aged(&full, &job.params, job.age);
let lod = job.lod.map(|settings| {
let levels = coarse_levels(&out, &job.params, &settings).map(|level| {
let mut leaves = level.foliage;
leaves.extend(&level.flowers);
CoarseMeshes {
wood: (!level.trunk.is_empty()).then(|| level.trunk.into_bevy_mesh()),
leaves: (!leaves.is_empty()).then(|| leaves.into_bevy_mesh()),
}
});
(settings, levels)
});
RebuiltTree {
entity: job.entity,
age: job.age,
regenerated,
background: job.background,
full,
trunk: (!out.trunk.is_empty()).then(|| out.trunk.into_bevy_mesh()),
foliage: (!out.foliage.is_empty()).then(|| out.foliage.into_bevy_mesh()),
flowers: (!out.flowers.is_empty()).then(|| out.flowers.into_bevy_mesh()),
mature_at: out.mature_at,
trunk_progress: out.skeleton.nodes[0].progress,
lod,
}
}
#[derive(Default)]
pub(crate) struct CoarseLevel {
wood: Option<(Entity, Handle<Mesh>)>,
leaves: Option<(Entity, Handle<Mesh>)>,
}
impl CoarseLevel {
pub(crate) fn children(&self) -> impl Iterator<Item = Entity> + '_ {
self.wood.iter().chain(&self.leaves).map(|(child, _)| *child)
}
}
pub(crate) struct CoarseMeshes {
wood: Option<Mesh>,
leaves: Option<Mesh>,
}
#[derive(Component, Clone, Copy, Debug, PartialEq, Eq)]
pub struct TreeLodMesh(pub u8);
fn level_band(level: usize, distances: LodDistances) -> VisibilityRange {
match level {
0 => band(0.0, distances.lod1),
1 => band(distances.lod1, distances.lod2),
_ => band(distances.lod2, f32::INFINITY),
}
}
fn sync_levels(
commands: &mut Commands,
meshes: &mut Assets<Mesh>,
tree: Entity,
have: &mut Option<(LodSettings, [CoarseLevel; 2])>,
want: Option<(LodSettings, [CoarseMeshes; 2])>,
paint: &Paint,
level0: [Option<Entity>; 3],
) {
let old = have.as_ref().map(|(settings, _)| settings.distances);
let Some((settings, built)) = want else {
if let Some((_, levels)) = have.take() {
for child in levels.iter().flat_map(CoarseLevel::children) {
commands.entity(child).despawn();
}
for child in level0.into_iter().flatten() {
commands.entity(child).remove::<VisibilityRange>();
}
}
return;
};
let distances = settings.distances;
let moved = old != Some(distances);
for child in level0.into_iter().flatten() {
if moved {
commands.entity(child).insert(level_band(0, distances));
} else {
commands.entity(child).insert_if_new(level_band(0, distances));
}
}
let (kept, levels) = have.get_or_insert_with(|| (settings, Default::default()));
*kept = settings;
for (index, (level, built)) in levels.iter_mut().zip(built).enumerate() {
let range = level_band(index + 1, distances);
if moved {
for child in level.children() {
commands.entity(child).insert(range.clone());
}
}
let marker = TreeLodMesh(index as u8 + 1);
sync_leaf_child(commands, meshes, &mut level.wood, built.wood, |commands, mesh| {
spawn_mesh_child(commands, mesh, (marker, range.clone()), tree, paint, false)
});
sync_leaf_child(commands, meshes, &mut level.leaves, built.leaves, |commands, mesh| {
spawn_mesh_child(commands, mesh, (marker, range.clone()), tree, paint, true)
});
}
}
pub(crate) fn replace_mesh(
commands: &mut Commands,
meshes: &mut Assets<Mesh>,
child: Entity,
handle: &mut Handle<Mesh>,
mesh: Mesh,
) {
if let Some(mut slot) = meshes.get_mut(&*handle) {
*slot = mesh;
return;
}
*handle = meshes.add(mesh);
commands.entity(child).insert(Mesh3d(handle.clone()));
}
pub(crate) fn sync_leaf_child<'w, 's>(
commands: &mut Commands<'w, 's>,
meshes: &mut Assets<Mesh>,
slot: &mut Option<(Entity, Handle<Mesh>)>,
mesh: Option<Mesh>,
spawn: impl FnOnce(&mut Commands<'w, 's>, Handle<Mesh>) -> Entity,
) {
match (slot.as_mut(), mesh) {
(Some((child, handle)), Some(mesh)) => replace_mesh(commands, meshes, *child, handle, mesh),
(None, Some(mesh)) => {
let handle = meshes.add(mesh);
*slot = Some((spawn(commands, handle.clone()), handle));
}
(Some((child, _)), None) => {
commands.entity(*child).despawn();
*slot = None;
}
(None, None) => {}
}
}
fn spawn_mesh_child(
commands: &mut Commands,
mesh: Handle<Mesh>,
bundle: impl Bundle,
tree: Entity,
paint: &Paint,
leaf: bool,
) -> Entity {
let mut child = commands.spawn((Mesh3d(mesh), bundle, ChildOf(tree)));
paint_child(&mut child, paint, leaf);
child.id()
}
#[allow(clippy::type_complexity)]
pub(crate) fn show_flowers(
season: Res<FloraSeason>,
mut flowers: ParamSet<(
Query<&mut Visibility, With<TreeFlowerMesh>>,
Query<&mut Visibility, Added<TreeFlowerMesh>>,
)>,
) {
let want = if season.blossom > 0.0 { Visibility::Inherited } else { Visibility::Hidden };
if season.is_changed() {
for mut visibility in &mut flowers.p0() {
visibility.set_if_neq(want);
}
} else {
for mut visibility in &mut flowers.p1() {
visibility.set_if_neq(want);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::presets::TreePreset;
use crate::shading::{GustFront, TreeShake, wind_vectors};
use bevy::math::Vec2;
use bevy::app::App;
use bevy::asset::{AssetApp, AssetPlugin};
use bevy::render::storage::ShaderBuffer;
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
}
#[test]
fn a_tree_shape_follows_its_tree_and_maturity_is_announced_once() {
use bevy::time::TimeUpdateStrategy;
use std::time::Duration;
let mut app = app();
app.insert_resource(TimeUpdateStrategy::ManualDuration(Duration::from_millis(100)));
let params = TreePreset::Oak.params();
let tree = app
.world_mut()
.spawn((ProceduralTree(params.clone()), TreeAge(2.0), TreeShape::with_limb_radius(0.04)))
.id();
let without = app.world_mut().spawn((ProceduralTree(params.clone()), TreeAge(2.0))).id();
app.update();
let young = app.world().get::<TreeShape>(tree).expect("kept").clone();
assert!(app.world().get::<TreeShape>(without).is_none(), "the shape is opt-in");
let expected = crate::pipeline::build_tree(¶ms, 2.0);
assert_eq!(young, TreeShape::from_skeleton(&expected.skeleton, ¶ms, 0.04));
app.world_mut().entity_mut(tree).insert(Growing { rate: 40.0 });
let mut cursor = app.world().resource::<Messages<TreeMature>>().get_cursor_current();
let mut announced = 0;
for _ in 0..30 {
app.update();
announced += cursor.read(app.world().resource::<Messages<TreeMature>>()).count();
}
assert_eq!(announced, 1);
let mature = app.world().get::<TreeShape>(tree).unwrap();
assert!(mature.height() > young.height() && mature.limbs.len() > young.limbs.len());
let full = crate::pipeline::build_tree(¶ms, f32::INFINITY);
assert_eq!(*mature, TreeShape::from_skeleton(&full.skeleton, ¶ms, 0.04));
}
#[test]
fn spawns_children_and_drops_foliage_at_age_zero() {
let mut app = app();
let tree = app
.world_mut()
.spawn((ProceduralTree(TreePreset::Oak.params()), TreeAge(f32::INFINITY)))
.id();
app.update();
{
let world = app.world();
let render = world.get::<TreeRender>(tree).expect("TreeRender inserted");
assert!(render.foliage.is_some());
assert_eq!(world.get::<Children>(tree).unwrap().len(), 2);
let meshes = world.resource::<Assets<Mesh>>();
let trunk = meshes.get(render.trunk_mesh.as_ref().unwrap()).unwrap();
assert!(trunk.count_vertices() > 0);
assert_eq!(world.get::<Visibility>(render.trunk_entity()), Some(&Visibility::Inherited));
assert!(render.mature_at.is_finite());
}
app.world_mut().get_mut::<TreeAge>(tree).unwrap().0 = 0.0;
app.update();
{
let world = app.world();
let render = world.get::<TreeRender>(tree).unwrap();
assert!(render.foliage.is_none());
assert_eq!(world.get::<Children>(tree).unwrap().len(), 1);
assert_eq!(world.get::<Visibility>(render.trunk_entity()), Some(&Visibility::Hidden));
}
app.world_mut().get_mut::<TreeAge>(tree).unwrap().0 = f32::INFINITY;
app.update();
assert_eq!(app.world().get::<Children>(tree).unwrap().len(), 2);
let world = app.world();
let render = world.get::<TreeRender>(tree).unwrap();
assert_eq!(world.get::<Visibility>(render.trunk_entity()), Some(&Visibility::Inherited));
}
#[test]
fn flowers_are_a_child_of_their_own_drawn_only_in_blossom() {
let mut app = app();
let cherry = app.world_mut().spawn((ProceduralTree(TreePreset::Cherry.params()), TreeAge(f32::INFINITY))).id();
let oak = app.world_mut().spawn((ProceduralTree(TreePreset::Oak.params()), TreeAge(f32::INFINITY))).id();
app.update();
assert_eq!(app.world().get::<TreeRender>(oak).unwrap().flower_entity(), None, "an oak has no flowers");
let flowers = app.world().get::<TreeRender>(cherry).unwrap().flower_entity().expect("a cherry flowers");
assert!(app.world().get::<TreeFlowerMesh>(flowers).is_some());
assert_eq!(app.world().get::<Children>(cherry).unwrap().len(), 3);
let visibility = |app: &App| *app.world().get::<Visibility>(flowers).unwrap();
assert_eq!(visibility(&app), Visibility::Hidden);
for (season, shown) in [
(FloraSeason::BLOSSOM, Visibility::Inherited),
(FloraSeason { blossom: 0.01, ..FloraSeason::BLOSSOM }, Visibility::Inherited),
(FloraSeason::SUMMER, Visibility::Hidden),
(FloraSeason::BLOSSOM, Visibility::Inherited),
] {
*app.world_mut().resource_mut::<FloraSeason>() = season;
app.update();
assert_eq!(visibility(&app), shown, "{season:?}");
}
app.world_mut().get_mut::<TreeAge>(cherry).unwrap().0 = 30.0;
app.update();
assert_eq!(app.world().get::<TreeRender>(cherry).unwrap().flower_entity(), Some(flowers));
assert_eq!(visibility(&app), Visibility::Inherited);
*app.world_mut().resource_mut::<FloraSeason>() = FloraSeason::SUMMER;
app.world_mut().get_mut::<TreeAge>(cherry).unwrap().0 = 0.0;
app.update();
assert_eq!(app.world().get::<TreeRender>(cherry).unwrap().flower_entity(), None);
app.world_mut().get_mut::<TreeAge>(cherry).unwrap().0 = f32::INFINITY;
app.update();
let back = app.world().get::<TreeRender>(cherry).unwrap().flower_entity().expect("flowers again");
assert_eq!(app.world().get::<Visibility>(back), Some(&Visibility::Hidden));
}
#[test]
fn trunk_born_empty_gets_its_mesh_once_it_grows() {
let mut app = app();
let tree = app
.world_mut()
.spawn((ProceduralTree(TreePreset::Oak.params()), TreeAge(0.0)))
.id();
app.update();
let trunk = {
let world = app.world();
let render = world.get::<TreeRender>(tree).unwrap();
assert!(render.trunk_mesh.is_none());
let trunk = render.trunk_entity();
assert_eq!(world.get::<Visibility>(trunk), Some(&Visibility::Hidden));
assert!(world.get::<Mesh3d>(trunk).is_none());
trunk
};
app.world_mut().get_mut::<TreeAge>(tree).unwrap().0 = 2.0;
app.update();
let world = app.world();
let render = world.get::<TreeRender>(tree).unwrap();
let handle = render.trunk_mesh.as_ref().expect("mesh created once the trunk exists");
assert!(world.resource::<Assets<Mesh>>().get(handle).unwrap().count_vertices() > 0);
assert_eq!(world.get::<Mesh3d>(trunk).map(|m| &m.0), Some(handle));
assert_eq!(world.get::<Visibility>(trunk), Some(&Visibility::Inherited));
}
#[test]
fn growing_stops_at_maturity() {
let mut app = app();
let tree = app
.world_mut()
.spawn((
ProceduralTree(TreePreset::Oak.params()),
TreeAge(0.0),
Growing { rate: 1.0e7 },
))
.id();
app.update(); app.update(); let age = app.world().get::<TreeAge>(tree).unwrap().0;
let mature_at = app.world().get::<TreeRender>(tree).unwrap().mature_at;
assert_eq!(age, mature_at);
app.update();
assert_eq!(app.world().get::<TreeAge>(tree).unwrap().0, mature_at);
}
fn uploaded_trunk(app: &App, tree: Entity) -> Vec<[f32; 3]> {
let world = app.world();
let render = world.get::<TreeRender>(tree).unwrap();
let mesh = world.resource::<Assets<Mesh>>().get(render.trunk_mesh.as_ref().unwrap()).unwrap();
mesh.attribute(Mesh::ATTRIBUTE_POSITION).unwrap().as_float3().unwrap().to_vec()
}
fn expected_trunk(params: &TreeParams, age: f32) -> Vec<[f32; 3]> {
crate::pipeline::build_tree(params, age).trunk.positions.iter().map(|p| p.to_array()).collect()
}
#[test]
fn growing_trees_reuse_their_skeleton_until_the_parameters_change() {
let mut app = app();
let mut params = TreePreset::Oak.params();
let tree = app.world_mut().spawn((ProceduralTree(params.clone()), TreeAge(2.0))).id();
app.update();
app.world_mut().get_mut::<TreeAge>(tree).unwrap().0 = 5.0;
app.update();
assert_eq!(uploaded_trunk(&app, tree), expected_trunk(¶ms, 5.0));
params.seed = 7;
params.trunk.height *= 1.2;
app.world_mut().get_mut::<ProceduralTree>(tree).unwrap().0 = params.clone();
app.update();
assert_eq!(uploaded_trunk(&app, tree), expected_trunk(¶ms, 5.0));
app.world_mut().get_mut::<TreeAge>(tree).unwrap().0 = 6.0;
app.update();
assert_eq!(uploaded_trunk(&app, tree), expected_trunk(¶ms, 6.0));
}
#[test]
fn a_tree_built_in_the_background_shows_only_its_latest_parameters_and_reaches_its_age() {
let mut app = app();
let mut params = TreePreset::Oak.params();
let tree = app.world_mut().spawn((ProceduralTree(params.clone()), TreeAge(3.0), BuildInBackground)).id();
app.update();
assert!(app.world().get::<TreeRender>(tree).is_none());
params.seed = 7;
app.world_mut().get_mut::<ProceduralTree>(tree).unwrap().0 = params.clone();
app.update();
app.world_mut().get_mut::<TreeAge>(tree).unwrap().0 = 6.0;
let mut cursor = app.world().resource::<Messages<TreeGenerated>>().get_cursor_current();
let mut generated = 0;
for _ in 0..3000 {
app.update();
generated += cursor.read(app.world().resource::<Messages<TreeGenerated>>()).count();
if let Some(age) = app.world().get::<TreeRender>(tree).map(|render| render.age) {
assert_eq!(uploaded_trunk(&app, tree), expected_trunk(¶ms, age), "age {age}");
if age == 6.0 {
break;
}
}
std::thread::sleep(std::time::Duration::from_millis(1));
}
assert_eq!(app.world().get::<TreeRender>(tree).map(|render| render.age), Some(6.0));
assert!((1..=2).contains(&generated), "{generated} builds applied");
let gone = app.world_mut().spawn((ProceduralTree(params.clone()), TreeAge(3.0), BuildInBackground)).id();
app.update();
app.world_mut().despawn(gone);
for _ in 0..20 {
app.update();
}
}
#[test]
fn many_trees_built_in_one_frame_match_the_serial_build() {
let mut app = app();
let mut specs = Vec::new();
for i in 0..40 {
let mut params = TreePreset::ALL[i % TreePreset::ALL.len()].params();
params.seed = i as i32;
let age = 1.0 + i as f32 * 0.4;
let tree = app.world_mut().spawn((ProceduralTree(params.clone()), TreeAge(age))).id();
specs.push((tree, params, age));
}
app.update();
let check = |app: &App, specs: &[(Entity, TreeParams, f32)]| {
for (tree, params, age) in specs {
let want = crate::pipeline::build_tree(params, *age);
let world = app.world();
let render = world.get::<TreeRender>(*tree).unwrap();
let meshes = world.resource::<Assets<Mesh>>();
let same = |handle: &Handle<Mesh>, data: crate::mesh::MeshData| {
let got = meshes.get(handle).unwrap();
let want = data.into_bevy_mesh();
assert_eq!(got.attributes().count(), want.attributes().count());
for (attribute, values) in want.attributes() {
assert_eq!(got.attribute(attribute.id), Some(values), "{}", attribute.name);
}
assert_eq!(got.indices(), want.indices());
};
match &render.trunk_mesh {
Some(handle) => same(handle, want.trunk),
None => assert!(want.trunk.is_empty()),
}
match &render.foliage {
Some((_, handle)) => same(handle, want.foliage),
None => assert!(want.foliage.is_empty()),
}
assert_eq!(render.mature_at, want.mature_at);
}
};
check(&app, &specs);
specs[3].1.trunk.height *= 1.3;
specs[3].1.seed = 99;
app.world_mut().get_mut::<ProceduralTree>(specs[3].0).unwrap().0 = specs[3].1.clone();
specs[5].2 += 2.0;
app.world_mut().get_mut::<TreeAge>(specs[5].0).unwrap().0 = specs[5].2;
app.update();
check(&app, &specs);
}
#[test]
fn growing_leaves_materials_alone_until_a_colour_changes() {
let modified = |app: &App| {
app.world()
.resource::<Messages<bevy::asset::AssetEvent<StandardMaterial>>>()
.iter_current_update_messages()
.filter(|e| matches!(e, bevy::asset::AssetEvent::Modified { .. }))
.count()
};
let mut app = app();
let tree = app.world_mut().spawn((ProceduralTree(TreePreset::Oak.params()), TreeAge(2.0))).id();
app.update();
for age in [3.0, 4.0] {
app.world_mut().get_mut::<TreeAge>(tree).unwrap().0 = age;
app.update();
assert_eq!(modified(&app), 0, "age {age}");
}
let red = bevy::color::Color::srgb(1.0, 0.0, 0.0);
app.world_mut().get_mut::<ProceduralTree>(tree).unwrap().0.trunk_color = red;
app.update();
assert_eq!(modified(&app), 1);
let world = app.world();
let render = world.get::<TreeRender>(tree).unwrap();
let Paint::Standard { trunk, .. } = &render.paint else { panic!("a plain tree has plain materials") };
assert_eq!(world.resource::<Assets<StandardMaterial>>().get(trunk).unwrap().base_color, red);
}
fn flora_pair(app: &App, tree: Entity) -> (Handle<FloraMaterial>, Handle<FloraMaterial>) {
let render = app.world().get::<TreeRender>(tree).unwrap();
let Paint::Flora { trunk, foliage } = &render.paint else { panic!("expected flora materials") };
(trunk.clone(), foliage.clone())
}
fn update_and_modified(app: &mut App) -> Vec<bevy::asset::AssetId<FloraMaterial>> {
let mut cursor = app
.world()
.resource::<Messages<bevy::asset::AssetEvent<FloraMaterial>>>()
.get_cursor_current();
app.update();
let messages = app.world().resource::<Messages<bevy::asset::AssetEvent<FloraMaterial>>>();
cursor
.read(messages)
.filter_map(|e| match e {
bevy::asset::AssetEvent::Modified { id } => Some(*id),
_ => None,
})
.collect()
}
fn flora_app() -> App {
let mut app = app();
app.insert_resource(FloraMaterialSupport);
app
}
fn spawn_oak(app: &mut App, edit: impl FnOnce(&mut TreeParams)) -> Entity {
let mut params = TreePreset::Oak.params();
edit(&mut params);
app.world_mut().spawn((ProceduralTree(params), TreeAge(f32::INFINITY))).id()
}
fn material_of(app: &App, child: Entity) -> Handle<FloraMaterial> {
app.world().get::<MeshMaterial3d<FloraMaterial>>(child).unwrap().0.clone()
}
#[test]
fn trees_use_flora_materials_where_the_renderer_registered_them() {
let mut app = flora_app();
let tree = app.world_mut().spawn((ProceduralTree(TreePreset::Oak.params()), TreeAge(2.0))).id();
app.update();
let (trunk, foliage) = flora_pair(&app, tree);
{
let assets = app.world().resource::<Assets<FloraMaterial>>();
assert!(assets.get(&foliage).unwrap().extension.leaf, "the leaves are drawn as leaves");
assert!(!assets.get(&trunk).unwrap().extension.leaf, "the wood is not");
}
for age in [3.0, 4.0, 5.0] {
app.world_mut().get_mut::<TreeAge>(tree).unwrap().0 = age;
assert!(update_and_modified(&mut app).is_empty(), "age {age} rewrote a material");
}
let render = app.world().get::<TreeRender>(tree).unwrap();
assert_eq!(material_of(&app, render.trunk), trunk);
}
#[test]
fn equal_looks_share_materials() {
let mut app = flora_app();
let a = spawn_oak(&mut app, |_| {});
let b = spawn_oak(&mut app, |p| p.seed = 99);
let c = spawn_oak(&mut app, |p| p.foliage.color = bevy::color::Color::srgb(1.0, 0.0, 0.0));
let d = spawn_oak(&mut app, |p| p.foliage.shading.translucency = 0.2);
let e = spawn_oak(&mut app, |p| p.wind.sway = 2.0);
app.update();
let (aw, al) = flora_pair(&app, a);
let (bw, bl) = flora_pair(&app, b);
assert_eq!((&aw, &al), (&bw, &bl));
let (cw, cl) = flora_pair(&app, c);
assert_eq!(aw, cw);
assert_ne!(al, cl);
let (dw, dl) = flora_pair(&app, d);
assert_eq!(aw, dw);
assert_ne!(al, dl);
let (ew, el) = flora_pair(&app, e);
assert_ne!(aw, ew);
assert_ne!(al, el);
let render = app.world().get::<TreeRender>(b).unwrap();
assert_eq!(material_of(&app, render.trunk), aw);
assert_eq!(material_of(&app, render.foliage.as_ref().unwrap().0), al);
}
#[test]
fn changing_one_trees_look_leaves_the_sharing_tree_alone() {
let mut app = flora_app();
let a = spawn_oak(&mut app, |_| {});
let b = spawn_oak(&mut app, |p| p.seed = 5);
app.update();
let (aw, al) = flora_pair(&app, a);
app.world_mut().get_mut::<ProceduralTree>(b).unwrap().0.foliage.shading.translucency = 0.2;
assert!(update_and_modified(&mut app).is_empty(), "the shared asset is not modified");
assert_eq!(flora_pair(&app, a), (aw.clone(), al.clone()));
let (bw, bl) = flora_pair(&app, b);
assert_eq!(bw, aw);
assert_ne!(bl, al);
let assets = app.world().resource::<Assets<FloraMaterial>>();
assert_eq!(assets.get(&bl).unwrap().extension.flora.leaf.x, 0.2);
assert_ne!(assets.get(&al).unwrap().extension.flora.leaf.x, 0.2);
let render = app.world().get::<TreeRender>(b).unwrap();
assert_eq!(material_of(&app, render.foliage.as_ref().unwrap().0), bl);
}
#[test]
fn a_look_nobody_uses_any_more_is_made_again() {
let mut app = flora_app();
let a = spawn_oak(&mut app, |_| {});
app.update();
app.world_mut().entity_mut(a).despawn();
for _ in 0..3 {
app.update();
}
let b = spawn_oak(&mut app, |p| p.seed = 3);
app.update();
let (bw, bl) = flora_pair(&app, b);
let assets = app.world().resource::<Assets<FloraMaterial>>();
assert!(assets.get(&bw).is_some() && assets.get(&bl).is_some());
}
fn wind_buffer(app: &App) -> Vec<[f32; 4]> {
let handle = &app.world().resource::<FloraWindBuffer>().0;
let buffers = app.world().resource::<Assets<ShaderBuffer>>();
buffers.get(handle).unwrap().cast_slice::<[f32; 4]>().unwrap()[..crate::shading::WIND_VECTORS].to_vec()
}
fn modified_buffers(app: &mut App) -> usize {
let mut cursor = app
.world()
.resource::<Messages<bevy::asset::AssetEvent<ShaderBuffer>>>()
.get_cursor_current();
app.update();
let messages = app.world().resource::<Messages<bevy::asset::AssetEvent<ShaderBuffer>>>();
cursor.read(messages).filter(|e| matches!(e, bevy::asset::AssetEvent::Modified { .. })).count()
}
#[test]
fn changing_the_wind_modifies_no_material() {
let mut app = app();
app.insert_resource(FloraMaterialSupport);
app.world_mut().spawn((ProceduralTree(TreePreset::Oak.params()), TreeAge(f32::INFINITY)));
for seed in 1..4 {
spawn_oak(&mut app, |p| p.seed = seed);
}
app.update();
assert!(update_and_modified(&mut app).is_empty());
let (season, still) = (FloraSeason::default(), TreeShakes::default());
assert_eq!(wind_buffer(&app), wind_vectors(&FloraWind::default(), &season, &still).map(|v| v.to_array()));
app.world_mut().resource_mut::<FloraWind>().strength = 0.9;
assert!(update_and_modified(&mut app).is_empty(), "a new strength modified a material");
let wind = *app.world().resource::<FloraWind>();
assert_eq!(wind_buffer(&app), wind_vectors(&wind, &season, &still).map(|v| v.to_array()));
assert_eq!(wind_buffer(&app)[0][2], 0.9);
app.world_mut().resource_mut::<FloraWind>().launch_gust(GustFront::default(), &Time::default());
assert!(update_and_modified(&mut app).is_empty(), "a launched gust modified a material");
let wind = *app.world().resource::<FloraWind>();
assert_eq!(wind_buffer(&app), wind_vectors(&wind, &season, &still).map(|v| v.to_array()));
assert!(wind_buffer(&app)[4][2] > 0.0);
app.world_mut().resource_mut::<FloraWind>().set_changed();
assert_eq!(modified_buffers(&mut app), 0, "an unchanged wind wrote the buffer");
app.world_mut().resource_mut::<FloraWind>().strength = 0.2;
assert_eq!(modified_buffers(&mut app), 1, "a changed wind writes the buffer once");
*app.world_mut().resource_mut::<FloraSeason>() = FloraSeason::AUTUMN;
assert!(update_and_modified(&mut app).is_empty(), "the season modified a material");
let wind = *app.world().resource::<FloraWind>();
let still = TreeShakes::default();
assert_eq!(wind_buffer(&app), wind_vectors(&wind, &FloraSeason::AUTUMN, &still).map(|v| v.to_array()));
assert_eq!(wind_buffer(&app)[5][0], 1.0);
let shake = TreeShake { at: Vec2::new(2.0, 1.0), duration: 1.0e4, ..TreeShake::default() };
let time = *app.world().resource::<Time>();
app.world_mut().resource_mut::<TreeShakes>().shake(shake, &time);
assert!(update_and_modified(&mut app).is_empty(), "a shake modified a material");
let shakes = app.world().resource::<TreeShakes>().clone();
assert_eq!(wind_buffer(&app), wind_vectors(&wind, &FloraSeason::AUTUMN, &shakes).map(|v| v.to_array()));
assert_eq!(wind_buffer(&app)[7][0], 2.0);
}
#[test]
fn a_shake_that_has_died_away_is_dropped_and_the_shaders_skip_shakes_again() {
use bevy::time::TimeUpdateStrategy;
use std::time::Duration;
let mut app = app();
app.insert_resource(FloraMaterialSupport);
app.insert_resource(TimeUpdateStrategy::ManualDuration(Duration::from_millis(100)));
app.update();
let time = *app.world().resource::<Time>();
let knock = TreeShake { duration: 0.25, ..TreeShake::default() };
app.world_mut().resource_mut::<TreeShakes>().shake(knock, &time);
app.update();
assert_eq!(app.world().resource::<TreeShakes>().iter().count(), 1, "a shake under way was dropped");
assert!(wind_buffer(&app)[6][0] >= time.elapsed_secs_wrapped(), "the header says no shake is under way");
for _ in 0..3 {
app.update();
}
assert_eq!(app.world().resource::<TreeShakes>().iter().count(), 0, "a finished shake was kept");
assert_eq!(wind_buffer(&app)[6][0], -1.0, "the shaders still look at the shakes");
}
#[test]
fn a_material_made_after_a_wind_change_sees_the_current_wind() {
let mut app = app();
app.insert_resource(FloraMaterialSupport);
app.world_mut().resource_mut::<FloraWind>().strength = 0.77;
app.update();
let tree = app.world_mut().spawn((ProceduralTree(TreePreset::Oak.params()), TreeAge(f32::INFINITY))).id();
app.update();
let (_, foliage) = flora_pair(&app, tree);
let assets = app.world().resource::<Assets<FloraMaterial>>();
assert_eq!(assets.get(&foliage).unwrap().extension.wind, app.world().resource::<FloraWindBuffer>().0);
assert_eq!(wind_buffer(&app)[0][2], 0.77);
}
#[test]
fn a_fire_grows_the_shared_buffer_and_arms_the_materials_until_it_goes() {
use crate::fire::{FIRE_SLOT, FloraFire};
use bevy::math::Vec3;
use crate::shading::WIND_VECTORS;
let shared = |app: &App| {
let handle = &app.world().resource::<FloraWindBuffer>().0;
let buffers = app.world().resource::<Assets<ShaderBuffer>>();
buffers.get(handle).unwrap().cast_slice::<[f32; 4]>().unwrap().to_vec()
};
let mut app = app();
app.insert_resource(FloraMaterialSupport);
let tree = app.world_mut().spawn((ProceduralTree(TreePreset::Oak.params()), TreeAge(f32::INFINITY))).id();
app.update();
let (wood, foliage) = flora_pair(&app, tree);
let armed = |app: &App| {
let assets = app.world().resource::<Assets<FloraMaterial>>();
(assets.get(&wood).unwrap().extension.fire, assets.get(&foliage).unwrap().extension.fire)
};
let both = |modified: &[bevy::asset::AssetId<FloraMaterial>]| {
modified.contains(&wood.id()) && modified.contains(&foliage.id())
};
assert_eq!(shared(&app).len(), FIRE_SLOT + 1);
assert_eq!(armed(&app), (false, false));
let weather = wind_buffer(&app);
app.insert_resource(FloraFire::with_cells(Vec2::ZERO, 16.0, 8));
assert!(both(&update_and_modified(&mut app)), "arming did not re-prepare the materials");
assert_eq!(armed(&app), (true, true));
let buffer = shared(&app);
assert_eq!(buffer.len(), FIRE_SLOT + 2 + 32);
assert_eq!(&buffer[..WIND_VECTORS], &weather[..]);
assert_eq!(buffer[FIRE_SLOT][3], 0.0, "nothing burns, yet the shaders read the cells");
assert!(both(&update_and_modified(&mut app)), "the materials were not bound to the grown buffer");
assert!(update_and_modified(&mut app).is_empty(), "the materials are prepared again and again");
app.world_mut().resource_mut::<FloraFire>().scorch(Vec3::new(1.0, 0.0, 1.0), 0.1, 0.5);
assert!(update_and_modified(&mut app).is_empty(), "the fire's cells modified a material");
let buffer = shared(&app);
assert_eq!(buffer[FIRE_SLOT], [-8.0, -8.0, 0.5, 8.0]);
assert_eq!(buffer[FIRE_SLOT + 1], [-2.0, -2.0, 4.0, 4.0]);
assert_eq!(buffer[FIRE_SLOT + 2 + 18], [0.5, 0.0, 0.0, 0.0]);
app.world_mut().remove_resource::<FloraFire>();
app.update();
assert_eq!(shared(&app).len(), FIRE_SLOT + 1);
assert_eq!(armed(&app), (false, false));
}
fn assert_levels(app: &App, tree: Entity, params: &TreeParams, age: f32, lod: &LodSettings) {
use crate::lod::build_lods;
let world = app.world();
let margins = |child: Entity| {
let range = world.get::<VisibilityRange>(child).unwrap_or_else(|| panic!("{child} has no band"));
(range.start_margin.clone(), range.end_margin.clone())
};
let band_of = |range: VisibilityRange| (range.start_margin, range.end_margin);
let mesh = |child: Entity| {
let handle = &world.get::<Mesh3d>(child).unwrap().0;
world.resource::<Assets<Mesh>>().get(handle).unwrap().attribute(Mesh::ATTRIBUTE_POSITION).unwrap().as_float3().unwrap().to_vec()
};
let render = world.get::<TreeRender>(tree).unwrap();
assert_eq!(render.age, age);
let level0 = [Some(render.trunk), render.foliage.as_ref().map(|c| c.0), render.flowers.as_ref().map(|c| c.0)];
for child in level0.into_iter().flatten() {
assert_eq!(margins(child), band_of(level_band(0, lod.distances)));
}
let expected = build_lods(params, age, lod);
let (settings, levels) = render.lod.as_ref().expect("no coarse levels");
assert_eq!(settings, lod);
for (index, level) in levels.iter().enumerate() {
let want = &expected[index + 1];
let (wood, _) = level.wood.as_ref().expect("no coarse wood");
let (leaves, _) = level.leaves.as_ref().expect("no coarse leaves");
for child in [*wood, *leaves] {
assert_eq!(world.get::<TreeLodMesh>(child), Some(&TreeLodMesh(index as u8 + 1)));
assert_eq!(margins(child), band_of(level_band(index + 1, lod.distances)));
}
let wood_positions: Vec<[f32; 3]> = want.trunk.positions.iter().map(|p| p.to_array()).collect();
assert_eq!(mesh(*wood), wood_positions, "level {} wood", index + 1);
assert_eq!(mesh(*leaves).len(), want.foliage.positions.len() + want.flowers.positions.len());
}
assert_eq!(world.get::<Children>(tree).unwrap().len(), 7);
}
#[test]
fn a_tree_with_levels_of_detail_draws_its_coarse_levels_beyond_the_full_tree() {
use crate::felling::FellTree;
let mut app = app();
let params = TreePreset::Cherry.params();
let mut lod = LodSettings::default();
let tree = app.world_mut().spawn((ProceduralTree(params.clone()), TreeAge(9.0), TreeLod(lod), Transform::default())).id();
app.update();
assert_levels(&app, tree, ¶ms, 9.0, &lod);
app.world_mut().get_mut::<TreeAge>(tree).unwrap().0 = f32::INFINITY;
app.update();
assert_levels(&app, tree, ¶ms, f32::INFINITY, &lod);
lod.distances.lod1 = 12.0;
lod.distances.lod2 = 40.0;
app.world_mut().get_mut::<TreeLod>(tree).unwrap().0 = lod;
app.update();
assert_levels(&app, tree, ¶ms, f32::INFINITY, &lod);
let coarse: Vec<Entity> = {
let render = app.world().get::<TreeRender>(tree).unwrap();
render.lod.as_ref().unwrap().1.iter().flat_map(CoarseLevel::children).collect()
};
app.world_mut().entity_mut(tree).remove::<TreeLod>();
app.update();
let full_only = |app: &App| {
let world = app.world();
let render = world.get::<TreeRender>(tree).unwrap();
assert!(render.lod.is_none());
assert!(coarse.iter().all(|child| world.get_entity(*child).is_err()), "a coarse level is left");
let level0 = [Some(render.trunk), render.foliage.as_ref().map(|c| c.0), render.flowers.as_ref().map(|c| c.0)];
for child in level0.into_iter().flatten() {
assert!(world.get::<VisibilityRange>(child).is_none(), "{child} is still banded");
}
};
full_only(&app);
assert_eq!(app.world().get::<Children>(tree).unwrap().len(), 3);
app.world_mut().entity_mut(tree).insert(TreeLod(lod));
app.update();
assert_levels(&app, tree, ¶ms, f32::INFINITY, &lod);
let coarse: Vec<Entity> = {
let render = app.world().get::<TreeRender>(tree).unwrap();
render.lod.as_ref().unwrap().1.iter().flat_map(CoarseLevel::children).collect()
};
app.world_mut().entity_mut(tree).insert(FellTree::default());
app.update();
let world = app.world();
assert!(coarse.iter().all(|child| world.get_entity(*child).is_err()), "a coarse level is left");
let render = world.get::<TreeRender>(tree).unwrap();
for child in std::iter::once(render.trunk).chain(render.foliage.as_ref().map(|c| c.0)) {
assert!(world.get::<VisibilityRange>(child).is_none(), "{child} is still banded");
}
let background = app.world_mut().spawn((ProceduralTree(params.clone()), TreeAge(9.0), TreeLod(lod), BuildInBackground)).id();
for _ in 0..3000 {
app.update();
if app.world().get::<TreeRender>(background).is_some() {
break;
}
std::thread::sleep(std::time::Duration::from_millis(1));
}
assert_levels(&app, background, ¶ms, 9.0, &lod);
}
}