use bevy::app::App;
use bevy::asset::{Asset, AssetApp, AssetServer, Assets, Handle, embedded_asset};
use bevy::color::Color;
use bevy::ecs::prelude::*;
use bevy::log::warn;
use bevy::material::OpaqueRendererMethod;
use bevy::math::{Vec2, Vec4};
use bevy::mesh::MeshVertexBufferLayoutRef;
use bevy::pbr::{
ExtendedMaterial, MaterialExtension, MaterialExtensionKey, MaterialExtensionPipeline,
MaterialPlugin, PbrPlugin, StandardMaterial,
};
use bevy::reflect::Reflect;
use bevy::render::RenderApp;
use bevy::render::render_resource::{
AsBindGroup, Face, RenderPipelineDescriptor, ShaderType, SpecializedMeshPipelineError,
};
use bevy::render::renderer::RenderDevice;
use bevy::render::storage::ShaderBuffer;
use bevy::shader::ShaderRef;
use bevy::time::Time;
use crate::impostor::ImpostorSupport;
use crate::mesh::ATTRIBUTE_LEAF_SWAY;
use crate::params::{LeafStyle, TreeParams};
mod share;
pub use share::SharedFlora;
#[derive(Clone, Copy, Debug, PartialEq, Reflect)]
pub struct GustFront {
pub origin: Vec2,
pub direction: Vec2,
pub speed: f32,
pub width: f32,
pub strength: f32,
pub start: f32,
}
impl Default for GustFront {
fn default() -> Self {
Self { origin: Vec2::ZERO, direction: Vec2::X, speed: 15.0, width: 30.0, strength: 1.0, start: 0.0 }
}
}
#[derive(Resource, Clone, Copy, Debug, PartialEq, Reflect)]
#[reflect(Resource)]
pub struct FloraWind {
pub direction: Vec2,
pub strength: f32,
pub speed: f32,
pub flutter: f32,
pub bend: f32,
pub gust_strength: f32,
pub gust_frequency: f32,
pub flutter_frequency: f32,
pub turbulence: f32,
pub gust: Option<GustFront>,
}
impl Default for FloraWind {
fn default() -> Self {
Self {
direction: Vec2::new(0.8, 0.6),
strength: 0.3,
speed: 1.0,
flutter: 1.0,
bend: 1.0,
gust_strength: 0.4,
gust_frequency: 1.0,
flutter_frequency: 1.0,
turbulence: 0.2,
gust: None,
}
}
}
impl FloraWind {
pub fn launch_gust(&mut self, mut front: GustFront, time: &Time) {
front.start = time.elapsed_secs_wrapped();
self.gust = Some(front);
}
}
#[derive(ShaderType, Reflect, Clone, Copy, Debug, PartialEq, Default)]
pub struct FloraUniform {
pub tree: Vec4,
pub response: Vec4,
pub leaf: Vec4,
pub tint: Vec4,
}
impl FloraUniform {
pub(crate) fn new(params: &TreeParams) -> Self {
let s = ¶ms.foliage.shading;
let r = ¶ms.wind;
let veined = !matches!(params.foliage.style, LeafStyle::NeedleCluster | LeafStyle::ClusterSphere);
let tint = s.transmission_tint.to_linear();
Self {
tree: Vec4::new(1.0 / params.trunk.height.max(0.01), 0.0, 0.0, 0.0),
response: Vec4::new(
r.sway.max(0.0),
r.flutter.max(0.0),
r.sway_frequency.max(0.0),
r.flutter_frequency.max(0.0),
),
leaf: Vec4::new(
s.translucency.max(0.0),
s.roughness.clamp(0.05, 1.0),
if veined { s.veins.clamp(0.0, 1.0) } else { 0.0 },
s.underside.clamp(0.0, 1.0),
),
tint: Vec4::new(tint.red, tint.green, tint.blue, 0.0),
}
}
pub(crate) fn wood(self) -> Self {
Self { leaf: Vec4::ZERO, tint: Vec4::ZERO, ..self }
}
}
pub(crate) fn wind_vectors(wind: &FloraWind) -> [Vec4; 5] {
let d = wind.direction.normalize_or_zero();
let (front, front_shape) = match wind.gust {
Some(gust) if gust.direction.normalize_or_zero() != Vec2::ZERO => {
let d = gust.direction.normalize_or_zero();
(
Vec4::new(gust.origin.x, gust.origin.y, d.x, d.y),
Vec4::new(gust.speed.max(0.0), gust.width.max(0.01), gust.strength.max(0.0), gust.start),
)
}
_ => (Vec4::ZERO, Vec4::ZERO),
};
[
Vec4::new(d.x, d.y, wind.strength.max(0.0), wind.speed.max(0.0)),
Vec4::new(wind.bend.max(0.0), wind.flutter.max(0.0), wind.gust_strength.clamp(0.0, 1.0), wind.turbulence.max(0.0)),
Vec4::new(wind.gust_frequency.max(0.0), wind.flutter_frequency.max(0.0), 0.0, 0.0),
front,
front_shape,
]
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct FloraKey {
pub leaf: bool,
}
#[derive(Asset, AsBindGroup, Reflect, Debug, Clone, PartialEq)]
#[bind_group_data(FloraKey)]
pub struct FloraExtension {
#[uniform(101)]
pub flora: FloraUniform,
#[storage(102, read_only)]
pub wind: Handle<ShaderBuffer>,
pub leaf: bool,
}
impl From<&FloraExtension> for FloraKey {
fn from(extension: &FloraExtension) -> Self {
Self { leaf: extension.leaf }
}
}
pub type FloraMaterial = ExtendedMaterial<StandardMaterial, FloraExtension>;
impl MaterialExtension for FloraExtension {
fn vertex_shader() -> ShaderRef {
"embedded://bevy_flora/shading/vertex.wesl".into()
}
fn prepass_vertex_shader() -> ShaderRef {
"embedded://bevy_flora/shading/prepass.wesl".into()
}
fn fragment_shader() -> ShaderRef {
"embedded://bevy_flora/shading/fragment.wesl".into()
}
fn specialize(
_pipeline: &MaterialExtensionPipeline,
descriptor: &mut RenderPipelineDescriptor,
layout: &MeshVertexBufferLayoutRef,
key: MaterialExtensionKey<Self>,
) -> Result<(), SpecializedMeshPipelineError> {
specialize_flora(descriptor, layout, key.bind_group_data.leaf)
}
}
pub(crate) fn specialize_flora(
descriptor: &mut RenderPipelineDescriptor,
layout: &MeshVertexBufferLayoutRef,
leaf: bool,
) -> Result<(), SpecializedMeshPipelineError> {
if !leaf {
return Ok(());
}
descriptor.vertex.shader_defs.push("FLORA_LEAF".into());
if let Some(fragment) = descriptor.fragment.as_mut() {
fragment.shader_defs.push("FLORA_LEAF".into());
}
let sway = layout.0.get_layout(&[ATTRIBUTE_LEAF_SWAY.at_shader_location(11)])?;
descriptor.vertex.buffers[0].attributes.extend(sway.attributes);
Ok(())
}
pub(crate) fn flora_base(color: Color) -> StandardMaterial {
StandardMaterial {
base_color: color,
perceptual_roughness: 1.0,
cull_mode: Some(Face::Back),
opaque_render_method: OpaqueRendererMethod::Forward,
..Default::default()
}
}
pub(crate) fn flora_material(
color: Color,
flora: FloraUniform,
leaf: bool,
wind: &FloraWindBuffer,
) -> FloraMaterial {
FloraMaterial { base: flora_base(color), extension: FloraExtension { flora, wind: wind.0.clone(), leaf } }
}
#[derive(Resource, Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct FloraMaterialSupport;
#[derive(Resource, Clone, Debug)]
pub struct FloraWindBuffer(pub Handle<ShaderBuffer>);
impl FloraWindBuffer {
fn new(buffers: &mut Assets<ShaderBuffer>, wind: &FloraWind) -> Self {
Self(buffers.add(ShaderBuffer::from(wind_vectors(wind).map(|v| v.to_array()).to_vec())))
}
}
#[derive(Resource)]
pub(crate) struct FloraSupportPending;
pub(crate) fn flora_decided(pending: Option<Res<FloraSupportPending>>) -> bool {
pending.is_none()
}
pub(crate) fn detect_flora_support(
mut commands: Commands,
device: Option<Res<RenderDevice>>,
pending: Option<Res<FloraSupportPending>>,
) {
let (Some(device), Some(_)) = (device, pending) else { return };
commands.remove_resource::<FloraSupportPending>();
if bevy::render::storage_buffers_are_unsupported(&device.limits()) {
warn!(
"bevy_flora: this device has no storage buffers in the vertex stage (WebGL2?), so trees \
are drawn without leaf shading and wind, and pools without impostors"
);
commands.remove_resource::<ImpostorSupport>();
} else {
commands.insert_resource(FloraMaterialSupport);
}
}
pub(crate) fn sync_wind(
wind: Res<FloraWind>,
buffer: Res<FloraWindBuffer>,
mut buffers: ResMut<Assets<ShaderBuffer>>,
) {
let packed = wind_vectors(&wind).map(|v| v.to_array());
if buffers.get(&buffer.0).and_then(|buffer| buffer.cast_slice::<[f32; 4]>()) == Some(&packed[..]) {
return;
}
let Some(mut shader_buffer) = buffers.get_mut(&buffer.0) else { return };
match shader_buffer.cast_slice_mut::<[f32; 4]>() {
Some(slice) if slice.len() == packed.len() => slice.copy_from_slice(&packed),
_ => *shader_buffer = ShaderBuffer::from(packed.to_vec()),
}
}
pub(crate) fn build(app: &mut App) {
app.init_resource::<FloraWind>().init_resource::<SharedFlora>().register_type::<FloraWind>().register_type::<GustFront>();
if app.get_sub_app(RenderApp).is_some() && app.is_plugin_added::<PbrPlugin>() {
add_shaders(app);
app.add_plugins(MaterialPlugin::<FloraMaterial>::default()).insert_resource(FloraSupportPending);
} else {
add_asset_types(app);
}
add_wind_buffer(app);
}
pub(crate) fn finish(app: &mut App) {
add_asset_types(app);
add_wind_buffer(app);
if app.get_sub_app(RenderApp).is_some() && !app.world().contains_resource::<FloraSupportPending>() {
warn!(
"bevy_flora: FloraPlugin was added before the renderer's plugins, so trees are drawn \
without leaf shading and wind; add it after DefaultPlugins"
);
}
}
fn add_asset_types(app: &mut App) {
if app.world().contains_resource::<AssetServer>() {
if !app.world().contains_resource::<Assets<FloraMaterial>>() {
app.init_asset::<FloraMaterial>();
}
if !app.world().contains_resource::<Assets<ShaderBuffer>>() {
app.init_asset::<ShaderBuffer>();
}
}
}
fn add_wind_buffer(app: &mut App) {
if app.world().contains_resource::<FloraWindBuffer>() || !app.world().contains_resource::<Assets<ShaderBuffer>>() {
return;
}
let wind = *app.world().resource::<FloraWind>();
let buffer = FloraWindBuffer::new(&mut app.world_mut().resource_mut::<Assets<ShaderBuffer>>(), &wind);
app.insert_resource(buffer);
}
fn add_shaders(app: &mut App) {
bevy::shader::load_shader_library!(app, "common.wesl");
embedded_asset!(app, "vertex.wesl");
embedded_asset!(app, "prepass.wesl");
embedded_asset!(app, "fragment.wesl");
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use super::*;
use crate::presets::TreePreset;
#[test]
fn veins_are_drawn_only_on_leaf_shaped_cards() {
let veins = |style| {
let mut params = TreePreset::Oak.params();
params.foliage.style = style;
params.foliage.shading.veins = 0.7;
FloraUniform::new(¶ms).leaf.z
};
assert_eq!(veins(LeafStyle::CrossedPlanes), 0.7);
assert_eq!(veins(LeafStyle::SingleQuad), 0.7);
assert_eq!(veins(LeafStyle::NeedleCluster), 0.0);
assert_eq!(veins(LeafStyle::ClusterSphere), 0.0);
}
#[test]
fn the_uniform_holds_the_tree_and_nothing_of_the_weather() {
let uniform = FloraUniform::new(&TreePreset::Willow.params());
assert_eq!(uniform.response, Vec4::new(1.4, 1.6, 0.7, 0.8));
let wood = uniform.wood();
assert_eq!((wood.tree, wood.response), (uniform.tree, uniform.response));
assert_eq!((wood.leaf, wood.tint), (Vec4::ZERO, Vec4::ZERO));
}
#[test]
fn the_wind_is_packed_normalised_and_clamped() {
let gale = FloraWind {
direction: Vec2::new(0.0, -5.0),
strength: 7.0,
speed: -1.0,
flutter: 2.0,
bend: 3.0,
gust_strength: 2.0,
gust_frequency: 0.5,
flutter_frequency: 4.0,
turbulence: -1.0,
gust: None,
};
let [wind, motion, rate, front, front_shape] = wind_vectors(&gale);
assert_eq!(wind, Vec4::new(0.0, -1.0, 7.0, 0.0));
assert_eq!(motion, Vec4::new(3.0, 2.0, 1.0, 0.0));
assert_eq!(rate, Vec4::new(0.5, 4.0, 0.0, 0.0));
assert_eq!((front, front_shape), (Vec4::ZERO, Vec4::ZERO));
assert_ne!(wind_vectors(&FloraWind { strength: 0.0, ..gale }), wind_vectors(&gale));
}
#[test]
fn gust_front_is_packed_normalised_and_off_when_absent() {
let pack = |gust: Option<GustFront>| {
let [.., front, front_shape] = wind_vectors(&FloraWind { gust, ..Default::default() });
(front, front_shape)
};
assert_eq!(pack(None), (Vec4::ZERO, Vec4::ZERO));
let front = GustFront {
origin: Vec2::new(4.0, -2.0),
direction: Vec2::new(0.0, 3.0),
speed: 12.0,
width: 0.0,
strength: 2.0,
start: 7.5,
};
assert_eq!(pack(Some(front)), (Vec4::new(4.0, -2.0, 0.0, 1.0), Vec4::new(12.0, 0.01, 2.0, 7.5)));
assert_eq!(pack(Some(GustFront { direction: Vec2::ZERO, ..front })), (Vec4::ZERO, Vec4::ZERO));
}
#[test]
fn launching_a_gust_stamps_the_render_clock() {
let mut time = Time::<()>::default();
time.advance_by(Duration::from_secs_f32(5.0));
let mut wind = FloraWind::default();
wind.launch_gust(GustFront { start: -1.0, ..Default::default() }, &time);
let front = wind.gust.expect("the gust was launched");
assert_eq!(front.start, time.elapsed_secs_wrapped());
assert_eq!(front.start, 5.0);
let [.., front_shape] = wind_vectors(&wind);
assert_eq!(front_shape.w, 5.0);
}
}