bevy_elements 0.7.6

Living environments for Bevy: sun, moon and stars, climate-driven weather, GPU wind, volumetric clouds, rain, snow and fog, lightning, terrain with hydraulic and thermal erosion, flooding, snow cover, wetness and dust — simulated in compute shaders.
Documentation
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
// Water rendering: the CDLOD patch pool displaced to bedrock + sediment + water, and two blended
// passes of one fragment shader (docs/research/water-rendering.md, "Model adopted"):
//
//   absorb   (`AlphaMode::Multiply`)  dst ← dst · lerp(1, X, a),  X = (1 − F) · T(t_eff) · (1 − foam)
//   surface  (`AlphaMode::Add`)       dst ← dst + a · (F · L_refl + (1 − F) · L_body + foam · L_foam)
//
// Bevy sets `BLEND_MULTIPLY` for the first. The vertex entry point displaces exactly like the
// terrain's (`terrain/render.wesl`): same morph, same packed `terrain_cells`, plus the water depth
// and a 1 mm lift. The thickness comes from the depth prepass, which holds the opaque scene only.

import bevy_elements::grid::{GridParams, grid_taps};
import bevy_elements::noise::perlin3;
import bevy_elements::terrain::common::{TerrainParams, last_sample, morph_centre};
import bevy_pbr::render::{
    mesh_functions,
    mesh_view_bindings::{view, globals},
    pbr_types,
    view_transformations::{position_world_to_clip, position_world_to_view, depth_ndc_to_view_z},
    pbr_fragment::pbr_input_from_standard_material,
    pbr_functions::{apply_pbr_lighting, main_pass_post_lighting_processing},
    forward_io::{VertexOutput, FragmentOutput},
};
import bevy_pbr::prepass::utils::prepass_depth;

struct WaterParams {
    terrain: TerrainParams,
    // Per level: x morph start, y range (m).
    lod: array<vec4<f32>, 16>,
    // x: coarsest level.
    top: vec4<f32>,
    // What the vertices morph about: xyz a metre-frame point, w 1 to use it (an orthographic
    // followed camera's centre), 0 for the view's own `lod_view_world_position`.
    lod_centre: vec4<f32>,
    // xy: cloud shadow map origin (metre-frame XZ), z: its extent, w: the lights' mean transmittance.
    shadow: vec4<f32>,
    // xyz: unit direction toward the light the map was made for. w: 1 when a map is bound.
    light: vec4<f32>,
    // xyz: absorption σ (1/m). w: index of refraction n.
    absorption: vec4<f32>,
    // xyz: silt tint (linear). w: κ, scattering per unit volume fraction of sediment (1/m).
    silt: vec4<f32>,
    // xyz: deep-water body reflectance (linear). w: σ_clear, the scattering of clear water (1/m).
    deep: vec4<f32>,
    // x: flow cycle T (s). y: normal-map tile L (m). z: distortion limit D (tiles).
    // w: ripple slope relative to the normal map's rms slope.
    flow: vec4<f32>,
    // x: foam speed threshold (m/s). y: foam thickness threshold (m). z: foam albedo.
    // w: foam perceptual roughness.
    foam: vec4<f32>,
    // x: the finest wave cascade's texel (m). y: texels a side. z: 1 when a field is bound.
    waves: vec4<f32>,
    // x: the still water level (metre frame, above the terrain's origin; -1e9 for none).
    still: vec4<f32>,
    // World units to the metre (`ElementsUnits`): the shader divides the positions Bevy gives it
    // by this and multiplies the ones it hands back.
    units: f32,
}

@group(constants::MATERIAL_BIND_GROUP) @binding(100) var<uniform> water: WaterParams;
@group(constants::MATERIAL_BIND_GROUP) @binding(101) var<storage, read> terrain_cells: array<vec4<f32>>;
@group(constants::MATERIAL_BIND_GROUP) @binding(102) var<storage, read> flow_cells: array<vec4<f32>>;
@group(constants::MATERIAL_BIND_GROUP) @binding(103) var normal_map: texture_2d<f32>;
@group(constants::MATERIAL_BIND_GROUP) @binding(104) var normal_sampler: sampler;
@group(constants::MATERIAL_BIND_GROUP) @binding(105) var cloud_map: texture_2d<f32>;
@group(constants::MATERIAL_BIND_GROUP) @binding(106) var cloud_sampler: sampler;
@group(constants::MATERIAL_BIND_GROUP) @binding(107) var wave_map: texture_2d_array<f32>;
@group(constants::MATERIAL_BIND_GROUP) @binding(108) var wave_sampler: sampler;

