use bevy::asset::{AssetPath, RenderAssetUsages, uuid_handle};
use bevy::pbr::{ExtendedMaterial, MaterialExtension};
use bevy::prelude::*;
use bevy::render::render_resource::{AsBindGroup, ShaderType};
use bevy::render::storage::ShaderBuffer;
use bevy::shader::ShaderRef;
use super::lod::{LodConfig, MAX_LEVELS};
use super::{TerrainParamsGpu, TerrainState};
use crate::clouds::CloudShadows;
use crate::cover::{CoverState, WetnessSettings};
use crate::weathered::ElementsGlobals;
pub(crate) const NO_TERRAIN: Handle<ShaderBuffer> =
uuid_handle!("2f3c9a40-6a44-4b3a-9a67-0d1b5e7c4a11");
pub(crate) fn insert_placeholder(buffers: &mut Assets<ShaderBuffer>) {
let mut buffer = ShaderBuffer::new(vec![[0f32; 4]], RenderAssetUsages::RENDER_WORLD);
buffer.label = "elements_terrain_none".into();
if let Err(error) = buffers.insert(NO_TERRAIN.id(), buffer) {
error!("could not create the terrain placeholder buffer: {error}");
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, ShaderType, Reflect)]
#[reflect(opaque)]
pub struct TerrainMaterialParams {
pub(crate) terrain: TerrainParamsGpu,
pub(crate) lod: [Vec4; MAX_LEVELS],
pub(crate) top: Vec4,
pub(crate) shadow: Vec4,
pub(crate) light: Vec4,
pub(crate) absorption: Vec4,
pub(crate) cover: Vec4,
pub(crate) ripple: Vec4,
}
#[derive(Asset, AsBindGroup, Reflect, Debug, Clone)]
pub struct TerrainExtension {
#[uniform(100)]
pub(crate) params: TerrainMaterialParams,
#[storage(101, read_only)]
pub(crate) cells: Handle<ShaderBuffer>,
#[storage(102, read_only)]
pub(crate) cover: Handle<ShaderBuffer>,
#[texture(103)]
#[sampler(104)]
pub(crate) cloud_shadow: Option<Handle<Image>>,
}
const RENDER_SHADER: &str = "embedded://bevy_elements/terrain/render.wesl";
const GROUND_SHADER: &str = "embedded://bevy_elements/terrain/ground.wesl";
impl MaterialExtension for TerrainExtension {
fn vertex_shader() -> ShaderRef {
ShaderRef::Path(AssetPath::parse(RENDER_SHADER))
}
fn fragment_shader() -> ShaderRef {
ShaderRef::Path(AssetPath::parse(RENDER_SHADER))
}
fn prepass_vertex_shader() -> ShaderRef {
ShaderRef::Path(AssetPath::parse(RENDER_SHADER))
}
fn deferred_vertex_shader() -> ShaderRef {
ShaderRef::Path(AssetPath::parse(RENDER_SHADER))
}
fn deferred_fragment_shader() -> ShaderRef {
ShaderRef::Path(AssetPath::parse(RENDER_SHADER))
}
}
pub type TerrainMaterial = ExtendedMaterial<StandardMaterial, TerrainExtension>;
#[derive(Clone, Copy, Debug, PartialEq, ShaderType, Reflect)]
#[reflect(opaque)]
pub struct GroundParams {
pub(crate) terrain: TerrainParamsGpu,
pub(crate) shadow: Vec4,
pub(crate) light: Vec4,
pub(crate) absorption: Vec4,
pub(crate) cover: Vec4,
pub(crate) ripple: Vec4,
pub(crate) gate: Vec4,
}
pub(crate) const NO_COLUMN_GATE: f32 = -1.0e30;
impl Default for GroundParams {
fn default() -> Self {
Self {
terrain: TerrainParamsGpu::default(),
shadow: Vec4::ZERO,
light: Vec4::ZERO,
absorption: Vec4::ZERO,
cover: Vec4::ZERO,
ripple: Vec4::ZERO,
gate: Vec4::new(NO_COLUMN_GATE, 0.0, 0.0, 0.0),
}
}
}
#[derive(Asset, AsBindGroup, Reflect, Debug, Clone)]
pub struct GroundExtension {
#[uniform(100)]
pub(crate) params: GroundParams,
#[storage(101, read_only)]
pub(crate) terrain: Handle<ShaderBuffer>,
#[storage(102, read_only)]
pub(crate) cover: Handle<ShaderBuffer>,
#[texture(103)]
#[sampler(104)]
pub(crate) cloud_shadow: Option<Handle<Image>>,
}
impl Default for GroundExtension {
fn default() -> Self {
Self {
params: GroundParams::default(),
terrain: NO_TERRAIN,
cover: NO_TERRAIN,
cloud_shadow: None,
}
}
}
impl GroundExtension {
#[must_use]
pub fn with_column_gate_m(mut self, metres: f32) -> Self {
self.params.gate.x = metres;
self
}
}
impl MaterialExtension for GroundExtension {
fn fragment_shader() -> ShaderRef {
ShaderRef::Path(AssetPath::parse(GROUND_SHADER))
}
}
pub type GroundMaterial = ExtendedMaterial<StandardMaterial, GroundExtension>;
pub(crate) fn shadow_inputs(shadows: Option<&CloudShadows>) -> (Vec4, Vec4, Option<Handle<Image>>) {
match shadows {
Some(s) => (
Vec4::new(s.origin.x, s.origin.y, s.extent, s.camera_transmittance),
s.light_direction.extend(1.0),
Some(s.map.clone()),
),
None => (
Vec4::new(0.0, 0.0, 1.0, 1.0),
Vec4::new(0.0, 1.0, 0.0, 0.0),
None,
),
}
}
pub(crate) fn lod_uniform(config: &LodConfig) -> ([Vec4; MAX_LEVELS], Vec4) {
(
std::array::from_fn(|k| {
if (k as u32) < config.levels {
Vec4::new(
config.morph_start_m(k as u32),
config.range_m(k as u32),
0.0,
0.0,
)
} else {
Vec4::ZERO
}
}),
Vec4::new((config.levels - 1) as f32, 0.0, 0.0, 0.0),
)
}
fn cover_inputs(
live: bool,
wetness: Option<&WetnessSettings>,
globals: Option<&ElementsGlobals>,
) -> (Vec4, Vec4) {
let defaults = WetnessSettings::default();
let wetness = wetness.unwrap_or(&defaults);
let rain = globals.map_or(0.0, |g| g.rain_intensity);
let ripple = match globals {
Some(g) if rain > 0.0 => Vec4::from_array(g.ripple_phase),
_ => Vec4::ZERO,
};
(
Vec4::new(
f32::from(u8::from(live)),
rain,
wetness.puddle_min_m,
wetness.puddle_full_m,
),
ripple,
)
}
#[allow(clippy::too_many_arguments)]
pub(super) fn update_materials(
state: Option<Res<TerrainState>>,
runtime: Res<super::TerrainRuntime>,
shadows: Option<Res<CloudShadows>>,
cover: Option<Res<CoverState>>,
wetness: Option<Res<WetnessSettings>>,
globals: Option<Res<ElementsGlobals>>,
mut terrain_materials: ResMut<Assets<TerrainMaterial>>,
mut ground_materials: ResMut<Assets<GroundMaterial>>,
water: Option<Res<crate::hydrology::WaterSettings>>,
) {
let (shadow, light, map) = shadow_inputs(shadows.as_deref());
let absorption = water.map_or(Vec4::ZERO, |w| w.absorption.extend(w.refractive_index));
let live = state.as_ref().is_some_and(|s| s.is_live());
let terrain_params = state
.as_ref()
.map(|s| TerrainParamsGpu::new(&s.grid, 1, live))
.unwrap_or_default();
let cells = state.as_ref().map_or(NO_TERRAIN, |s| s.cells.clone());
let cover_cells = cover.as_ref().map_or(NO_TERRAIN, |c| c.cover_cells.clone());
let (cover_params, ripple) = cover_inputs(
cover.is_some() && live,
wetness.as_deref(),
globals.as_deref(),
);
if let (Some(state), Some(config)) = (state.as_ref(), runtime.lod.as_ref()) {
let (lod, top) = lod_uniform(config);
let params = TerrainMaterialParams {
terrain: terrain_params,
lod,
top,
shadow,
light,
absorption,
cover: cover_params,
ripple,
};
let ids: Vec<_> = terrain_materials.iter().map(|(id, _)| id).collect();
for id in ids {
let Some(material) = terrain_materials.get(id) else {
continue;
};
let e = &material.extension;
if (e.params != params
|| e.cells != state.cells
|| e.cover != cover_cells
|| e.cloud_shadow != map)
&& let Some(mut material) = terrain_materials.get_mut(id)
{
material.extension.params = params;
material.extension.cells = state.cells.clone();
material.extension.cover = cover_cells.clone();
material.extension.cloud_shadow = map.clone();
}
}
}
let params = GroundParams {
terrain: terrain_params,
shadow,
light,
absorption,
cover: cover_params,
ripple,
gate: GroundParams::default().gate,
};
let ids: Vec<_> = ground_materials.iter().map(|(id, _)| id).collect();
for id in ids {
let Some(material) = ground_materials.get(id) else {
continue;
};
let e = &material.extension;
let params = GroundParams {
gate: e.params.gate,
..params
};
if (e.params != params
|| e.terrain != cells
|| e.cover != cover_cells
|| e.cloud_shadow != map)
&& let Some(mut material) = ground_materials.get_mut(id)
{
material.extension.params = params;
material.extension.terrain = cells.clone();
material.extension.cover = cover_cells.clone();
material.extension.cloud_shadow = map.clone();
}
}
}
#[cfg(test)]
mod tests {
#[test]
fn each_extension_binds_at_most_two_storage_buffers() {
let source = include_str!("material.rs");
for extension in ["pub struct TerrainExtension", "pub struct GroundExtension"] {
let body = &source[source.find(extension).expect("the struct")..];
let body = &body[..body.find("\n}").expect("the end of the struct")];
let storage = body.matches("#[storage(").count();
assert!(
(1..=2).contains(&storage),
"{extension}: {storage} storage buffers"
);
}
}
}