use bevy::prelude::*;
use bevy::render::render_resource::{AsBindGroup, ShaderType};
use bevy::shader::ShaderRef;
use bevy::sprite_render::{AlphaMode2d, Material2d, Material2dPlugin};
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum WaterMode {
Sea = 0,
Pool = 1,
Glint = 2,
Ocean = 3,
}
#[derive(ShaderType, Clone, Copy, Debug)]
pub struct WaterUniform {
pub shallow: LinearRgba,
pub deep: LinearRgba,
pub tint: LinearRgba,
pub mode: u32,
pub caustics: f32,
pub wet: f32,
pub depth_span: f32,
pub motion: f32,
}
impl WaterUniform {
pub fn new(mode: WaterMode, shallow: Color, deep: Color) -> Self {
WaterUniform {
shallow: shallow.into(),
deep: deep.into(),
tint: LinearRgba::NONE,
mode: mode as u32,
caustics: CAUSTICS,
wet: 0.0,
depth_span: 1.0,
motion: 1.0,
}
}
}
const CAUSTICS: f32 = 0.16;
#[derive(Asset, TypePath, AsBindGroup, Clone, Debug)]
pub struct WaterMaterial {
#[uniform(0)]
pub water: WaterUniform,
#[texture(1)]
#[sampler(2)]
pub mask: Handle<Image>,
}
impl Material2d for WaterMaterial {
fn fragment_shader() -> ShaderRef {
"shaders/water.wgsl".into()
}
fn alpha_mode(&self) -> AlphaMode2d {
AlphaMode2d::Blend
}
}
pub const POOL_SHALLOW: Color = Color::srgb(0.44, 0.69, 0.80);
pub const POOL_DEEP: Color = Color::srgb(0.33, 0.57, 0.72);
fn ocean_uniform() -> WaterUniform {
let mut water = crate::app::board_render::sea_uniform();
water.mode = WaterMode::Ocean as u32;
water.deep = Color::srgb(0.16, 0.36, 0.52).into();
water.caustics = CAUSTICS * 0.8;
water
}
pub const SAND: Color = Color::srgb(0.78, 0.65, 0.46);
#[derive(Clone, Default)]
pub struct Shaders {
pub quad: Handle<Mesh>,
pub pool_shallow: Handle<WaterMaterial>,
pub pool_deep: Handle<WaterMaterial>,
pub glint: Handle<WaterMaterial>,
pub sea: Handle<WaterMaterial>,
pub ocean: Handle<WaterMaterial>,
pub sand: Handle<SandMaterial>,
pub board_sand: Handle<SandMaterial>,
pub prints: Handle<FadeMaterial>,
}
impl Shaders {
pub fn quad<M: Material2d>(
&self,
material: &Handle<M>,
place: Transform,
size: Vec2,
) -> (Mesh2d, MeshMaterial2d<M>, Transform) {
(
Mesh2d(self.quad.clone()),
MeshMaterial2d(material.clone()),
place.with_scale(size.extend(1.0)),
)
}
pub fn new(world: &mut World, puddle: &Handle<Image>) -> Self {
let quad = world
.resource_mut::<Assets<Mesh>>()
.add(Rectangle::new(1.0, 1.0));
let noise = world.resource_mut::<Assets<Image>>().add(sand_noise());
let mut materials = world.resource_mut::<Assets<WaterMaterial>>();
let mut layer = |mode, colour| {
materials.add(WaterMaterial {
water: WaterUniform::new(mode, colour, colour),
mask: puddle.clone(),
})
};
Shaders {
quad,
pool_shallow: layer(WaterMode::Pool, POOL_SHALLOW),
pool_deep: layer(WaterMode::Pool, POOL_DEEP),
glint: layer(WaterMode::Glint, Color::WHITE),
sea: materials.add(WaterMaterial {
water: crate::app::board_render::sea_uniform(),
mask: Handle::default(),
}),
ocean: materials.add(WaterMaterial {
water: ocean_uniform(),
mask: Handle::default(),
}),
sand: world
.resource_mut::<Assets<SandMaterial>>()
.add(SandMaterial {
sand: SandUniform::beach(),
noise: noise.clone(),
}),
board_sand: world
.resource_mut::<Assets<SandMaterial>>()
.add(SandMaterial {
sand: SandUniform::board(),
noise,
}),
prints: world
.resource_mut::<Assets<FadeMaterial>>()
.add(FadeMaterial::new(PRINT)),
}
}
}
pub(super) fn still_water(
settings: Res<crate::app::settings::GameSettings>,
art: Res<crate::app::art::Art>,
mut materials: ResMut<Assets<WaterMaterial>>,
) {
if !settings.is_changed() {
return;
}
let motion = if settings.reduced_motion { 0.0 } else { 1.0 };
let Shaders {
quad: _,
pool_shallow,
pool_deep,
glint,
sea,
ocean,
sand: _,
board_sand: _,
prints: _,
} = &art.shaded;
for handle in [pool_shallow, pool_deep, glint, sea, ocean] {
if materials
.get(handle)
.is_some_and(|m| m.water.motion != motion)
&& let Some(mut material) = materials.get_mut(handle)
{
material.water.motion = motion;
}
}
}
pub(super) fn install(app: &mut App) {
app.add_plugins((
Material2dPlugin::<WaterMaterial>::default(),
Material2dPlugin::<SandMaterial>::default(),
Material2dPlugin::<FadeMaterial>::default(),
))
.insert_resource(ClearColor(SAND))
.add_systems(Update, still_water);
app.world_mut()
.resource_mut::<Time<Virtual>>()
.set_wrap_period(std::time::Duration::from_secs(24 * 60 * 60));
}
pub const PRINT: Color = Color::srgba(0.45, 0.36, 0.24, 0.9);
#[derive(Asset, TypePath, AsBindGroup, Clone, Debug)]
pub struct FadeMaterial {
#[uniform(0)]
pub color: LinearRgba,
}
impl FadeMaterial {
pub fn new(color: Color) -> Self {
FadeMaterial {
color: color.into(),
}
}
}
impl Material2d for FadeMaterial {
fn fragment_shader() -> ShaderRef {
"shaders/fade.wesl".into()
}
fn alpha_mode(&self) -> AlphaMode2d {
AlphaMode2d::Blend
}
fn specialize(
pipeline: &bevy::sprite_render::Material2dPipeline,
descriptor: &mut bevy::render::render_resource::RenderPipelineDescriptor,
_layout: &bevy::mesh::MeshVertexBufferLayoutRef,
_key: bevy::sprite_render::Material2dKey<Self>,
) -> Result<(), bevy::render::render_resource::SpecializedMeshPipelineError> {
if let Some(batch) = pipeline.mesh2d_pipeline.per_object_buffer_batch_size
&& let Some(fragment) = descriptor.fragment.as_mut()
{
fragment.shader_defs.push(bevy::shader::ShaderDefVal::UInt(
"PER_OBJECT_BUFFER_BATCH_SIZE".into(),
batch,
));
}
Ok(())
}
}
pub fn fade_tag(alpha: f32) -> bevy::mesh::MeshTag {
bevy::mesh::MeshTag::new(alpha.to_bits())
}
#[derive(ShaderType, Clone, Copy, Debug)]
pub struct SandUniform {
pub low: LinearRgba,
pub high: LinearRgba,
pub grain_dark: LinearRgba,
pub grain_pale: LinearRgba,
pub mode: u32,
}
impl SandUniform {
fn beach() -> SandUniform {
SandUniform {
low: SAND.into(),
high: Color::srgb(0.85, 0.73, 0.53).into(),
grain_dark: Color::srgba(0.45, 0.36, 0.24, 0.35).into(),
grain_pale: Color::srgba(0.98, 0.94, 0.84, 0.4).into(),
mode: 0,
}
}
fn board() -> SandUniform {
SandUniform {
low: Color::srgb(0.898, 0.808, 0.631).into(),
high: Color::srgb(0.925, 0.847, 0.690).into(),
grain_dark: LinearRgba::new(0.0, 0.0, 0.0, 0.215),
grain_pale: LinearRgba::new(1.0, 1.0, 1.0, 0.33),
mode: 1,
}
}
}
#[derive(Asset, TypePath, AsBindGroup, Clone, Debug)]
pub struct SandMaterial {
#[uniform(0)]
pub sand: SandUniform,
#[texture(1)]
#[sampler(2)]
pub noise: Handle<Image>,
}
const NOISE_PX: u32 = 256;
const NOISE_CELLS: u32 = 16;
pub fn sand_noise() -> Image {
use bevy::asset::RenderAssetUsages;
use bevy::image::{ImageAddressMode, ImageSampler, ImageSamplerDescriptor};
use bevy::render::render_resource::{Extent3d, TextureDimension, TextureFormat};
let octave = |cells: u32, seed: u64| -> Vec<f32> {
let mut state = seed;
let mut next = || {
state = state.wrapping_add(0x9E37_79B9_7F4A_7C15);
let mut z = state;
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
((z ^ (z >> 31)) >> 40) as f32 / (1u64 << 24) as f32
};
let lattice: Vec<f32> = (0..cells * cells).map(|_| next()).collect();
let at = |x: u32, y: u32| lattice[((y % cells) * cells + (x % cells)) as usize];
let per = NOISE_PX as f32 / cells as f32;
(0..NOISE_PX * NOISE_PX)
.map(|i| {
let (px, py) = ((i % NOISE_PX) as f32 + 0.5, (i / NOISE_PX) as f32 + 0.5);
let (fx, fy) = (px / per, py / per);
let (x0, y0) = (fx.floor() as u32, fy.floor() as u32);
let ease = |t: f32| t * t * (3.0 - 2.0 * t);
let (u, v) = (ease(fx.fract()), ease(fy.fract()));
let top = at(x0, y0) + (at(x0 + 1, y0) - at(x0, y0)) * u;
let bottom = at(x0, y0 + 1) + (at(x0 + 1, y0 + 1) - at(x0, y0 + 1)) * u;
top + (bottom - top) * v
})
.collect()
};
let red = octave(NOISE_CELLS, 1);
let green = octave(NOISE_CELLS, 2);
let (blue_a, blue_b) = (octave(NOISE_CELLS, 3), octave(NOISE_CELLS * 2, 4));
let byte = |v: f32| (v.clamp(0.0, 1.0) * 255.0).round() as u8;
let data = (0..(NOISE_PX * NOISE_PX) as usize)
.flat_map(|i| {
let blue = (2.0 * blue_a[i] + blue_b[i]) / 3.0;
[byte(red[i]), byte(green[i]), byte(blue), 255]
})
.collect();
let mut image = Image::new(
Extent3d {
width: NOISE_PX,
height: NOISE_PX,
depth_or_array_layers: 1,
},
TextureDimension::D2,
data,
TextureFormat::Rgba8Unorm,
RenderAssetUsages::RENDER_WORLD,
);
image.sampler = ImageSampler::Descriptor(ImageSamplerDescriptor {
address_mode_u: ImageAddressMode::Repeat,
address_mode_v: ImageAddressMode::Repeat,
..ImageSamplerDescriptor::linear()
});
image
}
impl Material2d for SandMaterial {
fn fragment_shader() -> ShaderRef {
"shaders/sand.wgsl".into()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_sand_noise_tiles_without_a_seam() {
let image = sand_noise();
let data = image.data.as_ref().expect("built with its data");
let n = NOISE_PX as usize;
let at = |x: usize, y: usize, c: usize| i16::from(data[(y * n + x) * 4 + c]);
for c in 0..3 {
let mut inner = 0;
let mut seam = 0;
for i in 0..n {
inner = inner.max((at(n / 2, i, c) - at(n / 2 - 1, i, c)).abs());
inner = inner.max((at(i, n / 2, c) - at(i, n / 2 - 1, c)).abs());
seam = seam.max((at(0, i, c) - at(n - 1, i, c)).abs());
seam = seam.max((at(i, 0, c) - at(i, n - 1, c)).abs());
}
assert!(
seam <= inner + 2,
"channel {c}: seam {seam} against {inner} inside"
);
}
}
}