use std::sync::RwLock;
use bevy::asset::{Asset, AssetId, Assets, Handle};
use bevy::ecs::prelude::*;
use bevy::math::{Vec3, Vec4};
use bevy::reflect::Reflect;
use bevy::render::storage::ShaderBuffer;
use bevy::shader::Shader;
use bevy::camera::Projection;
use bevy::transform::components::GlobalTransform;
use crate::gpu::GrowthMaterial;
use crate::impostor::ImpostorMaterial;
use crate::shading::{FloraMaterial, FloraWindBuffer, TreeShakes};
use crate::units::FloraUnits;
#[derive(Component, Clone, Copy, Debug, PartialEq, Reflect)]
#[reflect(Component)]
pub struct SeeThroughTrees {
pub target: Vec3,
pub radius: f32,
pub keep: f32,
pub inner: f32,
pub in_front: Option<f32>,
pub floor: Option<f32>,
}
impl Default for SeeThroughTrees {
fn default() -> Self {
Self { target: Vec3::ZERO, radius: 1.5, keep: 0.0, inner: 0.7, in_front: None, floor: None }
}
}
pub(crate) const SEE_THROUGH_SLOT: usize = 7 + 2 * TreeShakes::MAX;
pub(crate) const SEE_THROUGH_VECTORS: usize = 4;
const NO_FLOOR: f32 = -1.0e30;
#[derive(Resource, Clone, Copy, Debug, Default, PartialEq)]
pub(crate) struct SeeThroughCapsule(pub [Vec4; SEE_THROUGH_VECTORS]);
impl SeeThroughCapsule {
fn of(
camera: Option<(&SeeThroughTrees, &GlobalTransform, Option<&Projection>)>,
units: &FloraUnits,
) -> Self {
let Some((fade, at, projection)) = camera else {
return Self::default();
};
let orthographic = matches!(projection, Some(Projection::Orthographic(_)));
Self([
at.translation().extend(units.world(fade.radius.max(0.0))),
fade.target.extend(if orthographic { 2.0 } else { 1.0 }),
at.forward().as_vec3().extend(fade.keep.clamp(0.0, 1.0)),
Vec4::new(
fade.inner.clamp(0.0, 0.99),
fade.in_front.map_or(-1.0, |d| units.world(d.max(0.0))),
fade.floor.unwrap_or(NO_FLOOR),
0.0,
),
])
}
}
pub(crate) fn sync_see_through(
cameras: Query<(&SeeThroughTrees, &GlobalTransform, Option<&Projection>)>,
units: Res<FloraUnits>,
mut capsule: ResMut<SeeThroughCapsule>,
buffer: Option<Res<FloraWindBuffer>>,
buffers: Option<ResMut<Assets<ShaderBuffer>>>,
) {
let now = SeeThroughCapsule::of(cameras.iter().next(), &units);
if *capsule == now {
return;
}
*capsule = now;
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() >= SEE_THROUGH_SLOT + SEE_THROUGH_VECTORS
{
for (slot, vector) in slice[SEE_THROUGH_SLOT..].iter_mut().zip(now.0) {
*slot = vector.to_array();
}
}
}
pub(crate) fn arm_see_through(
cameras: Query<(), With<SeeThroughTrees>>,
flora: Option<ResMut<Assets<FloraMaterial>>>,
growth: Option<ResMut<Assets<GrowthMaterial>>>,
impostors: Option<ResMut<Assets<ImpostorMaterial>>>,
) {
let on = !cameras.is_empty();
if let Some(mut assets) = flora {
arm(&mut assets, on, |m| m.extension.see_through, |m, on| m.extension.see_through = on);
}
if let Some(mut assets) = growth {
arm(&mut assets, on, |m| m.extension.see_through, |m, on| m.extension.see_through = on);
}
if let Some(mut assets) = impostors {
arm(&mut assets, on, |m| m.see_through, |m, on| m.see_through = on);
}
}
pub(crate) fn arm<A: Asset>(assets: &mut Assets<A>, on: bool, flag: fn(&A) -> bool, set: fn(&mut A, bool)) {
let stale: Vec<AssetId<A>> = assets.iter().filter(|(_, material)| flag(material) != on).map(|(id, _)| id).collect();
for id in stale {
if let Some(mut material) = assets.get_mut(id) {
set(&mut material, on);
}
}
}
static PREPASS_FRAGMENT: RwLock<Option<Handle<Shader>>> = RwLock::new(None);
pub(crate) fn set_prepass_fragment(shader: Handle<Shader>) {
if let Ok(mut slot) = PREPASS_FRAGMENT.write() {
*slot = Some(shader);
}
}
pub(crate) fn prepass_fragment() -> Option<Handle<Shader>> {
PREPASS_FRAGMENT.read().ok()?.clone()
}
#[cfg(test)]
mod tests {
use bevy::app::App;
use bevy::asset::{AssetApp, AssetPlugin};
use bevy::mesh::Mesh;
use bevy::pbr::StandardMaterial;
use super::*;
use crate::shading::{FloraMaterialSupport, FloraWind};
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
}
fn flags(app: &App) -> Vec<bool> {
app.world().resource::<Assets<FloraMaterial>>().iter().map(|(_, m)| m.extension.see_through).collect()
}
fn slots(app: &App) -> [[f32; 4]; SEE_THROUGH_VECTORS] {
let buffer = app.world().resource::<FloraWindBuffer>();
let data = app.world().resource::<Assets<ShaderBuffer>>().get(&buffer.0).unwrap().cast_slice::<[f32; 4]>().unwrap();
std::array::from_fn(|i| data[SEE_THROUGH_SLOT + i])
}
#[test]
fn a_camera_that_sees_through_turns_the_fade_on_in_every_flora_material_until_it_goes() {
let mut app = app();
app.world_mut().spawn(ProceduralTree(TreePreset::Oak.params()));
app.update();
assert!(!flags(&app).is_empty() && flags(&app).iter().all(|on| !on), "off without a camera");
assert_eq!(slots(&app), [[0.0; 4]; SEE_THROUGH_VECTORS]);
let camera = app
.world_mut()
.spawn((
SeeThroughTrees { target: Vec3::new(4.0, 5.0, 6.0), radius: 2.0, ..Default::default() },
GlobalTransform::from_translation(Vec3::new(1.0, 2.0, 3.0)),
))
.id();
app.update();
assert!(flags(&app).iter().all(|on| *on));
assert_eq!(
slots(&app),
[[1.0, 2.0, 3.0, 2.0], [4.0, 5.0, 6.0, 1.0], [0.0, 0.0, -1.0, 0.0], [0.7, -1.0, NO_FLOOR, 0.0]]
);
app.world_mut().spawn(ProceduralTree(TreePreset::Birch.params()));
app.world_mut().resource_mut::<FloraWind>().strength = 0.9;
app.update();
app.update();
assert!(flags(&app).len() > 2 && flags(&app).iter().all(|on| *on));
assert_eq!(slots(&app)[1], [4.0, 5.0, 6.0, 1.0]);
let fade = SeeThroughTrees {
target: Vec3::new(4.0, 5.0, 6.0),
radius: 2.0,
keep: 0.35,
inner: 0.55,
in_front: Some(1.2),
floor: Some(0.45),
};
let projection = Projection::Orthographic(bevy::camera::OrthographicProjection::default_3d());
app.world_mut().entity_mut(camera).insert((fade, projection));
app.update();
assert_eq!(slots(&app)[1..], [[4.0, 5.0, 6.0, 2.0], [0.0, 0.0, -1.0, 0.35], [0.55, 1.2, 0.45, 0.0]]);
app.world_mut().entity_mut(camera).remove::<SeeThroughTrees>();
app.update();
assert!(flags(&app).iter().all(|on| !on), "off again once no camera sees through");
assert_eq!(slots(&app), [[0.0; 4]; SEE_THROUGH_VECTORS]);
}
#[test]
fn the_radius_and_the_distance_in_front_are_metres_the_floor_is_not() {
let fade = SeeThroughTrees { radius: 2.0, in_front: Some(1.5), floor: Some(0.45), ..Default::default() };
let at = GlobalTransform::default();
let metres = SeeThroughCapsule::of(Some((&fade, &at, None)), &FloraUnits::default());
let feet = SeeThroughCapsule::of(Some((&fade, &at, None)), &FloraUnits { units_per_metre: 3.0 });
assert_eq!((metres.0[0].w, metres.0[3].y, metres.0[3].z), (2.0, 1.5, 0.45));
assert_eq!((feet.0[0].w, feet.0[3].y, feet.0[3].z), (6.0, 4.5, 0.45));
}
}