const WAVE_CASCADES: i32 = 3;
// The lowest elevation (sine) a reflection may take: the horizon.
const HORIZON_Y: f32 = 0.03;

// 1 mm: the water vertex sits this far above the surface so it never z-fights the ground where
// the fragment discard (3 mm) has not yet removed it.
const LIFT: f32 = 0.001;
const DISCARD_DEPTH: f32 = 0.003;
const FADE_DEPTH: f32 = 0.03;
const FRESNEL_F0: f32 = 0.02;
// Half widths of the foam ramps around the thresholds: ±10 % of the speed, ±0.03 m of thickness.
const FOAM_SPEED_RAMP: f32 = 0.1;
const FOAM_THICKNESS_RAMP: f32 = 0.03;
// Offset of the second flow layer, in tiles (Vlachos 2010: layers are offset against repetition).
const LAYER_OFFSET: vec2<f32> = vec2<f32>(0.37, 0.59);
const PHASE_NOISE_M: f32 = 37.0;
/// The slope a body with no flow keeps: a sheltered pond is a mirror with a skin on it, not a
/// wind sea (`docs/research/water-surface.md`, Model 3). A floor of zero would make still water
/// read as glass, with no surface at all.
const STILL_RIPPLE: f32 = 0.06;
const MAX_THICKNESS: f32 = 1000.0;
// Under a still sea the terrain's border is a hard edge in the bed seen through the water. A bed
// point within this many metres of it is blended into the deep colour the skirt shows past it: its
// transmittance is scaled by a smoothstep of the distance and the body's alpha rises to 1
// (`docs/research/still-water.md`, Deviation 7).
const EDGE_BAND_M: f32 = 32.0;

struct WaterVertex {
    @builtin(instance_index) instance_index: u32,
    @location(0) position: vec3<f32>,
}

fn terrain_height(xz: vec2<f32>) -> f32 {
    let t = grid_taps(water.terrain.grid, xz);
    let a = mix(terrain_cells[t.i00].x, terrain_cells[t.i10].x, t.w.x);
    let b = mix(terrain_cells[t.i01].x, terrain_cells[t.i11].x, t.w.x);
    return mix(a, b, t.w.y);
}

// The water depth at world XZ: the simulated depth, or the still level over the ground where that
// is deeper (the surface is the higher of the two).
fn water_depth_at(xz: vec2<f32>) -> f32 {
    let t = grid_taps(water.terrain.grid, xz);
    let a = mix(terrain_cells[t.i00].z, terrain_cells[t.i10].z, t.w.x);
    let b = mix(terrain_cells[t.i01].z, terrain_cells[t.i11].z, t.w.x);
    let sim = mix(a, b, t.w.y);
    return max(sim, water.still.x - terrain_height(xz));
}

fn morph_k(distance: f32, lod: u32) -> f32 {
    if f32(lod) >= water.top.x {
        return 0.0;
    }
    let range = water.lod[lod];
    return clamp((distance - range.x) / (range.y - range.x), 0.0, 1.0);
}

// A tag at or above this value marks a skirt tile (the sea past the terrain grid, `water.rs`): its
// 33 × 33 mesh is placed by the transform's scale, the grid's taps clamp to the edge sample, and
// nothing morphs.
const SKIRT_TAG: u32 = 1000u;

