bevy-aqua-core 0.1.2

Aqua cascade and material ABI, water-field types, bed map, shared render plumbing, and WGSL contracts.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
// Aqua cascade sampling boundary: ABI structs, material bindings, shared
// constants, and the field/level/flow/depth sampling entry points. Feature
// modules (#import-ed by the composed material) may only reach shared state
// through this file; per-invocation private state is read/written here and
// handed to feature modules as plain values or small snapshots.
//
// Reimplementation of the approach in Crest OceanVertHelpers.hlsl, OceanHelpersNew.hlsl, and Ocean.shader.
// AnimWaves sampling follows Scripts/LodData/LodDataMgrAnimWaves.cs.

#define_import_path aqua::cascade

#import bevy_pbr::{
    forward_io::Vertex,
    mesh_functions,
    mesh_view_types,
    clustered_forward as clustering,
    lighting,
    prepass_utils,
    shadows,
    mesh_view_bindings::{light_probes, lights, view},
    view_transformations::position_world_to_clip,
}
#import bevy_pbr::mesh_view_bindings as view_bindings

// The six-element array is fixed uniform ABI; active LOD count comes from the uniform.
const CASCADE_COUNT: u32 = 6u;
const VERTEX_SNAP_MULTIPLIER: f32 = 2.0;
const COARSE_GRID_MULTIPLIER: f32 = 4.0;
const LOD_TRANSITION_START: f32 = 1.0;
// Crest 0.4 morph fade at eight vertices per 64-vertex tile.
const MORPH_BLACK_POINT: f32 = 0.05;
const MORPH_FADE_SIDES: f32 = 2.0;
const MORPH_INNER_RADIUS: f32 = 0.375;

const GRID_CELL_CENTER: f32 = 0.5;
const UV_CENTER: f32 = 0.5;
const MIN_SAMPLE_WEIGHT: f32 = 0.001;
const MIN_NORMAL_Y: f32 = 0.0001;
const SAFE_LENGTH_SQUARED: f32 = 1e-8;
const LUMINANCE_EPSILON: f32 = 0.0001;

const DEBUG_MODE_WATER_PATH: u32 = 1u;
const DEBUG_MODE_REFRACTION_VALIDITY: u32 = 2u;
const DEBUG_MODE_TRANSMISSION: u32 = 3u;
const DEBUG_MODE_UNREFRACTED: u32 = 4u;
const DEBUG_MODE_BEER_LAMBERT: u32 = 5u;
const DEBUG_MODE_SEA_FLOOR: u32 = 6u;
const DEBUG_MODE_BEAUTY: u32 = 7u;
const DEBUG_MODE_REFLECTION: u32 = 8u;
const DEBUG_MODE_FOAM: u32 = 9u;
const DEBUG_MODE_WAVE_HEIGHT: u32 = 10u;
const DEBUG_MODE_LIGHT_RADIANCE: u32 = 11u;
const DEBUG_MODE_REFLECTION_FRACTION: u32 = 12u;
const DEBUG_MODE_FAR_TIER: u32 = 13u;

const CREST_SSS_MAXIMUM: f32 = 0.6;
const CREST_SSS_RANGE: f32 = 0.12;
const CREST_SSS_UNCOMPRESSED: f32 = CREST_SSS_MAXIMUM - CREST_SSS_RANGE;

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 PlanarReflectionView {
    view_projection: mat4x4<f32>,
    level: f32,
}

struct PlanarReflectionParams {
    views: array<PlanarReflectionView, 2>,
    view_count: u32,
    distortion: f32,
}

struct PlanarReflectionSample {
    color: vec3<f32>,
    weight: f32,
}

// Allows displaced surface positions to feather just beyond the projected target.
const PLANAR_PROJECTION_GUARD: f32 = 0.03;

struct SurfaceParams {
    deep_color: vec4<f32>,
    grazing_color: vec4<f32>,
    shallow_color: vec4<f32>,
    fresnel: vec4<f32>,
    reflection: vec4<f32>,
    sun: vec4<f32>,
    debug: vec4<f32>,
    fog_density: vec4<f32>,
    sea_floor: vec4<f32>,
    sss_tint: vec4<f32>,
    sss: vec4<f32>,
    detail: vec4<f32>,
    capillary: vec4<f32>,
    foam: vec4<f32>,
    advection: vec4<f32>,
    /// x/y: configurable far-tier start/end distances in metres.
    far_tier: vec4<f32>,
    /// Strength, metres per cell, metres per second, and maximum depth in metres.
    caustics: vec4<f32>,
}

