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::image::Image;
use bevy::math::{Vec2, Vec4};
use bevy::mesh::{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::foliage::uses_scan;
use crate::impostor::ImpostorSupport;
use crate::mesh::ATTRIBUTE_LEAF_SWAY;
use crate::params::{LeafStyle, TreeParams};
mod share;
pub use share::SharedFlora;
mod textures;
pub use textures::{BarkTexture, FloraTextures, LeafTexture, TextureLibrary};
#[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,
pub bark: Vec4,
pub bark_mean: 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),
bark: Vec4::ZERO,
bark_mean: Vec4::ZERO,
}
}
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,
pub detail: bool,
pub venation: 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,
#[texture(103)]
#[sampler(105)]
pub detail_color: Option<Handle<Image>>,
#[texture(104)]
pub detail_normal: Option<Handle<Image>>,
#[texture(106)]
#[sampler(107)]
pub venation: Option<Handle<Image>>,
}
impl From<&FloraExtension> for FloraKey {
fn from(extension: &FloraExtension) -> Self {
Self {
leaf: extension.leaf,
detail: extension.detail_color.is_some() && extension.detail_normal.is_some(),
venation: extension.venation.is_some(),
}
}
}
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)
}
}
pub(crate) fn specialize_flora(
descriptor: &mut RenderPipelineDescriptor,
layout: &MeshVertexBufferLayoutRef,
key: FloraKey,
) -> Result<(), SpecializedMeshPipelineError> {
if key.venation
&& layout.0.contains(Mesh::ATTRIBUTE_UV_0.id)
&& let Some(fragment) = descriptor.fragment.as_mut()
{
fragment.shader_defs.push("FLORA_VENATION".into());
}
if key.detail
&& layout.0.contains(Mesh::ATTRIBUTE_UV_0.id)
&& layout.0.contains(Mesh::ATTRIBUTE_UV_1.id)
&& let Some(fragment) = descriptor.fragment.as_mut()
{
fragment.shader_defs.push("FLORA_DETAIL".into());
}
if !key.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()
}
}
#[derive(Clone, Debug)]
pub(crate) struct Surface {
pub color: Color,
pub uniform: FloraUniform,
pub leaf: bool,
pub bark: Option<BarkTexture>,
pub leaf_scan: Option<LeafTexture>,
pub venation: Option<Handle<Image>>,
}
impl Surface {
pub(crate) fn wood(params: &TreeParams, bark: Option<BarkTexture>) -> Self {
let mut uniform = FloraUniform::new(params).wood();
if let Some(bark) = &bark {
uniform.bark = Vec4::new(params.bark.smooth_below, params.bark.fissured_above, 0.0, 0.0);
uniform.bark_mean = Vec4::new(bark.mean.red, bark.mean.green, bark.mean.blue, 0.0);
}
let color = bark.as_ref().map_or(params.trunk_color, |bark| bark.base_color(params.trunk_color));
Self { color, uniform, leaf: false, bark, leaf_scan: None, venation: None }
}
pub(crate) fn leaves(
params: &TreeParams,
leaf_scan: Option<LeafTexture>,
venation: Option<Handle<Image>>,
) -> Self {
let mut uniform = FloraUniform::new(params);
if uses_scan(¶ms.foliage) {
uniform.leaf.z = 0.0;
}
let color = leaf_scan.as_ref().map_or(params.foliage.color, |scan| scan.base_color(params.foliage.color));
Self { color, uniform, leaf: true, bark: None, leaf_scan, venation }
}
pub(crate) fn detail(&self) -> (Option<Handle<Image>>, Option<Handle<Image>>) {
if let Some(bark) = &self.bark {
(Some(bark.color.clone()), Some(bark.normal.clone()))
} else if let Some(scan) = &self.leaf_scan {
(Some(scan.color.clone()), Some(scan.normal.clone()))
} else {
(None, None)
}
}
}
pub(crate) fn flora_material(surface: &Surface, wind: &FloraWindBuffer) -> FloraMaterial {
let (detail_color, detail_normal) = surface.detail();
FloraMaterial {
base: flora_base(surface.color),
extension: FloraExtension {
flora: surface.uniform,
wind: wind.0.clone(),
leaf: surface.leaf,
detail_color,
detail_normal,
venation: surface.venation.clone(),
},
}
}
#[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>()
.init_resource::<FloraTextures>()
.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));
}
fn surfaces(params: &TreeParams, images: bool) -> (Surface, Surface) {
use bevy::ecs::system::SystemState;
let mut world = World::new();
world.init_resource::<FloraTextures>();
if images {
world.init_resource::<Assets<Image>>();
}
let mut state = SystemState::<TextureLibrary>::new(&mut world);
let mut library = state.get_mut(&mut world).expect("the library's resources exist");
(library.wood(params), library.leaves(params))
}
#[test]
fn leaves_wear_a_scan_or_the_vein_image_never_both_and_veins_only_where_a_card_is_a_leaf() {
let oak = TreePreset::Oak.params();
let (_, leaves) = surfaces(&oak, true);
if cfg!(feature = "textures") {
assert!(leaves.leaf_scan.is_some() && leaves.venation.is_none());
assert_eq!(leaves.uniform.leaf.z, 0.0);
assert_ne!(leaves.color, oak.foliage.color, "the scan's mean is divided out of the colour");
assert!(leaves.detail().0.is_some() && leaves.detail().1.is_some());
} else {
assert!(leaves.leaf_scan.is_none() && leaves.venation.is_some());
assert!(leaves.uniform.leaf.z > 0.0);
}
let dragon = TreePreset::DragonTree.params();
assert_eq!(dragon.foliage.scan, None);
let (_, a) = surfaces(&dragon, true);
assert!(a.leaf_scan.is_none() && a.venation.is_some() && a.uniform.leaf.z > 0.0);
assert_eq!(a.color, dragon.foliage.color);
let (_, needles) = surfaces(&TreePreset::Pine.params(), true);
assert!(needles.venation.is_none() && needles.leaf_scan.is_none() && needles.uniform.leaf.z == 0.0);
let mut plain = dragon.clone();
plain.foliage.shading.veins = 0.0;
assert!(surfaces(&plain, true).1.venation.is_none());
let (wood, leaves) = surfaces(&oak, false);
assert!(wood.bark.is_none() && leaves.leaf_scan.is_none() && leaves.venation.is_none());
assert_eq!(wood.color, oak.trunk_color);
}
#[test]
fn trees_of_other_seeds_share_their_materials_and_other_scans_do_not() {
use crate::shading::SharedFlora;
let mut shared = SharedFlora::default();
let mut materials = Assets::<FloraMaterial>::default();
let wind = FloraWindBuffer(Handle::default());
let mut resolve = |surface: &Surface| shared.resolve(&mut materials, surface, &wind);
let (mut oak, other) = (TreePreset::Oak.params(), TreePreset::Maple.params());
let (wood, leaves) = surfaces(&oak, true);
oak.seed += 1;
let (wood_again, leaves_again) = surfaces(&oak, true);
assert_eq!(resolve(&wood), resolve(&wood_again));
assert_eq!(resolve(&leaves), resolve(&leaves_again));
let (other_wood, other_leaves) = surfaces(&other, true);
assert_ne!(resolve(&wood), resolve(&other_wood));
assert_ne!(resolve(&leaves), resolve(&other_leaves));
}
#[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);
}
}