// The terrain's CDLOD vertex with the morph measured against the terrain surface (so the water
// morphs with the ground under it), lifted by the water depth. In the metre frame: the instance
// matrix's translation (world units) is divided by `units`.
fn water_vertex_position(local: vec3<f32>, instance_index: u32) -> vec4<f32> {
    let m = mesh_functions::get_world_from_local(instance_index);
    let lod = mesh_functions::get_tag(instance_index);
    let origin = vec3<f32>(m[3].x, m[3].y, m[3].z) / water.units;
    if lod >= SKIRT_TAG {
        let size = vec2<f32>(m[0].x, m[2].z) / water.units;
        let sxz = origin.xz + local.xz * size;
        return vec4<f32>(sxz.x, origin.y + terrain_height(sxz) + water_depth_at(sxz) + LIFT, sxz.y, 1.0);
    }
    let gi = round(local.xz * 32.0);
    let step = water.terrain.grid.cell * f32(1u << lod);
    var wxz = origin.xz + gi * step;
    let centre = morph_centre(view.lod_view_world_position, water.lod_centre, water.units);
    let d = distance(centre, vec3<f32>(wxz.x, origin.y + terrain_height(wxz), wxz.y));
    let odd = gi - 2.0 * floor(gi * 0.5);
    wxz = min(wxz - odd * step * morph_k(d, lod), last_sample(water.terrain.grid));
    return vec4<f32>(wxz.x, origin.y + terrain_height(wxz) + water_depth_at(wxz) + LIFT, wxz.y, 1.0);
}

@vertex
fn vertex(v: WaterVertex) -> VertexOutput {
    var out: VertexOutput;
    let world = vec4<f32>(water_vertex_position(v.position, v.instance_index).xyz * water.units, 1.0);
    out.world_position = world;
    out.position = position_world_to_clip(world.xyz);
    out.world_normal = vec3<f32>(0.0, 1.0, 0.0);
    @if(VERTEX_OUTPUT_INSTANCE_INDEX)
    out.instance_index = v.instance_index;
    return out;
}

// ---- The pieces the CPU mirrors in `water.rs` -------------------------------------------------

// Beer–Lambert transmittance of `t` metres of water, per channel.
fn beer_lambert(sigma: vec3<f32>, t: f32) -> vec3<f32> {
    return exp(-sigma * t);
}

// Schlick's Fresnel reflectance of water at `cos_v`.
fn fresnel(cos_v: f32) -> f32 {
    let k = 1.0 - clamp(cos_v, 0.0, 1.0);
    let k2 = k * k;
    return FRESNEL_F0 + (1.0 - FRESNEL_F0) * k2 * k2 * k;
}

// Thickness along the refracted ray under a flat surface from the thickness along the view ray:
// `t_ray · cos θ_v / cos θ_r` (Snell's law).
fn refracted_thickness(t_ray: f32, cos_v: f32, n: f32) -> f32 {
    let cos_r = sqrt(max(1.0 - (1.0 - cos_v * cos_v) / (n * n), 1e-4));
    return t_ray * cos_v / cos_r;
}

// Vlachos's two-phase weights for the phase `p` of a layer: a triangle that is 0 at the restart.
fn phase_weight(p: f32) -> f32 {
    return 1.0 - abs(2.0 * p - 1.0);
}

fn foam_from_speed(speed: f32) -> f32 {
    let thr = water.foam.x;
    return smoothstep(thr * (1.0 - FOAM_SPEED_RAMP), thr * (1.0 + FOAM_SPEED_RAMP), speed);
}

fn foam_from_thickness(t_ray: f32) -> f32 {
    let thr = water.foam.y;
    return 1.0 - smoothstep(thr - FOAM_THICKNESS_RAMP, thr + FOAM_THICKNESS_RAMP, t_ray);
}

fn edge_alpha(depth: f32) -> f32 {
    return smoothstep(DISCARD_DEPTH, FADE_DEPTH, depth);
}

// How much of the bed point `scene` (metre frame) is seen, near the border of the terrain grid:
// 1 away from it, 0 at it. 1 without a still level.
fn edge_share(scene: vec2<f32>) -> f32 {
    if water.still.x < -1.0e8 {
        return 1.0;
    }
    let grid = water.terrain.grid;
    let lo = grid.origin + 0.5 * grid.cell;
    let hi = last_sample(grid);
    let d = min(min(scene.x - lo.x, hi.x - scene.x), min(scene.y - lo.y, hi.y - scene.y));
    return smoothstep(0.0, EDGE_BAND_M, d);
}

