use bevy::asset::{AssetPath, RenderAssetUsages, uuid_handle};
use bevy::mesh::MeshVertexBufferLayoutRef;
use bevy::pbr::{
ExtendedMaterial, MaterialExtension, MaterialExtensionKey, MaterialExtensionPipeline,
};
use bevy::prelude::*;
use bevy::render::render_resource::{
AsBindGroup, RenderPipelineDescriptor, ShaderType, SpecializedMeshPipelineError,
};
use bevy::render::storage::ShaderBuffer;
use bevy::shader::ShaderRef;
use super::lod::{LodConfig, MAX_LEVELS};
use super::textures::GroundTexturePack;
use super::{TerrainParamsGpu, TerrainState};
use crate::ElementsUnits;
use crate::clouds::CloudShadows;
use crate::cover::{CoverState, WetnessSettings};
use crate::trails::TrailState;
use crate::trails::model::MARCH_BOUND_M;
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, PartialEq, ShaderType, Reflect)]
#[reflect(opaque)]
pub struct TerrainMaterialParams {
pub(crate) terrain: TerrainParamsGpu,
pub(crate) lod: [Vec4; MAX_LEVELS],
pub(crate) top: Vec4,
pub(crate) lod_centre: Vec4,
pub(crate) shadow: Vec4,
pub(crate) light: Vec4,
pub(crate) absorption: Vec4,
pub(crate) cover: Vec4,
pub(crate) ripple: Vec4,
pub(crate) trail: Vec4,
pub(crate) trail_light: Vec4,
pub(crate) ground: Vec4,
pub(crate) colours: [Vec4; 3],
pub(crate) look: Vec4,
pub(crate) detail: Vec4,
pub(crate) path: Vec4,
pub(crate) path_scale: Vec4,
pub(crate) units: f32,
}
impl Default for TerrainMaterialParams {
fn default() -> Self {
Self {
terrain: TerrainParamsGpu::default(),
lod: [Vec4::ZERO; MAX_LEVELS],
top: Vec4::ZERO,
lod_centre: Vec4::ZERO,
shadow: Vec4::ZERO,
light: Vec4::ZERO,
absorption: Vec4::ZERO,
cover: Vec4::ZERO,
ripple: Vec4::ZERO,
trail: Vec4::ZERO,
trail_light: Vec4::ZERO,
ground: Vec4::W,
colours: [Vec4::ZERO; 3],
look: Vec4::ONE,
detail: Vec4::new(1.0, 0.0, 0.0, 0.0),
path: Vec4::ZERO,
path_scale: Vec4::ZERO,
units: 1.0,
}
}
}
#[derive(Asset, AsBindGroup, Reflect, Debug, Clone)]
#[bind_group_data(TerrainExtensionKey)]
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>>,
#[texture(105)]
#[sampler(106)]
pub(crate) trail: Option<Handle<Image>>,
#[texture(107, dimension = "2d_array")]
#[sampler(108)]
pub(crate) ground: Option<Handle<Image>>,
#[texture(109)]
#[sampler(110)]
pub(crate) path: Option<Handle<Image>>,
}
#[repr(C)]
#[derive(Eq, PartialEq, Hash, Copy, Clone, Debug)]
pub struct TerrainExtensionKey {
pub ground_textures: bool,
pub ground_colours: bool,
pub paths: bool,
}
impl From<&TerrainExtension> for TerrainExtensionKey {
fn from(extension: &TerrainExtension) -> Self {
let ground_textures = extension.ground.is_some();
Self {
ground_textures,
ground_colours: ground_textures && extension.params.colours.iter().any(|c| c.w > 0.0),
paths: extension.path.is_some(),
}
}
}
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))
}
fn specialize(
_pipeline: &MaterialExtensionPipeline,
descriptor: &mut RenderPipelineDescriptor,
_layout: &MeshVertexBufferLayoutRef,
key: MaterialExtensionKey<Self>,
) -> Result<(), SpecializedMeshPipelineError> {
if key.bind_group_data.ground_textures
&& let Some(fragment) = descriptor.fragment.as_mut()
{
fragment.shader_defs.push("GROUND_TEXTURES".into());
if key.bind_group_data.ground_colours {
fragment.shader_defs.push("GROUND_COLOURS".into());
}
}
if key.bind_group_data.paths
&& let Some(fragment) = descriptor.fragment.as_mut()
{
fragment.shader_defs.push("PATHS".into());
}
Ok(())
}
}
pub type TerrainMaterial = ExtendedMaterial<StandardMaterial, TerrainExtension>;
#[derive(Resource, Clone, Copy, Debug, PartialEq, Reflect)]
#[reflect(Resource)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct TerrainLook {
pub tint: Vec3,
pub saturation: f32,
pub detail: f32,
pub finest_m: Option<f32>,
}
impl Default for TerrainLook {
fn default() -> Self {
Self {
tint: Vec3::ONE,
saturation: 1.0,
detail: 1.0,
finest_m: None,
}
}
}
impl TerrainLook {
pub(crate) fn packed(&self) -> Vec4 {
self.tint.max(Vec3::ZERO).extend(self.saturation.max(0.0))
}
pub(crate) fn packed_detail(&self) -> Vec4 {
let floor = self
.finest_m
.filter(|m| m.is_finite())
.map_or(0.0, |m| m.max(0.0));
Vec4::new(self.detail.max(0.0), floor, 0.0, 0.0)
}
}
fn ground_inputs(
pack: &GroundTexturePack,
images: &Assets<Image>,
) -> (Vec4, [Vec4; 3], Option<Handle<Image>>) {
match &pack.image {
Some(image) if images.contains(image) => (pack.ground, pack.colours, Some(image.clone())),
_ => (Vec4::W, [Vec4::ZERO; 3], None),
}
}
#[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) trail: Vec4,
pub(crate) trail_light: Vec4,
pub(crate) units: f32,
}
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),
trail: Vec4::ZERO,
trail_light: Vec4::ZERO,
units: 1.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>>,
#[texture(105)]
#[sampler(106)]
pub(crate) trail: Option<Handle<Image>>,
}
impl Default for GroundExtension {
fn default() -> Self {
Self {
params: GroundParams::default(),
terrain: NO_TERRAIN,
cover: NO_TERRAIN,
cloud_shadow: None,
trail: 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.mean_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),
)
}
pub(crate) fn lod_centre_uniform(centre: Option<Vec3>) -> Vec4 {
centre.map_or(Vec4::ZERO, |c| c.extend(1.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,
)
}
fn trail_inputs(trails: Option<&TrailState>, live: bool) -> (Vec4, Vec4, Option<Handle<Image>>) {
match trails {
Some(t) if live => (
Vec4::new(t.texel_m, t.texels as f32, 1.0, MARCH_BOUND_M),
Vec4::new(t.direct_share, t.centre.x, t.centre.y, 0.0),
Some(t.field.clone()),
),
_ => (Vec4::ZERO, Vec4::ZERO, None),
}
}
#[allow(clippy::too_many_arguments)]
pub(super) fn update_materials(
state: Option<Res<TerrainState>>,
runtime: Res<super::TerrainRuntime>,
shadows: Option<Res<CloudShadows>>,
units: Res<ElementsUnits>,
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>>,
trails: Option<Res<TrailState>>,
textures: Res<GroundTexturePack>,
images: Res<Assets<Image>>,
look: Res<TerrainLook>,
paths: Option<Res<crate::PathState>>,
) {
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(),
);
let (trail_window, trail_light, field) = trail_inputs(trails.as_deref(), live);
let (ground, colours, ground_maps) = ground_inputs(&textures, &images);
let (path_lane, path_map) =
paths
.as_ref()
.filter(|p| images.contains(&p.mask))
.map_or((Vec4::ZERO, None), |p| {
(
p.origin.extend(p.texel_m).extend(p.texels as f32),
Some(p.mask.clone()),
)
});
let path_scale = paths
.as_ref()
.filter(|p| images.contains(&p.mask))
.map_or(Vec4::ZERO, |p| Vec4::new(p.half_width_m, 0.0, 0.0, 0.0));
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,
lod_centre: lod_centre_uniform(runtime.lod_centre),
shadow,
light,
absorption,
cover: cover_params,
ripple,
trail: trail_window,
trail_light,
ground,
colours,
look: look.packed(),
detail: look.packed_detail(),
path: path_lane,
path_scale,
units: units.per_metre(),
};
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
|| e.trail != field
|| e.ground != ground_maps
|| e.path != path_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();
material.extension.trail = field.clone();
material.extension.ground = ground_maps.clone();
material.extension.path = path_map.clone();
}
}
}
let params = GroundParams {
terrain: terrain_params,
shadow,
light,
absorption,
cover: cover_params,
ripple,
gate: GroundParams::default().gate,
trail: trail_window,
trail_light,
units: units.per_metre(),
};
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
|| e.trail != field)
&& 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();
material.extension.trail = field.clone();
}
}
}
#[cfg(test)]
mod tests {
use bevy::math::{Vec3, Vec4};
use super::TerrainLook;
#[test]
fn the_default_look_packs_to_the_shaders_identity() {
assert_eq!(TerrainLook::default().packed(), Vec4::ONE);
let clamped = TerrainLook {
tint: Vec3::new(-1.0, 0.5, 2.0),
saturation: -0.3,
..TerrainLook::default()
}
.packed();
assert_eq!(clamped, Vec4::new(0.0, 0.5, 2.0, 0.0));
}
#[test]
fn the_detail_lane_defaults_to_the_identity_and_clamps() {
assert_eq!(
TerrainLook::default().packed_detail(),
Vec4::new(1.0, 0.0, 0.0, 0.0)
);
let look = |detail, finest_m| TerrainLook {
detail,
finest_m,
..TerrainLook::default()
};
assert_eq!(
look(-2.0, Some(0.04)).packed_detail(),
Vec4::new(0.0, 0.04, 0.0, 0.0)
);
assert_eq!(look(0.5, Some(-1.0)).packed_detail().y, 0.0);
assert_eq!(look(0.5, Some(f32::NAN)).packed_detail().y, 0.0);
assert_eq!(look(0.5, Some(f32::INFINITY)).packed_detail().y, 0.0);
}
#[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"
);
}
}
}