mod gpu;
pub(crate) mod model;
use std::collections::HashMap;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use bevy::asset::{RenderAssetUsages, embedded_asset};
use bevy::image::{ImageAddressMode, ImageFilterMode, ImageSampler, ImageSamplerDescriptor};
use bevy::prelude::*;
use bevy::render::render_resource::{
Extent3d, ShaderType, TextureDimension, TextureFormat, TextureUsages, TextureViewDescriptor,
TextureViewDimension,
};
use bevy::render::storage::ShaderBuffer;
use crate::gpu::grid::zeroed_buffer;
use crate::hydrology::{NO_STILL_LEVEL, StillWater};
use crate::plugin::{
ElementsQuality, ElementsSystems, ElementsUnits, ElementsView, ensure_gpu_core,
};
use crate::query::Environment;
use crate::terrain::{TerrainGpu, TerrainParamsGpu, TerrainState};
use crate::trails::model::window_origin;
use model::{
CASCADES, ContactGpu, MAX_CONTACTS, MAX_SUBSTEPS, SUBSTEP_S, WaveKernel, band_centre,
cascade_texel, entry_head,
};
#[derive(Component, Clone, Copy, Debug, PartialEq, Reflect)]
#[require(Transform)]
pub struct WaterContact {
pub half_extents: Vec3,
}
impl WaterContact {
pub fn new(half_extents: Vec3) -> Self {
Self { half_extents }
}
pub fn sphere(radius: f32) -> Self {
Self::new(Vec3::splat(radius))
}
}
#[derive(Resource, Clone, Debug, PartialEq, Reflect)]
pub struct WaveSettings {
pub enabled: bool,
pub damping_per_s: f32,
}
impl Default for WaveSettings {
fn default() -> Self {
Self {
enabled: true,
damping_per_s: 0.05,
}
}
}
#[derive(Resource, Clone, Debug)]
pub struct WaveState {
pub field: Handle<Image>,
pub texel_m: [f32; CASCADES],
pub texels: u32,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, ShaderType)]
pub(crate) struct WaveParams {
terrain: TerrainParamsGpu,
origins: [IVec4; CASCADES],
counts: UVec4,
sim: Vec4,
band: Vec4,
kernel: [Vec4; 7 * CASCADES],
}
#[derive(Resource, Clone)]
pub(crate) struct WaveGpu {
pub state_a: Handle<ShaderBuffer>,
pub state_b: Handle<ShaderBuffer>,
pub head: Handle<ShaderBuffer>,
pub force: Handle<ShaderBuffer>,
pub field: Handle<Image>,
pub params: WaveParams,
pub contacts: Vec<ContactGpu>,
pub substeps: u32,
pub executed: Arc<AtomicU64>,
pub ready: Arc<AtomicBool>,
}
#[derive(Resource, Default)]
struct WaveRuntime {
built: Option<(u32, f32)>,
terrain: Option<AssetId<ShaderBuffer>>,
kernel: [Vec4; 7 * CASCADES],
debt: f32,
last: HashMap<Entity, f32>,
}
pub struct WavesPlugin;
impl Plugin for WavesPlugin {
fn build(&self, app: &mut App) {
ensure_gpu_core(app);
embedded_asset!(app, "waves.wesl");
app.init_resource::<WaveSettings>()
.init_resource::<WaveRuntime>()
.add_systems(
Update,
(ensure_waves, run_waves)
.chain()
.in_set(ElementsSystems::Simulation),
);
gpu::plugin(app);
}
}
fn field_image(images: &mut Assets<Image>, size: u32) -> Handle<Image> {
let mut image = Image::new_fill(
Extent3d {
width: size,
height: size,
depth_or_array_layers: CASCADES as u32,
},
TextureDimension::D2,
&[0u8; 8],
TextureFormat::Rgba16Float,
RenderAssetUsages::RENDER_WORLD,
);
image.texture_descriptor.usage = TextureUsages::STORAGE_BINDING
| TextureUsages::TEXTURE_BINDING
| TextureUsages::COPY_DST
| TextureUsages::COPY_SRC;
image.texture_view_descriptor = Some(TextureViewDescriptor {
dimension: Some(TextureViewDimension::D2Array),
..default()
});
image.sampler = ImageSampler::Descriptor(ImageSamplerDescriptor {
address_mode_u: ImageAddressMode::Repeat,
address_mode_v: ImageAddressMode::Repeat,
mag_filter: ImageFilterMode::Linear,
min_filter: ImageFilterMode::Linear,
..default()
});
images.add(image)
}
#[allow(clippy::too_many_arguments)]
fn ensure_waves(
mut commands: Commands,
settings: Res<WaveSettings>,
quality: Res<ElementsQuality>,
terrain: Option<Res<TerrainState>>,
gpu: Option<Res<WaveGpu>>,
mut runtime: ResMut<WaveRuntime>,
mut buffers: ResMut<Assets<ShaderBuffer>>,
mut images: ResMut<Assets<Image>>,
) {
let (Some(terrain), true) = (terrain, settings.enabled) else {
if gpu.is_some() {
commands.remove_resource::<WaveGpu>();
commands.remove_resource::<WaveState>();
*runtime = WaveRuntime::default();
}
return;
};
let window = quality.wave_window();
if gpu.is_some() && runtime.built == Some(window) && runtime.terrain == Some(terrain.water.id())
{
return;
}
let (size, base) = window;
let texels = u64::from(size) * u64::from(size);
let field = field_image(&mut images, size);
let mut kernel = [Vec4::ZERO; 7 * CASCADES];
for c in 0..CASCADES {
kernel[7 * c..7 * c + 7].copy_from_slice(&WaveKernel::fit(cascade_texel(base, c)).packed());
}
*runtime = WaveRuntime {
built: Some(window),
terrain: Some(terrain.water.id()),
kernel,
debt: 0.0,
last: HashMap::new(),
};
commands.insert_resource(WaveState {
field: field.clone(),
texel_m: std::array::from_fn(|c| cascade_texel(base, c)),
texels: size,
});
let cascades = CASCADES as u64;
commands.insert_resource(WaveGpu {
state_a: zeroed_buffer(
&mut buffers,
"elements_waves_state_a",
cascades * texels * 12,
),
state_b: zeroed_buffer(
&mut buffers,
"elements_waves_state_b",
cascades * texels * 12,
),
head: zeroed_buffer(
&mut buffers,
"elements_waves_head",
(2 * cascades - 1) * texels * 4,
),
force: zeroed_buffer(&mut buffers, "elements_waves_force", cascades * texels * 16),
field,
params: WaveParams::default(),
contacts: vec![ContactGpu::default()],
substeps: 0,
executed: Arc::new(AtomicU64::new(0)),
ready: Arc::new(AtomicBool::new(false)),
});
}
#[allow(clippy::too_many_arguments)]
fn run_waves(
view: Res<ElementsView>,
time: Res<Time<Virtual>>,
settings: Res<WaveSettings>,
terrain: Option<Res<TerrainState>>,
terrain_gpu: Option<Res<TerrainGpu>>,
gpu: Option<ResMut<WaveGpu>>,
mut runtime: ResMut<WaveRuntime>,
contacts: Query<(Entity, &WaterContact, &GlobalTransform)>,
env: Environment,
units: Res<ElementsUnits>,
still: Option<Res<StillWater>>,
) {
let (Some(terrain), Some(terrain_gpu), Some(mut gpu)) = (terrain, terrain_gpu, gpu) else {
return;
};
let Some((size, base)) = runtime.built else {
return;
};
let camera = Vec2::new(view.position.x, view.position.z);
let mut origins = [IVec4::ZERO; CASCADES];
for (c, origin) in origins.iter_mut().enumerate() {
let o = window_origin(camera, cascade_texel(base, c), size);
*origin = IVec4::new(o.x, o.y, o.x, o.y);
}
let dt = time.delta_secs();
let mut seen = HashMap::with_capacity(runtime.last.len());
let mut keyed: Vec<(f32, Entity, ContactGpu)> = contacts
.iter()
.filter_map(|(entity, contact, transform)| {
let transform = units.to_metres(transform);
let (scale, rotation, translation) = transform.to_scale_rotation_translation();
let half = contact.half_extents * scale;
let mut upload = ContactGpu::new(translation, half, rotation)?;
let xz = Vec2::new(translation.x, translation.z);
seen.insert(entity, translation.y);
if let (Some(prev), Some(ground), Some(depth)) = (
runtime.last.get(&entity),
env.terrain_height_metres(xz),
env.water_depth_metres(xz),
) {
upload.m1.z = entry_head(
*prev,
translation.y,
ContactGpu::half_height(half, rotation),
ground + depth,
dt,
);
}
Some((xz.distance_squared(camera), entity, upload))
})
.collect();
runtime.last = seen;
if keyed.len() > MAX_CONTACTS {
keyed.select_nth_unstable_by(MAX_CONTACTS, |a, b| a.0.total_cmp(&b.0).then(a.1.cmp(&b.1)));
keyed.truncate(MAX_CONTACTS);
}
let tile = keyed
.iter()
.map(|(_, _, c)| (2.0 * c.centre.w / base).ceil() as u32 + 3)
.max()
.unwrap_or(0)
.min(1024);
runtime.debt = (runtime.debt + time.delta_secs()).min(MAX_SUBSTEPS as f32 * SUBSTEP_S);
let substeps = (runtime.debt / SUBSTEP_S).floor() as u32;
runtime.debt -= substeps as f32 * SUBSTEP_S;
let live = gpu.ready.load(Ordering::Relaxed)
&& terrain.is_live()
&& terrain_gpu.generated.load(Ordering::Relaxed);
gpu.substeps = if live { substeps } else { 0 };
gpu.params = WaveParams {
terrain: TerrainParamsGpu::new(&terrain.grid, 1, true),
origins,
counts: UVec4::new(size, keyed.len() as u32, 0, tile),
sim: Vec4::new(
base,
SUBSTEP_S,
settings.damping_per_s.max(0.0),
still.map_or(NO_STILL_LEVEL, |s| s.level / units.per_metre()),
),
band: Vec4::new(
band_centre(cascade_texel(base, 0)),
band_centre(cascade_texel(base, 1)),
band_centre(cascade_texel(base, 2)),
0.0,
),
kernel: runtime.kernel,
};
gpu.contacts = if keyed.is_empty() {
vec![ContactGpu::default()]
} else {
keyed.into_iter().map(|(_, _, c)| c).collect()
};
}