// Cloud shadow scale ρ = (0.25 + 0.75 T_px) / (0.25 + 0.75 T̄) at a world position, as the
// terrain's fragment computes it.
fn cloud_shadow_ratio(p: vec3<f32>) -> f32 {
    let l = water.light.xyz;
    var t = 1.0;
    if water.light.w > 0.5 && l.y > 0.1 {
        let uv = (p.xz - l.xz * (p.y / l.y) - water.shadow.xy) / water.shadow.z;
        if all(uv >= vec2<f32>(0.0)) && all(uv <= vec2<f32>(1.0)) {
            t = textureSampleLevel(cloud_map, cloud_sampler, uv, 0.0).r;
        }
    }
    let here = 0.25 + 0.75 * t;
    let lights = max(0.25 + 0.75 * water.shadow.w, 0.01);
    return clamp(here / lights, 0.0, 4.0);
}

struct Flow {
    // The ripple normal (world, y up) and the foam patchiness source (the map's alpha).
    normal: vec3<f32>,
    patch_noise: f32,
}

// Two-phase flow-mapped normals (Vlachos 2010): two layers of the tiling normal map, each advected
// along the velocity by up to D tiles over a cycle, half a cycle apart and weighted by a triangle
// so that a layer is invisible when it restarts.
fn flow_normal(p: vec2<f32>, velocity: vec2<f32>, gx: vec2<f32>, gy: vec2<f32>) -> Flow {
    let cycle = water.flow.x;
    let tile = water.flow.y;
    let limit = water.flow.z;
    let uv = p / tile;
    var offset = velocity * cycle / tile;
    let len = length(offset);
    if len > limit {
        offset = offset * (limit / len);
    }
    let psi = 0.5 + 0.5 * perlin3(vec3<f32>(p / PHASE_NOISE_M, 0.5));
    let phase = globals.time / cycle + psi;
    let p0 = fract(phase);
    let p1 = fract(phase + 0.5);
    let w0 = phase_weight(p0);
    let w1 = phase_weight(p1);
    let s0 = textureSampleGrad(normal_map, normal_sampler, uv - offset * p0, gx, gy);
    let s1 = textureSampleGrad(normal_map, normal_sampler, uv - offset * p1 + LAYER_OFFSET, gx, gy);
    let n0 = s0.xyz * 2.0 - 1.0;
    let n1 = s1.xyz * 2.0 - 1.0;
    let slope = w0 * (n0.xy / max(n0.z, 0.1)) + w1 * (n1.xy / max(n1.z, 0.1));
    let strength = water.flow.w * mix(STILL_RIPPLE, 1.0, saturate(length(velocity) / water.foam.x));
    var out: Flow;
    out.normal = normalize(vec3<f32>(slope.x * strength, 1.0, slope.y * strength));
    out.patch_noise = w0 * s0.a + w1 * s1.a;
    return out;
}

struct Column {
    depth: f32,
    velocity: vec2<f32>,
    turbidity: f32,
}

// The water column at a world XZ: four bilinear taps of `terrain_cells` (depth) and `flow_cells`
// (velocity, turbidity). The velocity is faded in with the depth: it is noisy where the film is
// a few millimetres deep.
fn column_at(xz: vec2<f32>) -> Column {
    let t = grid_taps(water.terrain.grid, xz);
    var c: Column;
    let bed = mix(
        mix(terrain_cells[t.i00].x, terrain_cells[t.i10].x, t.w.x),
        mix(terrain_cells[t.i01].x, terrain_cells[t.i11].x, t.w.x),
        t.w.y,
    );
    c.depth = max(
        mix(
            mix(terrain_cells[t.i00].z, terrain_cells[t.i10].z, t.w.x),
            mix(terrain_cells[t.i01].z, terrain_cells[t.i11].z, t.w.x),
            t.w.y,
        ),
        water.still.x - bed,
    );
    let f00 = flow_cells[t.i00];
    let f10 = flow_cells[t.i10];
    let f01 = flow_cells[t.i01];
    let f11 = flow_cells[t.i11];
    let f = mix(mix(f00, f10, t.w.x), mix(f01, f11, t.w.x), t.w.y);
    c.velocity = f.xy * smoothstep(DISCARD_DEPTH, FADE_DEPTH, c.depth);
    c.turbidity = max(f.z, 0.0);
    return c;
}

