use bevy::asset::{AssetId, Assets};
use bevy::color::{Color, LinearRgba, Mix};
use bevy::ecs::message::{MessageCursor, Messages};
use bevy::ecs::prelude::*;
use bevy::platform::collections::HashMap;
use bevy::render::storage::ShaderBuffer;
use bevy::time::Time;
use crate::felling::TreeStump;
use crate::pool::{PoolColors, PoolRecolors, RecolorPool, TreePool};
use crate::push::{PUSH_SLOT, PUSH_VECTORS};
use crate::recolor::{FromPool, RecolorTree};
use crate::shading::{FloraMaterialSupport, FloraWindBuffer};
pub(crate) const MAX_GLIDE_LOOKS: usize = 64;
pub(crate) const GLIDE_SLOT: usize = PUSH_SLOT + PUSH_VECTORS;
pub(crate) const GLIDE_VECTORS: usize = 1 + 2 * MAX_GLIDE_LOOKS;
#[derive(Resource, Clone, Debug, PartialEq)]
pub(crate) struct GlideTints(pub(crate) [[f32; 4]; GLIDE_VECTORS]);
impl Default for GlideTints {
fn default() -> Self {
let mut vectors = [[1.0; 4]; GLIDE_VECTORS];
vectors[0] = [0.0; 4];
Self(vectors)
}
}
#[derive(Resource, Default)]
pub(crate) struct GlideSlots(HashMap<(AssetId<TreePool>, u32), u32>);
impl GlideSlots {
pub(crate) fn tag(&mut self, pool: AssetId<TreePool>, look: u32) -> u32 {
if let Some(&tag) = self.0.get(&(pool, look)) {
return tag;
}
if self.0.len() >= MAX_GLIDE_LOOKS {
return 0;
}
let tag = self.0.len() as u32 + 1;
self.0.insert((pool, look), tag);
tag
}
fn get(&self, pool: AssetId<TreePool>, look: u32) -> u32 {
self.0.get(&(pool, look)).copied().unwrap_or(0)
}
}
#[derive(Clone, Copy, Debug)]
pub(crate) struct Glide {
base: PoolColors,
from: PoolColors,
to: PoolColors,
start: f32,
seconds: f32,
tag: u32,
}
impl Glide {
fn share(&self, now: f32) -> f32 {
((now - self.start) / self.seconds).clamp(0.0, 1.0)
}
fn at(&self, now: f32) -> PoolColors {
let t = self.share(now);
if t >= 1.0 {
return self.to;
}
let mix = |a: Color, b: Color| Color::from(LinearRgba::from(a).mix(&LinearRgba::from(b), t));
PoolColors { wood: mix(self.from.wood, self.to.wood), leaves: mix(self.from.leaves, self.to.leaves), unlit: self.from.unlit }
}
}
fn ratio(now: Color, base: Color) -> [f32; 4] {
let (now, base) = (LinearRgba::from(now), LinearRgba::from(base));
let channel = |n: f32, b: f32| if b > 1.0e-5 { (n / b).clamp(0.0, 1000.0) } else { 1.0 };
[channel(now.red, base.red), channel(now.green, base.green), channel(now.blue, base.blue), 1.0]
}
#[allow(clippy::too_many_arguments, clippy::type_complexity)]
pub(crate) fn glide_looks(
mut messages: ResMut<Messages<RecolorPool>>,
mut cursor: Local<MessageCursor<RecolorPool>>,
mut glides: Local<HashMap<(AssetId<TreePool>, u32), Glide>>,
pools: Res<Assets<TreePool>>,
recolors: Res<PoolRecolors>,
slots: Res<GlideSlots>,
support: Option<Res<FloraMaterialSupport>>,
mut tints: ResMut<GlideTints>,
buffer: Option<Res<FloraWindBuffer>>,
mut buffers: Option<ResMut<Assets<ShaderBuffer>>>,
mut felled: MessageWriter<RecolorTree>,
left: Query<(Entity, &FromPool, Option<&TreeStump>)>,
time: Res<Time>,
) {
let now = time.elapsed_secs();
let mut wanted = tints.0;
for message in cursor.read(&messages) {
let Some(pool) = pools.get(message.pool) else { continue };
let look = message.look % pool.looks.len().max(1) as u32;
let key = (message.pool, look);
let running = glides.remove(&key);
if let Some(stopped) = running.filter(|glide| glide.tag > 0) {
reset(&mut wanted, stopped.tag);
}
if message.glide > 0.0 {
let shown = recolors.colors(message.pool, look, pool);
let tag = if support.is_some() { slots.get(message.pool, look) } else { 0 };
let (base, from) = match running {
Some(glide) if glide.tag > 0 && tag > 0 => (glide.base, glide.at(now)),
Some(glide) => (shown, glide.at(now)),
None => (shown, shown),
};
glides.insert(key, Glide { base, from, to: message.colors, start: now, seconds: message.glide, tag });
}
}
let mut writes = Vec::new();
glides.retain(|&(pool, look), glide| {
let colors = glide.at(now);
let arrived = glide.share(now) >= 1.0;
if glide.tag == 0 || arrived {
writes.push(RecolorPool { pool, look, colors, glide: 0.0 });
if glide.tag > 0 {
reset(&mut wanted, glide.tag);
}
return !arrived;
}
let k = glide.tag as usize;
wanted[2 * k - 1] = ratio(colors.wood, glide.base.wood);
wanted[2 * k] = ratio(colors.leaves, glide.base.leaves);
for (entity, from, stump) in &left {
if (from.pool, from.look) == (pool, look) && !stump.and_then(|s| s.tree).is_some_and(|tree| left.contains(tree)) {
felled.write(RecolorTree { tree: entity, colors });
}
}
true
});
wanted[0][0] = if glides.values().any(|glide| glide.tag > 0) { 1.0 } else { 0.0 };
for write in writes {
messages.write(write);
}
cursor.read(&messages).for_each(drop);
if wanted == tints.0 {
return;
}
tints.0 = wanted;
let (Some(buffer), Some(buffers)) = (buffer, buffers.as_deref_mut()) 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() >= GLIDE_SLOT + GLIDE_VECTORS
{
slice[GLIDE_SLOT..GLIDE_SLOT + GLIDE_VECTORS].copy_from_slice(&tints.0);
}
}
fn reset(vectors: &mut [[f32; 4]; GLIDE_VECTORS], tag: u32) {
let k = tag as usize;
vectors[2 * k - 1] = [1.0; 4];
vectors[2 * k] = [1.0; 4];
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use bevy::app::App;
use bevy::asset::{AssetApp, AssetEvent, AssetPlugin, Handle};
use bevy::mesh::{Mesh, Mesh3d, MeshTag};
use bevy::pbr::StandardMaterial;
use bevy::time::TimeUpdateStrategy;
use super::*;
use crate::lod::LodSettings;
use crate::plugin::FloraPlugin;
use crate::pool::{PoolMaterials, PooledLook, PooledTree, build_pool};
use crate::presets::TreePreset;
use crate::shading::FloraMaterial;
const CRIMSON: PoolColors = PoolColors { wood: Color::srgb(0.30, 0.12, 0.05), leaves: Color::srgb(0.85, 0.25, 0.10), unlit: false };
const VIOLET: PoolColors = PoolColors { wood: Color::srgb(0.40, 0.10, 0.50), leaves: Color::srgb(0.70, 0.20, 0.90), unlit: false };
fn app() -> App {
let mut app = App::new();
app.add_plugins((bevy::MinimalPlugins, AssetPlugin::default()));
app.init_asset::<Mesh>();
app.init_asset::<StandardMaterial>();
app.init_asset::<bevy::image::Image>();
app.add_plugins(FloraPlugin);
app.insert_resource(FloraMaterialSupport);
app.insert_resource(TimeUpdateStrategy::ManualDuration(Duration::from_millis(100)));
app
}
fn planted(app: &mut App) -> (Handle<TreePool>, Entity) {
let mut oak = TreePreset::Oak.params();
oak.bark.scan = None;
oak.foliage.scan = None;
let mut pool = app.world_mut().resource_scope(|_, mut meshes: Mut<Assets<Mesh>>| {
build_pool(&oak, 1, f32::INFINITY, &LodSettings::default(), &mut meshes)
});
pool.add_look(CRIMSON);
let pool = app.world_mut().resource_mut::<Assets<TreePool>>().add(pool);
let tree = app.world_mut().spawn((PooledTree { pool: pool.clone(), variant: 0 }, PooledLook(1))).id();
(pool, tree)
}
fn drawn(app: &App, pool: &Handle<TreePool>) -> [AssetId<FloraMaterial>; 2] {
let pools = app.world().resource::<Assets<TreePool>>();
let Some(PoolMaterials::Flora { wood, leaves }) = pools.get(pool).unwrap().materials.get(1) else { panic!("flora materials") };
[wood.id(), leaves.id()]
}
fn shared(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()[GLIDE_SLOT..GLIDE_SLOT + GLIDE_VECTORS].to_vec()
}
#[test]
fn a_glide_on_the_gpu_writes_only_the_shared_buffer_and_arrives_in_the_materials() {
let mut app = app();
let (pool, tree) = planted(&mut app);
let (twin, _) = planted(&mut app);
app.update();
app.update();
assert_eq!(drawn(&app, &pool), drawn(&app, &twin), "alike, the two share");
let before = drawn(&app, &pool);
let tags: Vec<u32> = app.world().get::<Children>(tree).unwrap().iter().filter_map(|c| app.world().get::<MeshTag>(c).map(|t| t.value)).collect();
let meshes = app.world().get::<Children>(tree).unwrap().iter().filter(|c| app.world().get::<Mesh3d>(*c).is_some()).count();
let k = tags[0] as usize;
assert!(k >= 1 && tags.len() == meshes && tags.iter().all(|t| *t as usize == k), "every mesh carries the look's tag: {tags:?}");
app.world_mut().write_message(RecolorPool { pool: pool.id(), look: 1, colors: VIOLET, glide: 1.0 });
let mut events = app.world().resource::<Messages<AssetEvent<FloraMaterial>>>().get_cursor_current();
for _ in 0..6 {
app.update();
}
let modified = events
.read(app.world().resource::<Messages<AssetEvent<FloraMaterial>>>())
.filter(|e| matches!(e, AssetEvent::Modified { .. }))
.count();
assert_eq!(modified, 0, "a glide on the GPU writes no material");
assert_eq!(drawn(&app, &pool), before);
let half = |a: Color, b: Color| Color::from(LinearRgba::from(a).mix(&LinearRgba::from(b), 0.5));
let tints = app.world().resource::<GlideTints>().0;
assert_eq!(tints[0][0], 1.0, "a look glides");
for (got, want) in [(tints[2 * k - 1], ratio(half(CRIMSON.wood, VIOLET.wood), CRIMSON.wood)), (tints[2 * k], ratio(half(CRIMSON.leaves, VIOLET.leaves), CRIMSON.leaves))] {
assert!(got.iter().zip(want).all(|(g, w)| (g - w).abs() < 1.0e-4), "halfway: {got:?}, wanted {want:?}");
}
assert_eq!(shared(&app), tints.to_vec(), "the shaders read them");
let other = tints.iter().enumerate().filter(|(i, _)| *i > 0 && *i != 2 * k - 1 && *i != 2 * k).all(|(_, v)| *v == [1.0; 4]);
assert!(other, "no other look moves");
for _ in 0..6 {
app.update();
}
let tints = app.world().resource::<GlideTints>().0;
assert_eq!(tints, GlideTints::default().0);
assert_eq!(shared(&app), tints.to_vec());
assert_ne!(drawn(&app, &pool), before, "the look's materials changed");
app.world_mut().write_message(RecolorPool { pool: twin.id(), look: 1, colors: VIOLET, glide: 0.0 });
app.update();
assert_eq!(drawn(&app, &twin), drawn(&app, &pool));
}
}