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::shader::ShaderRef;
use bevy::time::Time;
use crate::gpu::GrowthMaterial;
use crate::impostor::ImpostorMaterial;
use crate::mesh::ATTRIBUTE_LEAF_SWAY;
use crate::params::{LeafStyle, TreeParams};
#[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 wind: Vec4,
pub motion: Vec4,
pub rate: Vec4,
pub front: Vec4,
pub front_shape: Vec4,
pub tree: Vec4,
pub response: Vec4,
pub leaf: Vec4,
pub tint: Vec4,
}
impl FloraUniform {
pub(crate) fn new(params: &TreeParams, wind: &FloraWind) -> 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();
let [wind, motion, rate, front, front_shape] = global_wind(wind);
Self {
wind,
motion,
rate,
front,
front_shape,
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_differs(&self, wind: &FloraWind) -> bool {
[self.wind, self.motion, self.rate, self.front, self.front_shape] != global_wind(wind)
}
pub(crate) fn set_wind(&mut self, wind: &FloraWind) {
[self.wind, self.motion, self.rate, self.front, self.front_shape] = global_wind(wind);
}
}
fn global_wind(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,
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) -> FloraMaterial {
FloraMaterial { base: flora_base(color), extension: FloraExtension { flora, leaf } }
}
pub(crate) fn sync_flora(
materials: &mut Assets<FloraMaterial>,
handle: &Handle<FloraMaterial>,
color: Color,
flora: FloraUniform,
) {
if let Some(mut material) = materials.get_mut(handle)
&& (material.base.base_color != color || material.extension.flora != flora)
{
material.base.base_color = color;
material.extension.flora = flora;
}
}
#[derive(Resource, Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct FloraMaterialSupport;
pub(crate) trait HasFlora: Asset {
fn flora(&self) -> &FloraUniform;
fn flora_mut(&mut self) -> &mut FloraUniform;
}
impl HasFlora for FloraMaterial {
fn flora(&self) -> &FloraUniform {
&self.extension.flora
}
fn flora_mut(&mut self) -> &mut FloraUniform {
&mut self.extension.flora
}
}
impl HasFlora for GrowthMaterial {
fn flora(&self) -> &FloraUniform {
&self.extension.flora
}
fn flora_mut(&mut self) -> &mut FloraUniform {
&mut self.extension.flora
}
}
impl HasFlora for ImpostorMaterial {
fn flora(&self) -> &FloraUniform {
&self.flora
}
fn flora_mut(&mut self) -> &mut FloraUniform {
&mut self.flora
}
}
fn rewrite_wind<A: HasFlora>(assets: Option<ResMut<Assets<A>>>, wind: &FloraWind) {
let Some(mut assets) = assets else { return };
let stale: Vec<_> = assets
.iter()
.filter(|(_, material)| material.flora().wind_differs(wind))
.map(|(id, _)| id)
.collect();
for id in stale {
if let Some(mut material) = assets.get_mut(id) {
material.flora_mut().set_wind(wind);
}
}
}
pub(crate) fn sync_wind(
wind: Res<FloraWind>,
flora: Option<ResMut<Assets<FloraMaterial>>>,
growth: Option<ResMut<Assets<GrowthMaterial>>>,
impostors: Option<ResMut<Assets<ImpostorMaterial>>>,
) {
rewrite_wind(flora, &wind);
rewrite_wind(growth, &wind);
rewrite_wind(impostors, &wind);
}
pub(crate) fn build(app: &mut App) {
app.init_resource::<FloraWind>().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(FloraMaterialSupport);
} else {
add_asset_types(app);
}
}
pub(crate) fn finish(app: &mut App) {
add_asset_types(app);
if app.get_sub_app(RenderApp).is_some() && !app.world().contains_resource::<FloraMaterialSupport>() {
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>()
&& !app.world().contains_resource::<Assets<FloraMaterial>>()
{
app.init_asset::<FloraMaterial>();
}
}
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 wind = FloraWind::default();
let veins = |style| {
let mut params = TreePreset::Oak.params();
params.foliage.style = style;
params.foliage.shading.veins = 0.7;
FloraUniform::new(¶ms, &wind).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_wind_is_rewritten_without_touching_the_tree() {
let params = TreePreset::Willow.params();
let calm = FloraUniform::new(¶ms, &FloraWind { strength: 0.0, ..Default::default() });
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,
};
assert!(calm.wind_differs(&gale));
let mut uniform = calm;
uniform.set_wind(&gale);
assert!(!uniform.wind_differs(&gale));
assert_eq!(uniform.wind, Vec4::new(0.0, -1.0, 7.0, 0.0));
assert_eq!(uniform.motion, Vec4::new(3.0, 2.0, 1.0, 0.0));
assert_eq!(uniform.rate, Vec4::new(0.5, 4.0, 0.0, 0.0));
assert_eq!(
(uniform.tree, uniform.response, uniform.leaf, uniform.tint),
(calm.tree, calm.response, calm.leaf, calm.tint)
);
assert_eq!(uniform.response, Vec4::new(1.4, 1.6, 0.7, 0.8));
assert_eq!(uniform, FloraUniform::new(¶ms, &gale));
}
#[test]
fn gust_front_is_packed_normalised_and_off_when_absent() {
let params = TreeParams::default();
let pack = |gust: Option<GustFront>| FloraUniform::new(¶ms, &FloraWind { gust, ..Default::default() });
let none = pack(None);
assert_eq!((none.front, none.front_shape), (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,
};
let on = pack(Some(front));
assert_eq!(on.front, Vec4::new(4.0, -2.0, 0.0, 1.0));
assert_eq!(on.front_shape, Vec4::new(12.0, 0.01, 2.0, 7.5));
let still = pack(Some(GustFront { direction: Vec2::ZERO, ..front }));
assert_eq!((still.front, still.front_shape), (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);
}
}