fn silt_body(turbidity: f32) -> vec3<f32> {
    let kappa = water.silt.w;
    let sigma_c = kappa * turbidity;
    let tau = sigma_c / (sigma_c + water.deep.w);
    return mix(water.deep.xyz, water.silt.xyz, tau);
}

// The wave field at world XZ (`waves.wesl`): the cascades' gradient summed and their breaking
// foam (the largest), each faded out over the last 10 % of its window (inside the sponge) and
// where one of its texels is smaller than about a pixel (the field has no mips; Crest fades its
// cascades the same way).
fn wave_field(xz: vec2<f32>, footprint: f32) -> vec3<f32> {
    if water.waves.z < 0.5 {
        return vec3<f32>(0.0);
    }
    let n = water.waves.y;
    var out = vec3<f32>(0.0);
    for (var c = 0; c < WAVE_CASCADES; c += 1) {
        let t = water.waves.x * pow(4.0, f32(c));
        let extent = t * n;
        // The window spans N texels from `floor(camera / t) − N/2`.
        let middle = floor(view.world_position.xz / water.units / t) * t;
        let reach = abs(xz - middle) / (0.5 * extent);
        let inside = 1.0 - smoothstep(0.85, 0.93, max(reach.x, reach.y));
        let detail = 1.0 - smoothstep(0.6, 1.2, footprint / t);
        let weight = inside * detail;
        if weight > 0.0 {
            let s = textureSampleLevel(wave_map, wave_sampler, xz / extent, c, 0.0);
            out = vec3<f32>(out.xy + weight * s.yz, max(out.z, weight * s.w));
        }
    }
    return out;
}

