bevy-aqua-waves 0.1.2

Aqua wave producers: Gerstner component bands and JONSWAP/FFT AnimWaves compute.
// Reimplementation of the approach in Crest Shaders/Resources/ShapeCombine.compute.

const LOD_COUNT: u32 = 5u;
const CASCADE_COUNT: u32 = LOD_COUNT + 1u;
const WAVE_COUNT: u32 = 40u;

struct CascadeParams {
    center: vec2<f32>,
    scale: f32,
    texture_res: f32,
    inv_texture_res: f32,
    texel_width: f32,
    weight: f32,
    max_wavelength: f32,
}
struct CascadeLayout {
    cascades: array<CascadeParams, CASCADE_COUNT>,
    center: vec4<f32>,
    // XY bed-map first-texel world origin, ZW inverse world extent.
    bed_transform: vec4<f32>,
    // X height minimum, Y height span (negative = no bed map), Z sea level.
    bed_range: vec4<f32>,
}
struct GerstnerWave {
    direction: vec2<f32>,
    amplitude: f32,
    wave_number: f32,
    angular_frequency: f32,
    phase: f32,
    chop_amplitude: f32,
}
struct AnimWavesUniform {
    cascade_layout: CascadeLayout,
    waves: array<GerstnerWave, WAVE_COUNT>,
    ranges: array<vec4<u32>, LOD_COUNT>,
    time: vec4<f32>,
    // xy: world-space current in m/s; kept identical to the other
    // AnimWavesUniform declarations so one upload serves every consumer.
    flow: vec4<f32>,
}

@group(0) @binding(0) var raw_waves: texture_2d_array<f32>;
@group(0) @binding(1) var previous: texture_2d_array<f32>;
@group(0) @binding(2) var linear_sampler: sampler;
@group(0) @binding(3) var output: texture_storage_2d_array<rgba16float, write>;
@group(0) @binding(4) var<uniform> params: AnimWavesUniform;

fn combine(id: vec3<u32>, slice: u32) {
    let cascade = params.cascade_layout.cascades[slice];
    if id.x >= u32(cascade.texture_res) || id.y >= u32(cascade.texture_res) {
        return;
    }
    var displacement = textureLoad(raw_waves, vec2<i32>(id.xy), i32(slice), 0).xyz;
    if slice + 1u < LOD_COUNT {
        let uv = (vec2<f32>(id.xy) + vec2(0.5)) * cascade.inv_texture_res;
        let coverage = cascade.texel_width * cascade.texture_res;
        let world_xz = coverage * (uv - vec2(0.5)) + cascade.center;
        let next = params.cascade_layout.cascades[slice + 1u];
        let next_coverage = next.texel_width * next.texture_res;
        let next_uv = (world_xz - next.center) / next_coverage + vec2(0.5);
        displacement += textureSampleLevel(
            previous,
            linear_sampler,
            next_uv,
            i32(slice + 1u),
            0.0,
        ).xyz;
    }
    textureStore(output, vec2<i32>(id.xy), i32(slice), vec4(displacement, 0.0));
}

@compute @workgroup_size(8, 8, 1)
fn combine_0(@builtin(global_invocation_id) id: vec3<u32>) { combine(id, 0u); }
@compute @workgroup_size(8, 8, 1)
fn combine_1(@builtin(global_invocation_id) id: vec3<u32>) { combine(id, 1u); }
@compute @workgroup_size(8, 8, 1)
fn combine_2(@builtin(global_invocation_id) id: vec3<u32>) { combine(id, 2u); }
@compute @workgroup_size(8, 8, 1)
fn combine_3(@builtin(global_invocation_id) id: vec3<u32>) { combine(id, 3u); }
@compute @workgroup_size(8, 8, 1)
fn combine_4(@builtin(global_invocation_id) id: vec3<u32>) { combine(id, 4u); }