Search across packages, versions, files, declarations, and comments. Use multiple terms to narrow results (AND), and add @vertex, @fragment, or @compute to filter by shader stage.
1// Aqua cascade sampling boundary: ABI structs, material bindings, shared
2// constants, and the field/level/flow/depth sampling entry points. Feature
3// modules (#import-ed by the composed material) may only reach shared state
4// through this file; per-invocation private state is read/written here and
5// handed to feature modules as plain values or small snapshots.
6//
7// Reimplementation of the approach in Crest OceanVertHelpers.hlsl, OceanHelpersNew.hlsl, and Ocean.shader.
8// AnimWaves sampling follows Scripts/LodData/LodDataMgrAnimWaves.cs.
10#define_import_path aqua::cascade
12#import bevy_pbr::{
13 forward_io::Vertex,
14 mesh_functions,
15 mesh_view_types,
16 clustered_forward as clustering,
17 lighting,
18 prepass_utils,
19 shadows,
20 mesh_view_bindings::{light_probes, lights, view},
21 view_transformations::position_world_to_clip,
23#import bevy_pbr::mesh_view_bindings as view_bindings
25// The six-element array is fixed uniform ABI; active LOD count comes from the uniform.
26const CASCADE_COUNT: u32 = 6u;
27const VERTEX_SNAP_MULTIPLIER: f32 = 2.0;
28const COARSE_GRID_MULTIPLIER: f32 = 4.0;
29const LOD_TRANSITION_START: f32 = 1.0;
30// Crest 0.4 morph fade at eight vertices per 64-vertex tile.
31const MORPH_BLACK_POINT: f32 = 0.05;
32const MORPH_FADE_SIDES: f32 = 2.0;
33const MORPH_INNER_RADIUS: f32 = 0.375;
35const GRID_CELL_CENTER: f32 = 0.5;
36const UV_CENTER: f32 = 0.5;
37const MIN_SAMPLE_WEIGHT: f32 = 0.001;
38const MIN_NORMAL_Y: f32 = 0.0001;
39const SAFE_LENGTH_SQUARED: f32 = 1e-8;
40const LUMINANCE_EPSILON: f32 = 0.0001;
42const DEBUG_MODE_WATER_PATH: u32 = 1u;
43const DEBUG_MODE_REFRACTION_VALIDITY: u32 = 2u;
44const DEBUG_MODE_TRANSMISSION: u32 = 3u;
45const DEBUG_MODE_UNREFRACTED: u32 = 4u;
46const DEBUG_MODE_BEER_LAMBERT: u32 = 5u;
47const DEBUG_MODE_SEA_FLOOR: u32 = 6u;
48const DEBUG_MODE_BEAUTY: u32 = 7u;
49const DEBUG_MODE_REFLECTION: u32 = 8u;
50const DEBUG_MODE_FOAM: u32 = 9u;
51const DEBUG_MODE_WAVE_HEIGHT: u32 = 10u;
52const DEBUG_MODE_LIGHT_RADIANCE: u32 = 11u;
53const DEBUG_MODE_REFLECTION_FRACTION: u32 = 12u;
54const DEBUG_MODE_FAR_TIER: u32 = 13u;
56const CREST_SSS_MAXIMUM: f32 = 0.6;
57const CREST_SSS_RANGE: f32 = 0.12;
58const CREST_SSS_UNCOMPRESSED: f32 = CREST_SSS_MAXIMUM - CREST_SSS_RANGE;
60struct CascadeParams {
61 center: vec2<f32>,
62 scale: f32,
63 texture_res: f32,
64 inv_texture_res: f32,
65 texel_width: f32,
66 weight: f32,
67 max_wavelength: f32,
70struct CascadeLayout {
71 cascades: array<CascadeParams, CASCADE_COUNT>,
72 center: vec4<f32>,
73 // XY bed-map first-texel world origin, ZW inverse world extent.
74 bed_transform: vec4<f32>,
75 // X height minimum, Y height span (negative = no bed map), Z sea level.
76 bed_range: vec4<f32>,
79struct PlanarReflectionView {
80 view_projection: mat4x4<f32>,
81 level: f32,
84struct PlanarReflectionParams {
85 views: array<PlanarReflectionView, 2>,
86 view_count: u32,
87 distortion: f32,
90struct PlanarReflectionSample {
91 color: vec3<f32>,
92 weight: f32,
95// Allows displaced surface positions to feather just beyond the projected target.
96const PLANAR_PROJECTION_GUARD: f32 = 0.03;
98struct SurfaceParams {
99 deep_color: vec4<f32>,
100 grazing_color: vec4<f32>,
101 shallow_color: vec4<f32>,
102 fresnel: vec4<f32>,
103 reflection: vec4<f32>,
104 sun: vec4<f32>,
105 debug: vec4<f32>,
106 fog_density: vec4<f32>,
107 sea_floor: vec4<f32>,
108 sss_tint: vec4<f32>,
109 sss: vec4<f32>,
110 detail: vec4<f32>,
111 capillary: vec4<f32>,
112 foam: vec4<f32>,
113 advection: vec4<f32>,
114 /// x/y: configurable far-tier start/end distances in metres.
115 far_tier: vec4<f32>,
116 /// Strength, metres per cell, metres per second, and maximum depth in metres.
117 caustics: vec4<f32>,
120/// Localized-water extent controls; mirrors lod::BodyParams. flags.x is 1.0
121/// for bounded bodies: the vertex stage skips camera snap/morph and the
122/// fragment stage culls against extent.xy (centre) and extent.w (radius).
123struct BodyParams {
124 flags: vec4<f32>,
125 extent: vec4<f32>,
126 aabb_min: vec4<f32>,
127 aabb_size: vec4<f32>,
128 /// rgb: per-channel Beer-Lambert extinction in 1/m; w: optics enable.
129 optics_a: vec4<f32>,
130 /// x: scatter-endpoint scale (deep-pool darkness); yzw reserved.
131 optics_b: vec4<f32>,
134@group(#{MATERIAL_BIND_GROUP}) @binding(0) var lod_data: texture_2d_array<f32>;
135@group(#{MATERIAL_BIND_GROUP}) @binding(1) var lod_sampler: sampler;
136@group(#{MATERIAL_BIND_GROUP}) @binding(2) var<uniform> cascade_layout: CascadeLayout;
137@group(#{MATERIAL_BIND_GROUP}) @binding(3) var<uniform> surface: SurfaceParams;
138@group(#{MATERIAL_BIND_GROUP}) @binding(4) var bed_height: texture_2d<f32>;
139@group(#{MATERIAL_BIND_GROUP}) @binding(5) var detail_normal: texture_2d<f32>;
140@group(#{MATERIAL_BIND_GROUP}) @binding(6) var detail_sampler: sampler;
141@group(#{MATERIAL_BIND_GROUP}) @binding(7) var foam_data: texture_2d_array<f32>;
142@group(#{MATERIAL_BIND_GROUP}) @binding(8) var foam_sampler: sampler;
143@group(#{MATERIAL_BIND_GROUP}) @binding(9) var foam_pattern: texture_2d<f32>;
144@group(#{MATERIAL_BIND_GROUP}) @binding(10) var foam_pattern_sampler: sampler;
145@group(#{MATERIAL_BIND_GROUP}) @binding(11) var fft_surface: texture_2d_array<f32>;
146/// Global water fields: region mapping, per-slot body parameters, and the
147/// baked level/slot + flow textures. Mirrors fields::FieldParams.
148const MAX_BODIES: u32 = 16u;
150struct FieldParams {
151 /// xy: region minimum in metres; zw: region size in metres.
152 region: vec4<f32>,
153 /// x: bounded body count; y: 1.0 when the Ocean resource is present;
154 /// z: metres per texel; w: reserved.
155 info: vec4<f32>,
156 bodies: array<BodyParams, MAX_BODIES>,
159@group(#{MATERIAL_BIND_GROUP}) @binding(15) var<uniform> field_params: FieldParams;
160@group(#{MATERIAL_BIND_GROUP}) @binding(16) var field_maps: texture_2d_array<f32>;
161@group(#{MATERIAL_BIND_GROUP}) @binding(17) var field_sampler: sampler;
162@group(#{MATERIAL_BIND_GROUP}) @binding(19) var reflection_a: texture_2d<f32>;
163@group(#{MATERIAL_BIND_GROUP}) @binding(20) var reflection_sampler: sampler;
164@group(#{MATERIAL_BIND_GROUP}) @binding(21) var reflection_b: texture_2d<f32>;
165@group(#{MATERIAL_BIND_GROUP}) @binding(22) var<uniform> planar_reflections: PlanarReflectionParams;
167@group(#{MATERIAL_BIND_GROUP}) @binding(23) var caustics_texture: texture_2d<f32>;
168@group(#{MATERIAL_BIND_GROUP}) @binding(24) var caustics_sampler: sampler;
170fn sample_planar_reflection(
171 world_position: vec3<f32>,
172 surface_level: f32,
173 surface_normal: vec3<f32>,
174) -> PlanarReflectionSample {
175 if planar_reflections.view_count == 0u {
176 return PlanarReflectionSample(vec3(0.0), 0.0);
177 }
178 var index = 0u;
179 if planar_reflections.view_count > 1u
180 && abs(surface_level - planar_reflections.views[1].level)
181 < abs(surface_level - planar_reflections.views[0].level) {
182 index = 1u;
183 }
184 let view = planar_reflections.views[index];
185 let clip = view.view_projection * vec4(world_position, 1.0);
186 if clip.w <= 0.0 {
187 return PlanarReflectionSample(vec3(0.0), 0.0);
188 }
189 let ndc = clip.xy / clip.w;
190 let projected_uv = vec2(ndc.x, -ndc.y) * 0.5 + 0.5;
191 let outside = max(
192 max(-projected_uv.x, projected_uv.x - 1.0),
193 max(-projected_uv.y, projected_uv.y - 1.0),
194 );
195 if outside >= PLANAR_PROJECTION_GUARD {
196 return PlanarReflectionSample(vec3(0.0), 0.0);
197 }
198 let dimensions = vec2<f32>(textureDimensions(reflection_a));
199 let half_texel = 0.5 / dimensions;
200 let projected_edge = min(
201 min(projected_uv.x, projected_uv.y),
202 min(1.0 - projected_uv.x, 1.0 - projected_uv.y),
203 );
204 // Preserve full reflection coverage: withdraw distortion near the edge instead
205 // of moving an otherwise valid projected sample outside the render target.
206 let distortion_guard = smoothstep(
207 0.0,
208 planar_reflections.distortion + max(half_texel.x, half_texel.y),
209 projected_edge,
210 );
211 var uv = projected_uv
212 + vec2(surface_normal.x, -surface_normal.z)
213 * planar_reflections.distortion * distortion_guard;
214 uv = clamp(uv, half_texel, vec2(1.0) - half_texel);
215 var sample = textureSampleLevel(
216 reflection_a,
217 reflection_sampler,
218 uv,
219 0.0,
220 );
221 if index == 1u {
222 sample = textureSampleLevel(
223 reflection_b,
224 reflection_sampler,
225 uv,
226 0.0,
227 );
228 }
229 // Deferred HDR alpha is not a validity signal. In-bounds projected pixels
230 // remain fully planar; only displaced projections beyond the target feather.
231 let weight = 1.0 - smoothstep(0.0, PLANAR_PROJECTION_GUARD, max(outside, 0.0));
232 return PlanarReflectionSample(sample.rgb, weight);
235// Effective current for wave advection at the current invocation: the
236// global uniform by default, the river's local flow inside bounded bodies
237// that bake one. Vertex and fragment stages set it before sampling.
238var<private> effective_flow: vec2<f32> = vec2(0.0, 0.0);
239var<private> effective_time: f32 = 0.0;
240// Conservative world-XZ metres covered by one screen pixel; nonnegative.
241var<private> xz_footprint: f32 = 0.0;
243fn set_xz_footprint(value: f32) {
244 xz_footprint = value;
247fn screen_xz_footprint() -> f32 {
248 return xz_footprint;
251// Analytic texture LOD: dUV/dpixel is the world-XZ footprint divided by
252// metres per UV repeat, and mip LOD is log2(texels covered per pixel).
253fn screen_texture_lod(metres_per_repeat: f32, texture_width: u32) -> f32 {
254 let texels_per_pixel =
255 screen_xz_footprint() * f32(texture_width) / max(metres_per_repeat, 1e-6);
256 return max(log2(max(texels_per_pixel, 1.0)), 0.0);
259/// Ripple-strength multiplier at the current fragment: 1.0 everywhere except
260/// inside river bodies, where faster narrows read rougher and banks calm.
261var<private> river_ripple_scale: f32 = 1.0;
263/// Per-invocation body state, set at stage entry from the owning slot:
264/// bounded flag, river flag, optics_a, optics_b. Defaults are inert.
265var<private> invocation_bounded: f32 = 0.0;
266var<private> invocation_river: f32 = 0.0;
267var<private> invocation_optics_a: vec4<f32> = vec4(0.0);
268var<private> invocation_optics_b: vec4<f32> = vec4(0.0);
270/// Per-body water optics: extinction replaces the ocean Beer-Lambert
271/// coefficients and the scatter endpoint scales down, so shallow fresh water
272/// reads clear over its bed instead of ocean-teal.
273var<private> body_extinction: vec3<f32> = vec3(0.0);
274var<private> body_scatter_scale: f32 = 1.0;
276/// Baked flow sample at the current fragment (xy: current m/s, z: signed
277/// bank margin, w: channel half-width in metres).
278var<private> fragment_river: vec4<f32> = vec4(0.0);
281struct LocalLightSample {
282 direction: vec3<f32>,
283 radiance: vec3<f32>,
286/// One snapshot of this invocation's river state, handed to imported
287/// modules that cannot read this file's private globals directly.
288struct RiverState {
289 /// True when the fragment sits inside a bounded river body.
290 enabled: bool,
291 /// The baked flow sample: xy current m/s, z signed bank margin,
292 /// w channel half-width in metres.
293 sample: vec4<f32>,
294 /// Effective advection current for wave-content sampling.
295 flow: vec2<f32>,
298/// Stage entry: records the owning body slot's parameters.
299fn begin_invocation(bounded: bool, params: BodyParams) {
300 invocation_bounded = select(0.0, 1.0, bounded);
301 invocation_river = params.flags.y;
302 invocation_optics_a = params.optics_a;
303 invocation_optics_b = params.optics_b;
306/// Stage entry: selects the advection current for wave-content sampling.
307fn set_effective_flow(flow: vec2<f32>) {
308 effective_flow = flow;
311fn set_effective_time(time: f32) {
312 effective_time = time;
315/// Fragment entry: records the baked river-flow sample under the fragment.
316fn set_fragment_river(sample: vec4<f32>) {
317 fragment_river = sample;
320/// Fragment entry: records the effective Beer-Lambert extinction and the
321/// scatter-endpoint scale after fresh-water optics override.
322fn set_body_optics(extinction: vec3<f32>, scatter_scale: f32) {
323 body_extinction = extinction;
324 body_scatter_scale = scatter_scale;
327/// Fragment entry: records the river ripple-strength multiplier.
328fn set_river_ripple(scale: f32) {
329 river_ripple_scale = scale;
332fn invocation_extinction() -> vec3<f32> {
333 return body_extinction;
336fn invocation_scatter_scale() -> f32 {
337 return body_scatter_scale;
340fn invocation_river_state() -> RiverState {
341 return RiverState(
342 invocation_bounded > 0.5 && invocation_river > 0.5,
343 fragment_river,
344 effective_flow,
345 );
348fn invocation_ripple() -> f32 {
349 return river_ripple_scale;
353fn advected_world(world_xz: vec2<f32>) -> vec2<f32> {
354 return world_xz - effective_flow * effective_time;
357/// Samples the baked river field; returns zero flow and a huge bank margin
358/// when this material has no flow texture.
359fn field_uv(world_xz: vec2<f32>) -> vec2<f32> {
360 return (world_xz - field_params.region.xy)
361 / max(field_params.region.zw, vec2(1e-4));
364/// rg: surface level, one-based body slot (0 = unclaimed).
365fn sample_field_level(world_xz: vec2<f32>) -> vec2<f32> {
366 return textureSampleLevel(field_maps, field_sampler, field_uv(world_xz), 0, 0.0).xy;
369/// rgb: flow m/s; z: signed bank margin in metres; w: speed m/s.
370fn sample_field_flow(world_xz: vec2<f32>) -> vec4<f32> {
371 return textureSampleLevel(field_maps, field_sampler, field_uv(world_xz), 1, 0.0);
374/// Parameters of the body owning a point; slot 0 falls back to the
375/// inactive ocean defaults so unguarded reads stay harmless.
376fn owning_body(slot: u32) -> BodyParams {
377 if slot > 0u {
378 return field_params.bodies[slot - 1u];
379 }
380 return field_params.bodies[0];
383fn lod_count() -> u32 {
384 return u32(cascade_layout.center.w);
387fn smootherstep01(value: f32) -> f32 {
388 let x = clamp(value, 0.0, 1.0);
389 return x * x * x * (x * (x * 6.0 - 15.0) + 10.0);
392fn far_tier_weight(base_world_position: vec3<f32>) -> f32 {
393 let distance = length(base_world_position - view.world_position.xyz);
394 return smootherstep01(
395 (distance - surface.far_tier.x)
396 / max(surface.far_tier.y - surface.far_tier.x, 1.0),
397 );
400fn lod_alpha(world_xz: vec2<f32>, cascade: CascadeParams) -> f32 {
401 let offset = abs(world_xz - cascade_layout.center.xy);
402 // Chebyshev distance matches the square LOD rings; Euclidean `length` is wrong here.
403 let chebyshev_distance = max(offset.x, offset.y);
404 let raw_alpha = chebyshev_distance / cascade.scale - LOD_TRANSITION_START;
405 let black_point = MORPH_BLACK_POINT;
406 let fade_width = 1.0 - MORPH_FADE_SIDES * black_point;
407 return clamp((raw_alpha - black_point) / fade_width, 0.0, 1.0);
410fn snap_and_transition(
411 world_xz: vec2<f32>,
412 object_xz: vec2<f32>,
413 cascade: CascadeParams,
414) -> vec3<f32> {
415 let grid_width = cascade.texel_width;
416 let snap_width = VERTEX_SNAP_MULTIPLIER * grid_width;
417 var transitioned = world_xz - fract(object_xz / snap_width) * snap_width;
418 let alpha = lod_alpha(transitioned, cascade);
420 let coarse_grid = COARSE_GRID_MULTIPLIER * grid_width;
421 let offset = fract(transitioned / coarse_grid) - vec2(GRID_CELL_CENTER);
422 if abs(offset.x) < MORPH_INNER_RADIUS {
423 transitioned.x += offset.x * alpha * coarse_grid;
424 }
425 if abs(offset.y) < MORPH_INNER_RADIUS {
426 transitioned.y += offset.y * alpha * coarse_grid;
427 }
428 return vec3(transitioned, alpha);
431fn world_to_uv(world_xz: vec2<f32>, cascade: CascadeParams) -> vec2<f32> {
432 let coverage = cascade.texel_width * cascade.texture_res;
433 return (world_xz - cascade.center) / coverage + vec2(UV_CENTER);
436// Wave content advects at `surface.advection.xy` metres per second. A
437// sampling-space shift is exact Doppler advection for both spectra:
438// sampling at `x - u * t` turns every component's phase into
439// `(k . x) - (omega + k . u) * t`. Snap/transition, depth lookups, and foam
440// gating stay world-anchored.
441fn sample_displacement(
442 world_xz: vec2<f32>,
443 lod: u32,
444 alpha: f32,
445) -> vec3<f32> {
446 let sampled_xz = advected_world(world_xz);
447 let smaller = cascade_layout.cascades[lod];
448 let bigger = cascade_layout.cascades[lod + 1u];
449 let smaller_weight = (1.0 - alpha) * smaller.weight;
450 let bigger_weight = (1.0 - smaller_weight) * bigger.weight;
451 var displacement = vec3(0.0);
453 if smaller_weight > MIN_SAMPLE_WEIGHT {
454 let uv = world_to_uv(sampled_xz, smaller);
455 displacement += smaller_weight
456 * textureSampleLevel(lod_data, lod_sampler, uv, i32(lod), 0.0).xyz;
457 }
458 if bigger_weight > MIN_SAMPLE_WEIGHT {
459 let uv = world_to_uv(sampled_xz, bigger);
460 displacement += bigger_weight
461 * textureSampleLevel(lod_data, lod_sampler, uv, i32(lod + 1u), 0.0).xyz;
462 }
463 return displacement;
466fn flow_frame(world_xz: vec2<f32>) -> vec2<f32> {
467 let speed = length(effective_flow);
468 if !(invocation_bounded > 0.5 && invocation_river > 0.5) || speed < 0.05 {
469 return world_xz;
470 }
471 let dir = effective_flow / speed;
472 let along = dot(dir, world_xz) / (1.0 + 0.55 * min(speed, 4.5));
473 let across = dot(vec2(-dir.y, dir.x), world_xz);
474 return vec2(along, across * 1.35);
477fn capillary_resolved_weight(world_xz: vec2<f32>) -> f32 {
478 let distance_to_view = length(world_xz - view.world_position.xz);
479 return 1.0 - smoothstep(
480 surface.capillary.z,
481 surface.capillary.w,
482 distance_to_view,
483 );
486// GodotOceanWaves `water.gdshader`: bounded GGX distribution and its Smith
487// masking-shadowing approximation. Aqua applies Fresnel later in Crest's
488// reflection composition, so this returns the remaining direct-sun factor.
489fn godot_fresnel(view_alignment: f32) -> f32 {
490 // Cubemap-only oceans preserve the accepted roughness-damped Godot curve.
491 // Planar mode uses physical dielectric Schlick: roughness broadens the
492 // reflected lobe but must not cap grazing-angle energy, or a bright sky
493 // leaves distant water saturated navy. Calm authored bodies use the same
494 // plain response in either reflection mode.
495 let body_active = invocation_bounded > 0.5 && invocation_optics_a.w > 0.5;
496 let sun_roughness = select(
497 surface.sun.y,
498 invocation_optics_b.y,
499 body_active && invocation_optics_b.y >= 0.0,
500 );
501 let plain_schlick = planar_reflections.view_count > 0u
502 || (body_active && invocation_optics_b.z > 0.5);
503 let exponent = select(
504 surface.fresnel.y * exp(-2.69 * sun_roughness),
505 surface.fresnel.y,
506 plain_schlick,
507 );
508 let damping = select(
509 1.0 + 22.7 * pow(sun_roughness, 1.5),
510 1.0,
511 plain_schlick,
512 );
513 let rough = pow(max(0.0, 1.0 - view_alignment), exponent) / damping;
514 return mix(rough, 1.0, surface.fresnel.x);
Contents