@fragment
fn fragment(in: VertexOutput, @builtin(front_facing) is_front: bool) -> FragmentOutput {
    let world_p = in.world_position.xyz;
    let p = world_p / water.units;
    // Derivatives of the tile coordinate, taken before the discard (uniform control flow).
    let gx = dpdx(p.xz / water.flow.y);
    let gy = dpdy(p.xz / water.flow.y);
    // The world size of this pixel on the water, for the wave cascades' level of detail.
    let footprint = max(length(dpdx(p.xz)), length(dpdy(p.xz)));
    let column = column_at(p.xz);
    if column.depth < DISCARD_DEPTH {
        discard;
    }
    let a_edge = edge_alpha(column.depth);

    let flow = flow_normal(p.xz, column.velocity, gx, gy);
    // The flow-mapped ripples' slope plus the wave field's: both small-slope height fields.
    let ripple = flow.normal.xz / flow.normal.y;
    let waves = wave_field(p.xz, footprint);
    let flat_n = normalize(vec3<f32>(ripple.x - waves.x, 1.0, ripple.y - waves.y));
    let v_dir = normalize(view.world_position / water.units - p);
    // A face steep enough to reflect below the horizon sees, in a real scene, the water and the
    // far shore; the environment map holds only Bevy's ground disc there (tan, with a hard edge
    // at the horizon). Hold its reflection at the horizon: the normal becomes the half vector of
    // the view and the reflection lifted to it.
    let reflected = reflect(-v_dir, flat_n);
    let n = select(
        flat_n,
        normalize(v_dir + normalize(vec3<f32>(reflected.x, HORIZON_Y, reflected.z))),
        reflected.y < HORIZON_Y,
    );
    let cos_v = max(dot(v_dir, n), 0.02);
    let f = fresnel(cos_v);

    // Thickness from the depth prepass: the opaque scene under the water along the view ray.
    let view_pos = position_world_to_view(world_p);
    let ray = normalize(view_pos);
    let z_scene = depth_ndc_to_view_z(max(prepass_depth(in.position, 0u), 1e-9));
    let t_ray = clamp((view_pos.z - z_scene) / max(-ray.z, 1e-3) / water.units, 0.0, MAX_THICKNESS);
    // The bed point this pixel shows: the same view ray, scaled to the scene's depth.
    let eye = view.world_position / water.units;
    let scene_xz = (eye + (p - eye) * (z_scene / view_pos.z)).xz;
    let t_eff = refracted_thickness(t_ray, cos_v, water.absorption.w);
    let bed_seen = edge_share(scene_xz);

    // Foam: fast flow, the contact with anything the water meets (shoreline, rocks, posts), and
    // the waves that break.
    let blotch = smoothstep(0.45, 0.75, flow.patch_noise);
    let f_speed = foam_from_speed(length(column.velocity)) * blotch;
    let f_contact = foam_from_thickness(t_ray) * mix(1.0, blotch, smoothstep(0.02, 0.03, t_ray));
    // Breaking foam dissolves into noise as it fades (lace, not a blur): its coverage is the share
    // of a fine fbm (the normal map's alpha at a fifth of the tile, with the coarse patchiness)
    // above a threshold that falls with the foam, never to zero (fresh foam is still bubbles and
    // gaps), with soft edges.
    let foam_cover = saturate(waves.z);
    let lace = 0.5 * textureSampleGrad(normal_map, normal_sampler, p.xz / (0.2 * water.flow.y), gx * 5.0, gy * 5.0).a
        + 0.5 * textureSampleGrad(normal_map, normal_sampler, p.xz / (0.05 * water.flow.y), gx * 20.0, gy * 20.0).a;
    let threshold = 1.0 - 0.65 * foam_cover;
    let f_breaking = sqrt(foam_cover)
        * smoothstep(threshold - 0.12, threshold + 0.15, 0.3 * flow.patch_noise + 0.7 * lace);
    let foam = max(max(f_speed, f_contact), f_breaking);

    // What the absorb pass leaves of the scene behind the water, per channel.
    let x = (1.0 - f) * beer_lambert(water.absorption.xyz, t_eff) * bed_seen * (1.0 - foam);
@if(BLEND_MULTIPLY) {
    var out: FragmentOutput;
    // Premultiplied by the edge alpha, written directly: a transmittance is not fogged or tonemapped.
    out.color = vec4<f32>(x * a_edge, a_edge);
    return out;
} @else {
    var pbr_input = pbr_input_from_standard_material(in, is_front);
    let sigma_s = water.deep.w + water.silt.w * column.turbidity;
    let alpha_body = mix(1.0, 1.0 - exp(-sigma_s * t_eff), bed_seen);
    let body = silt_body(column.turbidity) * alpha_body;
    let foam_albedo = vec3<f32>(water.foam.z);
    pbr_input.material.base_color = vec4<f32>(mix(body, foam_albedo, foam), 1.0);
    pbr_input.material.perceptual_roughness = mix(pbr_input.material.perceptual_roughness, water.foam.w, foam);
    pbr_input.material.metallic = 0.0;
    pbr_input.N = n;
    pbr_input.world_normal = n;
    var out: FragmentOutput;
    let lit = apply_pbr_lighting(pbr_input);
    let film = vec4<f32>(lit.rgb * cloud_shadow_ratio(p), a_edge);
    // A skirt tile has no bed behind it (the absorb pass left black), so it takes the camera's fog
    // itself. Over the grid the bed was fogged before the absorb pass multiplied it, so that
    // multiply also tinted the fog in front of the water (a fogged sea turned teal against a grey
    // sky). Fog sits in front of the surface, so the film is fogged here and the share of the
    // fog the absorb pass removed is put back: with `x` the pass's transmittance and `Lf` the fog
    // radiance over this pixel, the bed's fog `Lf` came out as `x · Lf` and `Lf · (1 − x)` is added.
    var bed_fogged = true;
@if(VERTEX_OUTPUT_INSTANCE_INDEX) {
    bed_fogged = mesh_functions::get_tag(in.instance_index) < SKIRT_TAG;
}
    let fogged = main_pass_post_lighting_processing(pbr_input, film);
    out.color = fogged;
    if bed_fogged {
        let fog_only = main_pass_post_lighting_processing(pbr_input, vec4<f32>(0.0, 0.0, 0.0, a_edge));
        out.color = vec4<f32>(fogged.rgb - x * fog_only.rgb, fogged.a);
    }
    return out;
}
}