mod model;
#[cfg(test)]
mod mirror;
use bevy::asset::{AssetPath, RenderAssetUsages, embedded_asset, uuid_handle};
use bevy::pbr::{ExtendedMaterial, MaterialExtension, MaterialPlugin};
use bevy::prelude::*;
use bevy::render::render_resource::AsBindGroup;
use bevy::render::storage::ShaderBuffer;
use bevy::shader::ShaderRef;
use crate::climate::{SurfaceWeathering, Weather};
use crate::clouds::CloudShadows;
use crate::plugin::{ElementsSystems, ElementsUnits, ensure_gpu_core};
use crate::trails::TrailState;
use crate::trails::model::MARCH_BOUND_M;
pub const SNOW_ALBEDO: LinearRgba = LinearRgba::new(0.80, 0.83, 0.88, 1.0);
pub const SNOW_ROUGHNESS: f32 = 0.9;
pub const ELEMENTS_GLOBALS_BUFFER: Handle<ShaderBuffer> =
uuid_handle!("6b0e2d51-7c1f-4f0d-8a53-2e9a41c7d0b8");
const GLOBALS_VEC4S: usize = 8;
#[derive(Resource, Clone, Copy, Debug, PartialEq, Reflect)]
pub struct ElementsGlobals {
pub wetness: f32,
pub snow_cover: f32,
pub rain_intensity: f32,
pub mean_transmittance: f32,
pub shadow_origin: Vec2,
pub shadow_extent: f32,
pub shadow_enabled: bool,
pub ripple_phase: [f32; 4],
pub wind: Vec2,
pub streak_phase: f32,
pub light_direction: Vec3,
pub units_per_metre: f32,
pub snow_depth_m: f32,
pub trail_texel_m: f32,
pub trail_texels: u32,
pub trail_centre: Vec2,
pub trail_direct_share: f32,
}
impl Default for ElementsGlobals {
fn default() -> Self {
Self {
wetness: 0.0,
snow_cover: 0.0,
rain_intensity: 0.0,
mean_transmittance: 1.0,
shadow_origin: Vec2::ZERO,
shadow_extent: 1.0,
shadow_enabled: false,
ripple_phase: [0.0; 4],
wind: Vec2::ZERO,
streak_phase: 0.0,
light_direction: Vec3::Y,
units_per_metre: 1.0,
snow_depth_m: 0.0,
trail_texel_m: 0.0,
trail_texels: 0,
trail_centre: Vec2::ZERO,
trail_direct_share: 0.0,
}
}
}
impl ElementsGlobals {
pub(crate) fn gather(
weathering: Option<&SurfaceWeathering>,
weather: &Weather,
shadows: Option<&CloudShadows>,
trails: Option<&TrailState>,
seconds: f64,
units: f32,
) -> Self {
let (shadow_origin, shadow_extent, shadow_enabled, mean_transmittance, light) =
match shadows {
Some(s) => (
s.origin,
s.extent,
true,
s.mean_transmittance,
s.light_direction,
),
None => (Vec2::ZERO, 1.0, false, 1.0, Vec3::Y),
};
Self {
wetness: weathering.map_or(0.0, |w| w.wetness),
snow_cover: weathering.map_or(0.0, |w| w.snow_cover),
rain_intensity: model::rain_intensity(weather),
mean_transmittance,
shadow_origin,
shadow_extent,
shadow_enabled,
ripple_phase: model::ripple_phases(seconds),
wind: weather.wind_direction * weather.wind_speed_ms,
streak_phase: model::streak_phase(seconds),
light_direction: light,
units_per_metre: units,
snow_depth_m: weathering.map_or(0.0, |w| w.snow_depth_m),
trail_texel_m: trails.map_or(0.0, |t| t.texel_m),
trail_texels: trails.map_or(0, |t| t.texels),
trail_centre: trails.map_or(Vec2::ZERO, |t| t.centre),
trail_direct_share: trails.map_or(0.0, |t| t.direct_share),
}
}
pub(crate) fn pack(&self) -> [[f32; 4]; GLOBALS_VEC4S] {
[
[
self.wetness,
self.snow_cover,
self.rain_intensity,
self.mean_transmittance,
],
[
self.shadow_origin.x,
self.shadow_origin.y,
self.shadow_extent,
f32::from(u8::from(self.shadow_enabled)),
],
self.ripple_phase,
[self.wind.x, self.wind.y, self.streak_phase, 0.0],
[
self.light_direction.x,
self.light_direction.y,
self.light_direction.z,
0.0,
],
[self.units_per_metre, 0.0, 0.0, 0.0],
[
self.trail_texel_m,
self.trail_texels as f32,
f32::from(u8::from(self.trail_texels > 0)),
MARCH_BOUND_M,
],
[
self.trail_direct_share,
self.trail_centre.x,
self.trail_centre.y,
self.snow_depth_m,
],
]
}
}
fn globals_buffer(globals: &ElementsGlobals) -> ShaderBuffer {
let mut buffer = ShaderBuffer::new(globals.pack().to_vec(), RenderAssetUsages::default());
buffer.label = "elements_globals".into();
buffer
}
#[derive(Asset, AsBindGroup, Reflect, Debug, Clone)]
pub struct WeatheredExtension {
#[uniform(100)]
pub exposure: f32,
#[uniform(100)]
trails: f32,
#[uniform(100)]
grain_m: f32,
#[uniform(100)]
pub(crate) look: Vec4,
#[storage(101, read_only, visibility(fragment))]
pub(crate) globals: Handle<ShaderBuffer>,
#[texture(102)]
#[sampler(103)]
pub(crate) cloud_shadow: Option<Handle<Image>>,
#[texture(104)]
#[sampler(105)]
pub(crate) trail: Option<Handle<Image>>,
}
impl Default for WeatheredExtension {
fn default() -> Self {
Self::with_exposure(1.0)
}
}
impl WeatheredExtension {
pub fn with_exposure(exposure: f32) -> Self {
Self {
exposure,
trails: 0.0,
grain_m: 0.0,
look: Vec4::ONE,
globals: ELEMENTS_GLOBALS_BUFFER,
cloud_shadow: None,
trail: None,
}
}
pub fn with_trails(mut self) -> Self {
self.trails = 1.0;
self
}
pub fn draws_trails(&self) -> bool {
self.trails > 0.5
}
pub fn with_grain(mut self, feature_m: f32) -> Self {
self.grain_m = feature_m.max(0.0);
self
}
pub fn grain_m(&self) -> f32 {
self.grain_m
}
}
const SHADER: &str = "embedded://bevy_elements/weathered/weathered.wesl";
impl MaterialExtension for WeatheredExtension {
fn fragment_shader() -> ShaderRef {
ShaderRef::Path(AssetPath::parse(SHADER))
}
fn deferred_fragment_shader() -> ShaderRef {
ShaderRef::Path(AssetPath::parse(SHADER))
}
}
pub type WeatheredMaterial = ExtendedMaterial<StandardMaterial, WeatheredExtension>;
pub struct WeatheredPlugin;
impl Plugin for WeatheredPlugin {
fn build(&self, app: &mut App) {
ensure_gpu_core(app);
embedded_asset!(app, "weathered.wesl");
app.add_plugins(MaterialPlugin::<WeatheredMaterial>::default())
.init_resource::<ElementsGlobals>()
.add_systems(
Update,
(update_globals, update_materials)
.chain()
.after(ElementsSystems::Simulation),
);
}
}
#[allow(clippy::too_many_arguments)]
fn update_globals(
weathering: Option<Res<SurfaceWeathering>>,
weather: Res<Weather>,
shadows: Option<Res<CloudShadows>>,
trails: Option<Res<TrailState>>,
time: Res<Time<Virtual>>,
units: Res<ElementsUnits>,
mut globals: ResMut<ElementsGlobals>,
buffers: Option<ResMut<Assets<ShaderBuffer>>>,
) {
let next = ElementsGlobals::gather(
weathering.as_deref(),
&weather,
shadows.as_deref(),
trails.as_deref(),
time.elapsed_secs_f64(),
units.per_metre(),
);
let Some(mut buffers) = buffers else {
globals.set_if_neq(next);
return;
};
if (globals.set_if_neq(next) || !buffers.contains(ELEMENTS_GLOBALS_BUFFER.id()))
&& let Err(error) = buffers.insert(ELEMENTS_GLOBALS_BUFFER.id(), globals_buffer(&next))
{
error!("could not write the weathered materials' globals buffer: {error}");
}
}
fn update_materials(
shadows: Option<Res<CloudShadows>>,
trails: Option<Res<TrailState>>,
mut materials: ResMut<Assets<WeatheredMaterial>>,
) {
let map = shadows.as_ref().map(|s| s.map.clone());
let field = trails.as_ref().map(|t| t.field.clone());
let wanted = |e: &WeatheredExtension| field.clone().filter(|_| e.draws_trails());
let stale: Vec<_> = materials
.iter()
.filter(|(_, m)| {
m.extension.cloud_shadow != map || m.extension.trail != wanted(&m.extension)
})
.map(|(id, _)| id)
.collect();
for id in stale {
if let Some(mut material) = materials.get_mut(id) {
material.extension.cloud_shadow = map.clone();
material.extension.trail = wanted(&material.extension);
}
}
}
#[cfg(test)]
mod tests;