/// Localized-water extent controls; mirrors lod::BodyParams. flags.x is 1.0
/// for bounded bodies: the vertex stage skips camera snap/morph and the
/// fragment stage culls against extent.xy (centre) and extent.w (radius).
struct BodyParams {
    flags: vec4<f32>,
    extent: vec4<f32>,
    aabb_min: vec4<f32>,
    aabb_size: vec4<f32>,
    /// rgb: per-channel Beer-Lambert extinction in 1/m; w: optics enable.
    optics_a: vec4<f32>,
    /// x: scatter-endpoint scale (deep-pool darkness); yzw reserved.
    optics_b: vec4<f32>,
}

@group(#{MATERIAL_BIND_GROUP}) @binding(0) var lod_data: texture_2d_array<f32>;
@group(#{MATERIAL_BIND_GROUP}) @binding(1) var lod_sampler: sampler;
@group(#{MATERIAL_BIND_GROUP}) @binding(2) var<uniform> cascade_layout: CascadeLayout;
@group(#{MATERIAL_BIND_GROUP}) @binding(3) var<uniform> surface: SurfaceParams;
@group(#{MATERIAL_BIND_GROUP}) @binding(4) var bed_height: texture_2d<f32>;
@group(#{MATERIAL_BIND_GROUP}) @binding(5) var detail_normal: texture_2d<f32>;
@group(#{MATERIAL_BIND_GROUP}) @binding(6) var detail_sampler: sampler;
@group(#{MATERIAL_BIND_GROUP}) @binding(7) var foam_data: texture_2d_array<f32>;
@group(#{MATERIAL_BIND_GROUP}) @binding(8) var foam_sampler: sampler;
@group(#{MATERIAL_BIND_GROUP}) @binding(9) var foam_pattern: texture_2d<f32>;
@group(#{MATERIAL_BIND_GROUP}) @binding(10) var foam_pattern_sampler: sampler;
@group(#{MATERIAL_BIND_GROUP}) @binding(11) var fft_surface: texture_2d_array<f32>;
/// Global water fields: region mapping, per-slot body parameters, and the
/// baked level/slot + flow textures. Mirrors fields::FieldParams.
const MAX_BODIES: u32 = 16u;

struct FieldParams {
    /// xy: region minimum in metres; zw: region size in metres.
    region: vec4<f32>,
    /// x: bounded body count; y: 1.0 when the Ocean resource is present;
    /// z: metres per texel; w: reserved.
    info: vec4<f32>,
    bodies: array<BodyParams, MAX_BODIES>,
}

@group(#{MATERIAL_BIND_GROUP}) @binding(15) var<uniform> field_params: FieldParams;
@group(#{MATERIAL_BIND_GROUP}) @binding(16) var field_level_id: texture_2d<f32>;
@group(#{MATERIAL_BIND_GROUP}) @binding(17) var field_sampler: sampler;
@group(#{MATERIAL_BIND_GROUP}) @binding(18) var field_flow: texture_2d<f32>;
@group(#{MATERIAL_BIND_GROUP}) @binding(19) var reflection_a: texture_2d<f32>;
@group(#{MATERIAL_BIND_GROUP}) @binding(20) var reflection_sampler: sampler;
@group(#{MATERIAL_BIND_GROUP}) @binding(21) var reflection_b: texture_2d<f32>;
@group(#{MATERIAL_BIND_GROUP}) @binding(22) var<uniform> planar_reflections: PlanarReflectionParams;

@group(#{MATERIAL_BIND_GROUP}) @binding(23) var caustics_texture: texture_2d<f32>;
@group(#{MATERIAL_BIND_GROUP}) @binding(24) var caustics_sampler: sampler;

fn sample_planar_reflection(
    world_position: vec3<f32>,
    surface_level: f32,
    surface_normal: vec3<f32>,
) -> PlanarReflectionSample {
    if planar_reflections.view_count == 0u {
        return PlanarReflectionSample(vec3(0.0), 0.0);
    }
    var index = 0u;
    if planar_reflections.view_count > 1u
        && abs(surface_level - planar_reflections.views[1].level)
            < abs(surface_level - planar_reflections.views[0].level) {
        index = 1u;
    }
    let view = planar_reflections.views[index];
    let clip = view.view_projection * vec4(world_position, 1.0);
    if clip.w <= 0.0 {
        return PlanarReflectionSample(vec3(0.0), 0.0);
    }
    let ndc = clip.xy / clip.w;
    let projected_uv = vec2(ndc.x, -ndc.y) * 0.5 + 0.5;
    let outside = max(
        max(-projected_uv.x, projected_uv.x - 1.0),
        max(-projected_uv.y, projected_uv.y - 1.0),
    );
    if outside >= PLANAR_PROJECTION_GUARD {
        return PlanarReflectionSample(vec3(0.0), 0.0);
    }
    let dimensions = vec2<f32>(textureDimensions(reflection_a));
    let half_texel = 0.5 / dimensions;
    let projected_edge = min(
        min(projected_uv.x, projected_uv.y),
        min(1.0 - projected_uv.x, 1.0 - projected_uv.y),
    );
    // Preserve full reflection coverage: withdraw distortion near the edge instead
    // of moving an otherwise valid projected sample outside the render target.
    let distortion_guard = smoothstep(
        0.0,
        planar_reflections.distortion + max(half_texel.x, half_texel.y),
        projected_edge,
    );
    var uv = projected_uv
        + vec2(surface_normal.x, -surface_normal.z)
            * planar_reflections.distortion * distortion_guard;
    uv = clamp(uv, half_texel, vec2(1.0) - half_texel);
    var sample = textureSampleLevel(
        reflection_a,
        reflection_sampler,
        uv,
        0.0,
    );
    if index == 1u {
        sample = textureSampleLevel(
            reflection_b,
            reflection_sampler,
            uv,
            0.0,
        );
    }
    // Deferred HDR alpha is not a validity signal. In-bounds projected pixels
    // remain fully planar; only displaced projections beyond the target feather.
    let weight = 1.0 - smoothstep(0.0, PLANAR_PROJECTION_GUARD, max(outside, 0.0));
    return PlanarReflectionSample(sample.rgb, weight);
}

// Effective current for wave advection at the current invocation: the
// global uniform by default, the river's local flow inside bounded bodies
// that bake one. Vertex and fragment stages set it before sampling.
var<private> effective_flow: vec2<f32> = vec2(0.0, 0.0);
var<private> effective_time: f32 = 0.0;

/// Ripple-strength multiplier at the current fragment: 1.0 everywhere except
/// inside river bodies, where faster narrows read rougher and banks calm.
var<private> river_ripple_scale: f32 = 1.0;

/// Per-invocation body state, set at stage entry from the owning slot:
/// bounded flag, river flag, optics_a, optics_b. Defaults are inert.
var<private> invocation_bounded: f32 = 0.0;
var<private> invocation_river: f32 = 0.0;
var<private> invocation_optics_a: vec4<f32> = vec4(0.0);
var<private> invocation_optics_b: vec4<f32> = vec4(0.0);

/// Per-body water optics: extinction replaces the ocean Beer-Lambert
/// coefficients and the scatter endpoint scales down, so shallow fresh water
/// reads clear over its bed instead of ocean-teal.
var<private> body_extinction: vec3<f32> = vec3(0.0);
var<private> body_scatter_scale: f32 = 1.0;

/// Baked flow sample at the current fragment (xy: current m/s, z: signed
/// bank margin, w: channel half-width in metres).
var<private> fragment_river: vec4<f32> = vec4(0.0);


struct LocalLightSample {
    direction: vec3<f32>,
    radiance: vec3<f32>,
}

/// One snapshot of this invocation's river state, handed to imported
/// modules that cannot read this file's private globals directly.
struct RiverState {
    /// True when the fragment sits inside a bounded river body.
    enabled: bool,
    /// The baked flow sample: xy current m/s, z signed bank margin,
    /// w channel half-width in metres.
    sample: vec4<f32>,
    /// Effective advection current for wave-content sampling.
    flow: vec2<f32>,
}

/// Stage entry: records the owning body slot's parameters.
fn begin_invocation(bounded: bool, params: BodyParams) {
    invocation_bounded = select(0.0, 1.0, bounded);
    invocation_river = params.flags.y;
    invocation_optics_a = params.optics_a;
    invocation_optics_b = params.optics_b;
}

/// Stage entry: selects the advection current for wave-content sampling.
fn set_effective_flow(flow: vec2<f32>) {
    effective_flow = flow;
}

fn set_effective_time(time: f32) {
    effective_time = time;
}

/// Fragment entry: records the baked river-flow sample under the fragment.
fn set_fragment_river(sample: vec4<f32>) {
    fragment_river = sample;
}

/// Fragment entry: records the effective Beer-Lambert extinction and the
/// scatter-endpoint scale after fresh-water optics override.
fn set_body_optics(extinction: vec3<f32>, scatter_scale: f32) {
    body_extinction = extinction;
    body_scatter_scale = scatter_scale;
}

/// Fragment entry: records the river ripple-strength multiplier.
fn set_river_ripple(scale: f32) {
    river_ripple_scale = scale;
}

fn invocation_extinction() -> vec3<f32> {
    return body_extinction;
}

fn invocation_scatter_scale() -> f32 {
    return body_scatter_scale;
}

fn invocation_river_state() -> RiverState {
    return RiverState(
        invocation_bounded > 0.5 && invocation_river > 0.5,
        fragment_river,
        effective_flow,
    );
}

fn invocation_ripple() -> f32 {
    return river_ripple_scale;
}


fn advected_world(world_xz: vec2<f32>) -> vec2<f32> {
    return world_xz - effective_flow * effective_time;
}

/// Samples the baked river field; returns zero flow and a huge bank margin
/// when this material has no flow texture.
fn field_uv(world_xz: vec2<f32>) -> vec2<f32> {
    return (world_xz - field_params.region.xy)
        / max(field_params.region.zw, vec2(1e-4));
}

/// rg: surface level, one-based body slot (0 = unclaimed).
fn sample_field_level(world_xz: vec2<f32>) -> vec2<f32> {
    return textureSampleLevel(field_level_id, field_sampler, field_uv(world_xz), 0.0).xy;
}

/// rgb: flow m/s; z: signed bank margin in metres; w: speed m/s.
fn sample_field_flow(world_xz: vec2<f32>) -> vec4<f32> {
    return textureSampleLevel(field_flow, field_sampler, field_uv(world_xz), 0.0);
}

/// Parameters of the body owning a point; slot 0 falls back to the
/// inactive ocean defaults so unguarded reads stay harmless.
fn owning_body(slot: u32) -> BodyParams {
    if slot > 0u {
        return field_params.bodies[slot - 1u];
    }
    return field_params.bodies[0];
}

fn lod_count() -> u32 {
    return u32(cascade_layout.center.w);
}

fn smootherstep01(value: f32) -> f32 {
    let x = clamp(value, 0.0, 1.0);
    return x * x * x * (x * (x * 6.0 - 15.0) + 10.0);
}

fn far_tier_weight(base_world_position: vec3<f32>) -> f32 {
    let distance = length(base_world_position - view.world_position.xyz);
    return smootherstep01(
        (distance - surface.far_tier.x)
            / max(surface.far_tier.y - surface.far_tier.x, 1.0),
    );
}

fn lod_alpha(world_xz: vec2<f32>, cascade: CascadeParams) -> f32 {
    let offset = abs(world_xz - cascade_layout.center.xy);
    // Chebyshev distance matches the square LOD rings; Euclidean `length` is wrong here.
    let chebyshev_distance = max(offset.x, offset.y);
    let raw_alpha = chebyshev_distance / cascade.scale - LOD_TRANSITION_START;
    let black_point = MORPH_BLACK_POINT;
    let fade_width = 1.0 - MORPH_FADE_SIDES * black_point;
    return clamp((raw_alpha - black_point) / fade_width, 0.0, 1.0);
}

fn snap_and_transition(
    world_xz: vec2<f32>,
    object_xz: vec2<f32>,
    cascade: CascadeParams,
) -> vec3<f32> {
    let grid_width = cascade.texel_width;
    let snap_width = VERTEX_SNAP_MULTIPLIER * grid_width;
    var transitioned = world_xz - fract(object_xz / snap_width) * snap_width;
    let alpha = lod_alpha(transitioned, cascade);

    let coarse_grid = COARSE_GRID_MULTIPLIER * grid_width;
    let offset = fract(transitioned / coarse_grid) - vec2(GRID_CELL_CENTER);
    if abs(offset.x) < MORPH_INNER_RADIUS {
        transitioned.x += offset.x * alpha * coarse_grid;
    }
    if abs(offset.y) < MORPH_INNER_RADIUS {
        transitioned.y += offset.y * alpha * coarse_grid;
    }
    return vec3(transitioned, alpha);
}

fn world_to_uv(world_xz: vec2<f32>, cascade: CascadeParams) -> vec2<f32> {
    let coverage = cascade.texel_width * cascade.texture_res;
    return (world_xz - cascade.center) / coverage + vec2(UV_CENTER);
}

// Wave content advects at `surface.advection.xy` metres per second. A
// sampling-space shift is exact Doppler advection for both spectra:
// sampling at `x - u * t` turns every component's phase into
// `(k . x) - (omega + k . u) * t`. Snap/transition, depth lookups, and foam
// gating stay world-anchored.
fn sample_displacement(
    world_xz: vec2<f32>,
    lod: u32,
    alpha: f32,
) -> vec3<f32> {
    let sampled_xz = advected_world(world_xz);
    let smaller = cascade_layout.cascades[lod];
    let bigger = cascade_layout.cascades[lod + 1u];
    let smaller_weight = (1.0 - alpha) * smaller.weight;
    let bigger_weight = (1.0 - smaller_weight) * bigger.weight;
    var displacement = vec3(0.0);

    if smaller_weight > MIN_SAMPLE_WEIGHT {
        let uv = world_to_uv(sampled_xz, smaller);
        displacement += smaller_weight
            * textureSampleLevel(lod_data, lod_sampler, uv, i32(lod), 0.0).xyz;
    }
    if bigger_weight > MIN_SAMPLE_WEIGHT {
        let uv = world_to_uv(sampled_xz, bigger);
        displacement += bigger_weight
            * textureSampleLevel(lod_data, lod_sampler, uv, i32(lod + 1u), 0.0).xyz;
    }
    return displacement;
}

fn flow_frame(world_xz: vec2<f32>) -> vec2<f32> {
    let speed = length(effective_flow);
    if !(invocation_bounded > 0.5 && invocation_river > 0.5) || speed < 0.05 {
        return world_xz;
    }
    let dir = effective_flow / speed;
    let along = dot(dir, world_xz) / (1.0 + 0.55 * min(speed, 4.5));
    let across = dot(vec2(-dir.y, dir.x), world_xz);
    return vec2(along, across * 1.35);
}

fn capillary_resolved_weight(world_xz: vec2<f32>) -> f32 {
    let distance_to_view = length(world_xz - view.world_position.xz);
    return 1.0 - smoothstep(
        surface.capillary.z,
        surface.capillary.w,
        distance_to_view,
    );
}

// GodotOceanWaves `water.gdshader`: bounded GGX distribution and its Smith
// masking-shadowing approximation. Aqua applies Fresnel later in Crest's
// reflection composition, so this returns the remaining direct-sun factor.
fn godot_fresnel(view_alignment: f32) -> f32 {
    // Cubemap-only oceans preserve the accepted roughness-damped Godot curve.
    // Planar mode uses physical dielectric Schlick: roughness broadens the
    // reflected lobe but must not cap grazing-angle energy, or a bright sky
    // leaves distant water saturated navy. Calm authored bodies use the same
    // plain response in either reflection mode.
    let body_active = invocation_bounded > 0.5 && invocation_optics_a.w > 0.5;
    let sun_roughness = select(
        surface.sun.y,
        invocation_optics_b.y,
        body_active && invocation_optics_b.y >= 0.0,
    );
    let plain_schlick = planar_reflections.view_count > 0u
        || (body_active && invocation_optics_b.z > 0.5);
    let exponent = select(
        surface.fresnel.y * exp(-2.69 * sun_roughness),
        surface.fresnel.y,
        plain_schlick,
    );
    let damping = select(
        1.0 + 22.7 * pow(sun_roughness, 1.5),
        1.0,
        plain_schlick,
    );
    let rough = pow(max(0.0, 1.0 - view_alignment), exponent) / damping;
    return mix(rough, 1.0, surface.fresnel.x);
}