use bevy::app::App;
use bevy::asset::{Asset, AssetApp, AssetServer, Assets, Handle, embedded_asset, load_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, MeshPipelineKey, PbrPlugin, StandardMaterial,
};
use bevy::reflect::Reflect;
use bevy::render::RenderApp;
use bevy::render::render_resource::{
AsBindGroup, Face, FragmentState, RenderPipelineDescriptor, ShaderType, SpecializedMeshPipelineError,
};
use bevy::render::renderer::RenderDevice;
use bevy::render::storage::ShaderBuffer;
use bevy::shader::{ShaderDefVal, ShaderRef};
use bevy::time::Time;
use crate::foliage::uses_scan;
use crate::impostor::ImpostorSupport;
use crate::mesh::ATTRIBUTE_LEAF_SWAY;
use crate::params::{LeafStyle, RenderStyle, TreeParams};
use crate::push::{PUSH_SLOT, PUSH_VECTORS, PushSlots};
use crate::see_through::{SEE_THROUGH_SLOT, SEE_THROUGH_VECTORS, SeeThroughCapsule};
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(Clone, Copy, Debug, PartialEq, Reflect)]
pub struct TreeShake {
pub at: Vec2,
pub radius: f32,
pub strength: f32,
pub direction: Vec2,
pub duration: f32,
pub start: f32,
}
impl Default for TreeShake {
fn default() -> Self {
Self { at: Vec2::ZERO, radius: 1.5, strength: 1.0, direction: Vec2::ZERO, duration: 2.5, start: 0.0 }
}
}
#[derive(Resource, Clone, Debug, Default, PartialEq, Reflect)]
#[reflect(Resource)]
pub struct TreeShakes {
shakes: Vec<TreeShake>,
}
impl TreeShakes {
pub const MAX: usize = 8;
pub fn shake(&mut self, mut shake: TreeShake, time: &Time) {
shake.start = time.elapsed_secs_wrapped();
if self.shakes.len() < Self::MAX {
self.shakes.push(shake);
} else if let Some(soonest) =
self.shakes.iter_mut().min_by(|a, b| (a.start + a.duration).total_cmp(&(b.start + b.duration)))
{
*soonest = shake;
}
}
pub fn clear(&mut self) {
self.shakes.clear();
}
pub fn iter(&self) -> impl Iterator<Item = &TreeShake> {
self.shakes.iter()
}
}
#[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,
pub autumn: Vec4,
pub blossom: Vec4,
}
const AUTUMN_RATIO_MAX: f32 = 1000.0;
#[derive(Resource, Clone, Copy, Debug, Default, PartialEq, Reflect)]
#[reflect(Resource)]
pub struct FloraSeason {
pub autumn: f32,
pub leaf_fall: f32,
pub snow: f32,
pub blossom: f32,
}
impl FloraSeason {
pub const SUMMER: Self = Self { autumn: 0.0, leaf_fall: 0.0, snow: 0.0, blossom: 0.0 };
pub const AUTUMN: Self = Self { autumn: 1.0, leaf_fall: 0.0, snow: 0.0, blossom: 0.0 };
pub const WINTER: Self = Self { autumn: 1.0, leaf_fall: 1.0, snow: 0.0, blossom: 0.0 };
pub const SNOW: Self = Self { autumn: 1.0, leaf_fall: 1.0, snow: 1.0, blossom: 0.0 };
pub const BLOSSOM: Self = Self { autumn: 0.0, leaf_fall: 1.0, snow: 0.0, blossom: 1.0 };
pub fn of_year(t: f32) -> Self {
let t = t.rem_euclid(1.0);
let ramp = |a: f32, b: f32| ((t - a) / (b - a)).clamp(0.0, 1.0);
if t < 0.07 {
Self {
autumn: 0.0,
leaf_fall: 1.0 - ramp(0.02, 0.07),
snow: 0.0,
blossom: ramp(0.0, 0.015).min(1.0 - ramp(0.03, 0.07)),
}
} else {
Self {
autumn: ramp(0.55, 0.75),
leaf_fall: ramp(0.7, 0.9),
snow: ramp(0.88, 0.92) * (1.0 - ramp(0.98, 1.0)),
blossom: 0.0,
}
}
}
}
#[derive(Resource, Clone, Copy, Debug, Default, PartialEq, Reflect)]
#[reflect(Resource)]
pub struct FloraWetness(pub f32);
impl FloraWetness {
pub const SOAKED: Self = Self(1.0);
pub fn amount(&self) -> f32 {
if self.0.is_nan() { 0.0 } else { self.0.clamp(0.0, 1.0) }
}
}
pub(crate) fn colour_ratio(to: bevy::color::Color, from: bevy::color::Color) -> Vec4 {
let (to, from) = (to.to_linear(), from.to_linear());
let ratio = |a: f32, b: f32| (a / b.max(1e-4)).clamp(0.0, AUTUMN_RATIO_MAX);
Vec4::new(ratio(to.red, from.red), ratio(to.green, from.green), ratio(to.blue, from.blue), 1.0)
}
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,
autumn: params.foliage.autumn.map_or(Vec4::ZERO, |autumn| colour_ratio(autumn, params.foliage.color)),
blossom: params.foliage.blossom.map_or(Vec4::ZERO, |blossom| colour_ratio(blossom, params.foliage.color)),
}
}
pub(crate) fn wood(self) -> Self {
Self { leaf: Vec4::ZERO, tint: Vec4::ZERO, autumn: Vec4::ZERO, blossom: Vec4::ZERO, ..self }
}
}
pub(crate) const WIND_VECTORS: usize = 7 + 2 * TreeShakes::MAX + SEE_THROUGH_VECTORS + PUSH_VECTORS;
pub(crate) const WETNESS_SLOT: usize = 2;
pub(crate) fn wind_vectors(wind: &FloraWind, season: &FloraSeason, shakes: &TreeShakes) -> [Vec4; WIND_VECTORS] {
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),
};
let mut packed = [Vec4::ZERO; WIND_VECTORS];
packed[..6].copy_from_slice(&[
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,
Vec4::new(
season.autumn.clamp(0.0, 1.0),
season.leaf_fall.clamp(0.0, 1.0),
season.snow.clamp(0.0, 1.0),
season.blossom.clamp(0.0, 1.0),
),
]);
let mut last_end = -1.0f32;
let live = shakes.iter().filter(|s| s.strength > 0.0).take(TreeShakes::MAX);
for (i, shake) in live.enumerate() {
let duration = shake.duration.max(0.01);
let dir = shake.direction.normalize_or_zero();
packed[7 + 2 * i] = Vec4::new(shake.at.x, shake.at.y, shake.radius.max(0.01), shake.strength);
packed[8 + 2 * i] = Vec4::new(dir.x, dir.y, shake.start, duration);
last_end = last_end.max(shake.start + duration);
}
packed[6] = Vec4::new(last_end, 0.0, 0.0, 0.0);
packed
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct FloraKey {
pub leaf: bool,
pub detail: bool,
pub venation: bool,
pub style: RenderStyle,
pub see_through: bool,
pub falling: bool,
pub fire: bool,
pub push: 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>>,
pub style: RenderStyle,
pub see_through: bool,
pub falling: bool,
pub fire: bool,
pub push: bool,
}
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(),
style: extension.style,
see_through: extension.see_through,
falling: extension.falling,
fire: extension.fire,
push: extension.push,
}
}
}
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, key.mesh_key)
}
}
pub(crate) fn specialize_flora(
descriptor: &mut RenderPipelineDescriptor,
layout: &MeshVertexBufferLayoutRef,
key: FloraKey,
mesh_key: MeshPipelineKey,
) -> Result<(), SpecializedMeshPipelineError> {
if key.push {
descriptor.vertex.shader_defs.push("FLORA_PUSH".into());
}
if key.fire {
descriptor.vertex.shader_defs.push("FLORA_FIRE".into());
if let Some(fragment) = descriptor.fragment.as_mut() {
fragment.shader_defs.push("FLORA_FIRE".into());
}
}
if key.see_through {
add_see_through(descriptor, mesh_key);
}
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.style == RenderStyle::Toon
&& let Some(fragment) = descriptor.fragment.as_mut()
{
fragment.shader_defs.push("FLORA_TOON".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);
if key.falling {
crate::falling::specialize_falling(descriptor, layout)?;
}
Ok(())
}
fn add_see_through(descriptor: &mut RenderPipelineDescriptor, mesh_key: MeshPipelineKey) {
let prepass = descriptor.vertex.shader_defs.contains(&ShaderDefVal::from("PREPASS_PIPELINE"));
if !prepass {
if let Some(fragment) = descriptor.fragment.as_mut() {
fragment.shader_defs.push("FLORA_SEE_THROUGH".into());
}
return;
}
if mesh_key.intersects(MeshPipelineKey::UNCLIPPED_DEPTH_ORTHO | MeshPipelineKey::DEFERRED_PREPASS) {
return;
}
let Some(shader) = crate::see_through::prepass_fragment() else { return };
match descriptor.fragment.as_mut() {
Some(fragment) => {
fragment.shader = shader;
fragment.shader_defs.push("FLORA_SEE_THROUGH".into());
}
None => {
let mut shader_defs = descriptor.vertex.shader_defs.clone();
shader_defs.push("FLORA_SEE_THROUGH".into());
descriptor.fragment = Some(FragmentState {
shader,
shader_defs,
entry_point: Some("fragment".into()),
targets: Vec::new(),
..Default::default()
});
}
}
}
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>>,
pub style: RenderStyle,
}
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, style: params.style, 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));
if let Some(blossom) = params.foliage.blossom {
uniform.blossom = colour_ratio(blossom, color);
}
Self { color, uniform, leaf: true, style: params.style, 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(),
style: surface.style,
see_through: false,
falling: false,
fire: false,
push: false,
},
}
}
#[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,
season: &FloraSeason,
shakes: &TreeShakes,
wetness: &FloraWetness,
) -> Self {
let mut packed = wind_vectors(wind, season, shakes).map(|v| v.to_array());
packed[WETNESS_SLOT][2] = wetness.amount();
Self(buffers.add(ShaderBuffer::from(shared_vectors(&packed))))
}
}
#[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);
}
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn sync_wind(
wind: Res<FloraWind>,
season: Res<FloraSeason>,
wetness: Res<FloraWetness>,
shakes: Res<TreeShakes>,
capsule: Res<SeeThroughCapsule>,
push: Res<PushSlots>,
buffer: Res<FloraWindBuffer>,
mut buffers: ResMut<Assets<ShaderBuffer>>,
) {
let mut packed = wind_vectors(&wind, &season, &shakes).map(|v| v.to_array());
packed[WETNESS_SLOT][2] = wetness.amount();
for (i, vector) in capsule.0.iter().enumerate() {
packed[SEE_THROUGH_SLOT + i] = vector.to_array();
}
packed[PUSH_SLOT..PUSH_SLOT + PUSH_VECTORS].copy_from_slice(&push.0);
let current = buffers.get(&buffer.0).and_then(|buffer| buffer.cast_slice::<[f32; 4]>());
if current.and_then(|slice| slice.get(..WIND_VECTORS)) == 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() > WIND_VECTORS => slice[..WIND_VECTORS].copy_from_slice(&packed),
_ => *shader_buffer = ShaderBuffer::from(shared_vectors(&packed)),
}
}
fn shared_vectors(packed: &[[f32; 4]; WIND_VECTORS]) -> Vec<[f32; 4]> {
let mut all = packed.to_vec();
all.push([0.0; 4]);
all
}
pub(crate) fn expire_shakes(mut shakes: ResMut<TreeShakes>, time: Res<Time>) {
let now = time.elapsed_secs_wrapped();
let under_way = |shake: &TreeShake| shake.start <= now && now <= shake.start + shake.duration.max(0.01);
if !shakes.shakes.iter().all(under_way) {
shakes.shakes.retain(under_way);
}
}
pub(crate) fn build(app: &mut App) {
app.init_resource::<FloraWind>()
.init_resource::<FloraSeason>()
.init_resource::<FloraWetness>()
.init_resource::<TreeShakes>()
.init_resource::<SeeThroughCapsule>()
.init_resource::<SharedFlora>()
.init_resource::<FloraTextures>()
.register_type::<FloraWind>()
.register_type::<FloraSeason>()
.register_type::<FloraWetness>()
.register_type::<TreeShakes>()
.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 season = *app.world().resource::<FloraSeason>();
let shakes = app.world().resource::<TreeShakes>().clone();
let wetness = *app.world().resource::<FloraWetness>();
let buffer = FloraWindBuffer::new(
&mut app.world_mut().resource_mut::<Assets<ShaderBuffer>>(),
&wind,
&season,
&shakes,
&wetness,
);
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");
embedded_asset!(app, "see_through_prepass.wesl");
crate::see_through::set_prepass_fragment(load_embedded_asset!(app, "see_through_prepass.wesl"));
bevy::shader::load_shader_library!(app, "falling_common.wesl");
embedded_asset!(app, "falling.wesl");
embedded_asset!(app, "falling_prepass.wesl");
crate::falling::set_shaders(
load_embedded_asset!(app, "falling.wesl"),
load_embedded_asset!(app, "falling_prepass.wesl"),
);
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use bevy::math::Vec3;
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 a_turned_leaf_takes_the_autumn_colour_and_evergreens_and_wood_never_turn() {
let maple = TreePreset::Maple.params();
let uniform = FloraUniform::new(&maple);
assert_eq!(uniform.autumn.w, 1.0, "a deciduous preset turns");
let (leaf, autumn) = (maple.foliage.color.to_linear(), maple.foliage.autumn.unwrap().to_linear());
let turned = Vec3::new(leaf.red, leaf.green, leaf.blue) * uniform.autumn.truncate();
assert!(turned.abs_diff_eq(Vec3::new(autumn.red, autumn.green, autumn.blue), 1e-5), "{turned}");
assert_eq!(uniform.wood().autumn, Vec4::ZERO, "wood never turns");
assert_eq!(FloraUniform::new(&TreePreset::Pine.params()).autumn, Vec4::ZERO, "a pine stays green");
let mut black = maple.clone();
black.foliage.color = Color::BLACK;
assert!(FloraUniform::new(&black).autumn.truncate().cmple(Vec3::splat(AUTUMN_RATIO_MAX)).all());
}
#[test]
fn over_a_year_leaves_turn_before_they_fall_snow_lies_on_bare_trees_and_leaves_come_back_green() {
assert_eq!(FloraSeason::of_year(0.3), FloraSeason::SUMMER);
assert_eq!(FloraSeason::of_year(0.95), FloraSeason::SNOW);
assert_eq!(FloraSeason::of_year(1.3), FloraSeason::of_year(0.3), "the year wraps");
assert_eq!(FloraSeason::of_year(-0.05), FloraSeason::of_year(0.95), "the year wraps");
let steps = 2000;
let mut last = FloraSeason::of_year(0.0);
let mut snowed = false;
let mut blossomed = 0.0f32;
for i in 1..=steps {
let now = FloraSeason::of_year(i as f32 / steps as f32);
if now.autumn < last.autumn {
assert_eq!(last.leaf_fall, 1.0, "colour reset on a tree in leaf at step {i}");
}
if now.leaf_fall < last.leaf_fall {
assert_eq!(now.autumn, 0.0, "leaves came back turned at step {i}");
assert_eq!(now.snow, 0.0, "leaves came back under snow at step {i}");
}
if now.leaf_fall > 0.0 && now.leaf_fall > last.leaf_fall {
assert!(now.autumn >= 0.75, "leaves fell green at step {i}: {now:?}");
}
if now.snow > 0.0 {
assert!(now.leaf_fall >= 0.9, "snow on a tree in leaf at step {i}: {now:?}");
snowed = true;
}
assert!((now.snow - last.snow).abs() < 0.05, "snow jumped at step {i}");
if now.blossom > 0.0 {
assert!(now.snow == 0.0 && now.autumn == 0.0, "blossom in snow or autumn at step {i}: {now:?}");
blossomed = blossomed.max(now.blossom);
}
if now.blossom == 1.0 {
assert!(now.leaf_fall > 0.5, "full blossom on a tree in leaf at step {i}: {now:?}");
}
assert!((now.blossom - last.blossom).abs() < 0.05, "blossom jumped at step {i}");
last = now;
}
assert!(snowed, "a year without snow");
assert_eq!(blossomed, 1.0, "a year without full blossom");
}
#[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, season, ..] =
wind_vectors(&gale, &FloraSeason { autumn: 2.0, leaf_fall: -1.0, snow: 3.0, blossom: 4.0 }, &TreeShakes::default());
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_eq!(season, Vec4::new(1.0, 0.0, 1.0, 1.0), "the season is clamped to 0..1");
let (calm, still) = (FloraSeason::default(), TreeShakes::default());
assert_ne!(
wind_vectors(&FloraWind { strength: 0.0, ..gale }, &calm, &still),
wind_vectors(&gale, &calm, &still)
);
}
#[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() }, &FloraSeason::default(), &TreeShakes::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 every_render_style_has_its_own_material_and_pipeline_key() {
use crate::shading::SharedFlora;
let mut shared = SharedFlora::default();
let mut materials = Assets::<FloraMaterial>::default();
let wind = FloraWindBuffer(Handle::default());
let mut params = TreePreset::Oak.params();
let mut resolved = Vec::new();
for style in [RenderStyle::Natural, RenderStyle::Painterly, RenderStyle::Toon] {
params.style = style;
let (wood, leaves) = surfaces(¶ms, true);
for surface in [&wood, &leaves] {
assert_eq!(FloraKey::from(&flora_material(surface, &wind).extension).style, style);
resolved.push(shared.resolve(&mut materials, surface, &wind));
}
}
let distinct: std::collections::HashSet<_> = resolved.iter().collect();
assert_eq!(distinct.len(), resolved.len(), "a style shares its material with another");
assert_eq!([RenderStyle::Natural, RenderStyle::Painterly, RenderStyle::Toon].map(|s| s as u32), [0, 1, 2]);
}
#[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, &FloraSeason::default(), &TreeShakes::default());
assert_eq!(front_shape.w, 5.0);
}
#[test]
fn a_shake_is_packed_and_a_new_one_always_gets_a_slot() {
let mut time = Time::<()>::default();
time.advance_by(Duration::from_secs_f32(10.0));
let calm = (FloraWind::default(), FloraSeason::default());
let pack = |shakes: &TreeShakes| wind_vectors(&calm.0, &calm.1, shakes);
let none = pack(&TreeShakes::default());
assert_eq!(none[6].x, -1.0);
assert!(none[7..].iter().all(|v| *v == Vec4::ZERO));
let mut shakes = TreeShakes::default();
let hit = TreeShake { at: Vec2::new(3.0, -4.0), radius: 0.0, strength: 2.0, direction: Vec2::new(0.0, 5.0), duration: 3.0, start: -1.0 };
shakes.shake(hit, &time);
assert_eq!(shakes.iter().next().map(|s| s.start), Some(10.0), "stamped with the render clock");
let packed = pack(&shakes);
assert_eq!(packed[7], Vec4::new(3.0, -4.0, 0.01, 2.0), "radius kept above 0");
assert_eq!(packed[8], Vec4::new(0.0, 1.0, 10.0, 3.0), "direction normalised");
assert_eq!(packed[6].x, 13.0, "the header says when the last shake ends");
let mut idle = TreeShakes::default();
idle.shake(TreeShake { strength: 0.0, ..hit }, &time);
assert_eq!(pack(&idle)[6].x, -1.0);
let mut full = TreeShakes::default();
for i in 0..TreeShakes::MAX {
full.shake(TreeShake { duration: 10.0 + i as f32, ..TreeShake::default() }, &time);
}
let newest = TreeShake { at: Vec2::new(99.0, 0.0), duration: 1.0, ..TreeShake::default() };
time.advance_by(Duration::from_secs_f32(1.0));
full.shake(newest, &time);
let durations: Vec<f32> = full.iter().map(|s| s.duration).collect();
assert_eq!(full.iter().count(), TreeShakes::MAX);
assert!(!durations.contains(&10.0), "the shortest-lived shake should have made room: {durations:?}");
assert!(full.iter().any(|s| s.at.x == 99.0 && s.start == 11.0));
assert_eq!(pack(&full)[6].x, 10.0 + 17.0, "the longest still ends last");
}
}