@yali-engine/water 0.12.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/water.js CHANGED
@@ -1,12 +1,42 @@
1
+ import { mat4 } from '@yali-engine/math';
1
2
  import { bakeShore, toHalf } from "./shore.js";
2
3
  import { waterSurface } from "./water-shaders.gen.js";
3
4
  import { WaterWaves } from "./waves.js";
5
+ /**
6
+ * The surface grid's index buffer (ADR-0222): quad (r, s) of the (rings + 1) × (segments + 1) lattice as (r, s) (r+1, s)
7
+ * (r+1, s+1) and (r, s) (r+1, s+1) (r, s+1).
8
+ */
9
+ function gridIndices(device, rings, segments) {
10
+ const w = segments + 1;
11
+ const idx = new Uint32Array(rings * segments * 6);
12
+ let k = 0;
13
+ for (let r = 0; r < rings; r++) {
14
+ for (let s = 0; s < segments; s++) {
15
+ const a = r * w + s;
16
+ const b = (r + 1) * w + s;
17
+ idx[k++] = a;
18
+ idx[k++] = b;
19
+ idx[k++] = b + 1;
20
+ idx[k++] = a;
21
+ idx[k++] = b + 1;
22
+ idx[k++] = a + 1;
23
+ }
24
+ }
25
+ const buf = device.createBuffer({ label: 'yali:water:grid', size: idx.byteLength, usage: 0x10 | 0x08 });
26
+ device.queue.writeBuffer(buf, 0, idx);
27
+ return buf;
28
+ }
4
29
  const TIERS = { high: 2, medium: 1, low: 0 };
5
- /** Bytes of `SurfaceParams` (`shaders/water/surface.wesl`): 4 cascades + 8 vectors. */
6
- const PARAMS_BYTES = (4 + 8 + 4) * 16;
7
- /** The motion uniform (ADR-0210): this frame's and last frame's view-projections (unjittered), the eye's move, a flag. */
8
- const MOTION_BYTES = 16 * 4 * 2 + 16;
30
+ /** Bytes of `SurfaceParams` (`shaders/water/surface.wesl`): 4 cascades + 8 vectors + 4 of the surf + the glints + the clouds' shadow. */
31
+ const PARAMS_BYTES = (4 + 8 + 4 + 2) * 16;
32
+ /** The sun's solid angle, steradians: a disc 0.5357° across (`SUN_ANGLE` in the render package's shadows). */
33
+ const SUN_SOLID_ANGLE = Math.PI * ((0.5357 * Math.PI) / 360) ** 2;
34
+ /** The motion uniform (ADR-0210): this frame's and last frame's view-projections (unjittered), the eye's move, a flag; the reactive mask's strength (ADR-0229). */
35
+ const MOTION_BYTES = 16 * 4 * 2 + 16 + 16;
9
36
  /** The first ring's spacing (m) and how much wider each next ring is. */
37
+ /** The surf field's texels a side (ADR-0223): 0.5 m over the demo's 256 m shore field; the low tier's 256 (ADR-0232). */
38
+ const SURF_TEXELS = 512;
39
+ const SURF_TEXELS_LOW = 256;
10
40
  /** Spray sprites (`SPRAY_SIDE`² in surface.wesl, ADR-0218). */
11
41
  const SPRAY_SPRITES = 192 * 192;
12
42
  const FIRST_RING = 0.12;
@@ -67,10 +97,30 @@ export class Water {
67
97
  surfLip;
68
98
  /** Spray thrown up by breaking waves (ADR-0218; live, 0 = none, 1 = default). */
69
99
  spray;
100
+ /** The ripples finer than the cascades (ADR-0223; live, 0 none, 1 Cox & Munk's slope for the wind). */
101
+ ripples = 1;
102
+ /** Rocks the swash and the surf reach are wet: darker below the water's line (ADR-0228; live). */
103
+ wetRocks = true;
104
+ /**
105
+ * How strongly fresh whitewater, the swash's front, the lip and spray mark TAA's reactive mask (ADR-0229; live, 0–1,
106
+ * default 0.6), where TAA runs with `taa: { reactive: true }`: their shading moves unlike the surface's points, so TAA
107
+ * leans on this frame there instead of smearing them.
108
+ */
109
+ reactive = 0.6;
110
+ /**
111
+ * Sun glints (ADR-0226; live, 0 = the smooth highlight only, 1 = default): far water mirrors the sun from a few ripples
112
+ * at a time, so the glitter path is sparkles that rise and fade, keeping the smooth highlight's mean.
113
+ */
114
+ glints = 1;
115
+ /** A glinting facet's size, metres (ADR-0226; live, default 0.02: the capillary ripples). Smaller = more, finer sparkles. */
116
+ glintSize = 0.02;
117
+ /** How long a glint lives, seconds (ADR-0226; live, default 0.3). */
118
+ glintLife = 0.3;
70
119
  #quality = 'high';
71
120
  /**
72
121
  * The quality tier (ADR-0220; live): `high` everything; `medium` the coarse foam pattern only, 16 reflection steps, half
73
- * the spray; `low` no caustics, no scene reflections, no spray. Setting it sets those knobs (they stay live after).
122
+ * the spray; `low` no caustics, no scene reflections, no spray, and a 256² surf field (1 m texels, ADR-0232). Setting it
123
+ * sets those knobs (they stay live after).
74
124
  */
75
125
  get quality() {
76
126
  return this.#quality;
@@ -82,8 +132,30 @@ export class Water {
82
132
  this.reflectionSteps = q === 'high' ? 24 : 16;
83
133
  this.reflections = q === 'low' ? 0 : 40;
84
134
  this.spray = q === 'high' ? 1 : q === 'medium' ? 0.5 : 0;
135
+ // The surf field's resolution follows the tier (ADR-0232): remade, and every group naming it with it.
136
+ if (this.#shoreField && this.#surfN !== this.#surfTexels()) {
137
+ this.#makeSurfField(this.#surfTexels());
138
+ this.#resetGroups();
139
+ }
140
+ }
141
+ /** The surf field's texels a side at this tier (ADR-0232). */
142
+ #surfTexels() {
143
+ return this.#quality === 'low' ? SURF_TEXELS_LOW : SURF_TEXELS;
144
+ }
145
+ /** The surf field's texels a side now. */
146
+ #surfN = 1;
147
+ #resetGroups() {
148
+ this.#groups = [null, null];
149
+ this.#groupKey = '';
150
+ this.#motionGroups = [null, null];
151
+ this.#sprayGroups = [null, null];
152
+ this.#surfGroups = [null, null];
153
+ this.#wetGroups = [null, null];
154
+ this.#sprayReactiveGroups = [null, null];
85
155
  }
86
156
  #prevSurfTime = 0;
157
+ /** The waves' time at the last motion upload: unchanged, they count as still (ADR-0228). */
158
+ #motionSeconds = Number.NaN;
87
159
  #shoreField = null;
88
160
  #shoreTexture;
89
161
  #spillTexture;
@@ -92,10 +164,33 @@ export class Water {
92
164
  segments;
93
165
  #pipeline;
94
166
  #sprayPipeline = null;
95
- #sprayGroup = null;
167
+ #sprayGroups = [null, null];
96
168
  #sprayKey = '';
169
+ /**
170
+ * The surf field (ADR-0223): A (displacement, broken) and D (front, phase, height factor, carried whitewater) in
171
+ * ping-pong pairs, B (slope, face, onset) and C (fresh foam, swash cover, wet, film); `SURF_TEXELS`² over the shore field.
172
+ */
173
+ #surfA = [];
174
+ #surfB = null;
175
+ #surfC = null;
176
+ #surfD = [];
177
+ #surfPipeline = null;
178
+ #surfGroups = [null, null];
179
+ /** Which half of each pair this frame's surf field wrote. */
180
+ #surfParity = 0;
97
181
  #motionPipeline = null;
98
- #motionGroup = null;
182
+ /** With TAA's reactive mask (ADR-0229): the motion pass with the mask as a second target, and spray into the mask. */
183
+ #motionReactivePipeline = null;
184
+ #sprayReactivePipeline = null;
185
+ #sprayReactiveGroups = [null, null];
186
+ #sprayReactiveKey = -1;
187
+ /** Wet rocks (ADR-0228): the fullscreen multiply, its inverse view-projection and bind group. */
188
+ #wetPipeline = null;
189
+ #wetParams;
190
+ #wetData = new Float32Array(16);
191
+ #wetGroups = [null, null];
192
+ #wetKey = -1;
193
+ #motionGroups = [null, null];
99
194
  #motionParams;
100
195
  #motionData = new Float32Array(MOTION_BYTES / 4);
101
196
  #prevVP = new Float32Array(16);
@@ -110,10 +205,12 @@ export class Water {
110
205
  #under = false;
111
206
  #inv = 1;
112
207
  #params;
208
+ /** The grid's triangles (ADR-0222): two a quad over the (rings + 1) × (segments + 1) lattice of points. */
209
+ #indices;
113
210
  #data = new Float32Array(PARAMS_BYTES / 4);
114
211
  #sampler;
115
212
  #clamp;
116
- #group = null;
213
+ #groups = [null, null];
117
214
  #groupKey = '';
118
215
  #depthKey = -1;
119
216
  #seconds = 0;
@@ -149,7 +246,11 @@ export class Water {
149
246
  this.rings = o.rings ?? 240;
150
247
  this.segments = o.segments ?? 384;
151
248
  this.#params = device.createBuffer({ label: 'yali:water:surface', size: PARAMS_BYTES, usage: 0x40 | 0x08 });
249
+ this.#indices = gridIndices(device, this.rings, this.segments);
250
+ // Stand-ins until a seabed brings the surf (the shaders name the field's bindings either way).
251
+ this.#makeSurfField(1);
152
252
  this.#motionParams = device.createBuffer({ label: 'yali:water:motion', size: MOTION_BYTES, usage: 0x40 | 0x08 });
253
+ this.#wetParams = device.createBuffer({ label: 'yali:water:wet', size: 64, usage: 0x40 | 0x08 });
153
254
  this.#clamp = device.createSampler({ label: 'yali:water:clamp', magFilter: 'linear', minFilter: 'linear' });
154
255
  this.#sampler = device.createSampler({ label: 'yali:water:wrap', addressModeU: 'repeat', addressModeV: 'repeat', magFilter: 'linear', minFilter: 'linear', mipmapFilter: 'linear' });
155
256
  }
@@ -165,6 +266,8 @@ export class Water {
165
266
  }),
166
267
  this.waves.init(cache),
167
268
  cache.compute({ label: 'yali:water:copy-depth', compute: { shader: waterSurface, entryPoint: 'copy_depth' } }).then((p) => (this.#copyDepth = p)),
269
+ // The surf field (ADR-0223).
270
+ cache.compute({ label: 'yali:water:surf-field', compute: { shader: waterSurface, entryPoint: 'surf_field' } }).then((p) => (this.#surfPipeline = p)),
168
271
  // Spray (ADR-0218): soft sprites over the surface, premultiplied, depth-tested, never written.
169
272
  cache
170
273
  .render({
@@ -175,6 +278,15 @@ export class Water {
175
278
  depthStencil: { format: 'depth32float', depthWriteEnabled: false, depthCompare: 'greater' },
176
279
  })
177
280
  .then((p) => (this.#sprayPipeline = p)),
281
+ // Wet rocks (ADR-0228): a fullscreen multiply over the opaque scene.
282
+ cache
283
+ .render({
284
+ label: 'yali:water:wet',
285
+ vertex: { shader: waterSurface, entryPoint: 'vs_wet' },
286
+ fragment: { shader: waterSurface, entryPoint: 'fs_wet', targets: [{ format, blend: { color: { srcFactor: 'dst', dstFactor: 'zero' }, alpha: { srcFactor: 'zero', dstFactor: 'one' } } }] },
287
+ primitive: { topology: 'triangle-list' },
288
+ })
289
+ .then((p) => (this.#wetPipeline = p)),
178
290
  // The waves' motion for TAA (ADR-0210): the same grid drawn into the motion target.
179
291
  cache
180
292
  .render({
@@ -185,6 +297,25 @@ export class Water {
185
297
  depthStencil: { format: 'depth32float', depthWriteEnabled: true, depthCompare: 'greater' },
186
298
  })
187
299
  .then((p) => (this.#motionPipeline = p)),
300
+ // With TAA's reactive mask (ADR-0229): the same motion with the mask beside it, and the spray's sprites into it.
301
+ cache
302
+ .render({
303
+ label: 'yali:water:motion-reactive',
304
+ vertex: { shader: waterSurface, entryPoint: 'vs_motion' },
305
+ fragment: { shader: waterSurface, entryPoint: 'fs_motion_reactive', targets: [{ format: 'rgba16float' }, { format: 'r8unorm' }] },
306
+ primitive: { topology: 'triangle-list', cullMode: 'none' },
307
+ depthStencil: { format: 'depth32float', depthWriteEnabled: true, depthCompare: 'greater' },
308
+ })
309
+ .then((p) => (this.#motionReactivePipeline = p)),
310
+ cache
311
+ .render({
312
+ label: 'yali:water:spray-reactive',
313
+ vertex: { shader: waterSurface, entryPoint: 'vs_spray' },
314
+ fragment: { shader: waterSurface, entryPoint: 'fs_spray_reactive', targets: [{ format: 'r8unorm', blend: { color: { operation: 'max', srcFactor: 'one', dstFactor: 'one' }, alpha: { operation: 'max', srcFactor: 'one', dstFactor: 'one' } } }] },
315
+ primitive: { topology: 'triangle-list', cullMode: 'none' },
316
+ depthStencil: { format: 'depth32float', depthWriteEnabled: false, depthCompare: 'greater' },
317
+ })
318
+ .then((p) => (this.#sprayReactivePipeline = p)),
188
319
  ]);
189
320
  this.#pipeline = pipeline;
190
321
  }
@@ -213,7 +344,16 @@ export class Water {
213
344
  d.set([...this.absorption, this.refraction], 36);
214
345
  // The scene behind is read at the pre-exposure it was written with (this frame's).
215
346
  this.#inv = 1 / Math.max(1e-6, f.scene.preExposure);
216
- d.set([this.shoreFoam, this.#under ? 1 : 0, this.#inv, 0], 40);
347
+ // The ripples finer than the cascades (ADR-0223) make up the slope variance Cox & Munk measured for this wind
348
+ // (0.003 + 0.00512 U in all, half an axis) that the cascades leave out; their gain over the finest cascade's own.
349
+ const sv = this.waves.slopeVariance;
350
+ const have = sv.reduce((a, b) => a + b, 0);
351
+ const missing = Math.max(0, (0.003 + 0.00512 * this.waves.wind) / 2 - have);
352
+ const ripples = this.ripples * Math.min(8, Math.sqrt(missing / Math.max(1e-9, sv[sv.length - 1] * 0.74)));
353
+ // Shore foam is churned by waves (ADR-0230): a calm sea's thin water over sand or a rock's top does not foam. Its band
354
+ // grows from nothing at 2.5 m/s of wind to the full depth from 6.5 (whitecaps begin about there).
355
+ const sea = Math.max(0, Math.min(1, (this.waves.wind - 2.5) / 4));
356
+ d.set([Math.max(1e-4, this.shoreFoam * sea), this.#under ? 1 : 0, this.#inv, ripples], 40);
217
357
  d.set([this.reflections, this.reflectionSteps, 0.6, this.debugMirror ? 1 : 0], 44);
218
358
  // The surf (ADR-0213): the eye on the shore field (f64 here, small after), the crest clock now and last frame.
219
359
  const sf = this.#shoreField;
@@ -233,7 +373,19 @@ export class Water {
233
373
  const surfTime = ((this.#seconds % span) + span) % span;
234
374
  d.set([surfTime, this.surfVariety, this.surfLip, this.#prevSurfTime], 60);
235
375
  this.#prevSurfTime = surfTime;
376
+ d.set([this.glints, Math.max(1e-3, this.glintSize), SUN_SOLID_ANGLE, Math.max(1 / 60, this.glintLife)], 64);
377
+ // The clouds' shadow (ADR-0227): the map's centre less the eye in f64, then small.
378
+ const cs = f.environment.cloudShadow;
379
+ if (cs)
380
+ d.set([cs.centre[0] - eye[0], cs.centre[1] - eye[2], 1 / cs.size, 1], 68);
381
+ else
382
+ d.set([0, 0, 0, 0], 68);
236
383
  this.device.queue.writeBuffer(this.#params, 0, d);
384
+ if (this.enabled)
385
+ this.#runSurfField();
386
+ // Wet rocks (ADR-0228): the scene's points from its depth, through this frame's inverse view-projection.
387
+ if (this.wetRocks && mat4.invert(this.#wetData, f.camera.uniform))
388
+ this.device.queue.writeBuffer(this.#wetParams, 0, this.#wetData);
237
389
  if (this.#prevDisplacement) {
238
390
  // Motion: this frame's view-projection (camera-relative, unjittered: `camera.uniform`), last frame's, and how far
239
391
  // the eye moved (last frame's camera-relative points are this frame's plus it).
@@ -242,10 +394,13 @@ export class Water {
242
394
  for (let i = 0; i < 16; i++)
243
395
  m[i] = vp[i];
244
396
  m.set(this.#prevVP, 16);
397
+ m[36] = Math.max(0, Math.min(1, this.reactive));
245
398
  for (let i = 0; i < 3; i++)
246
399
  m[32 + i] = eye[i] - this.#prevEye[i];
247
- // Until last frame's waves exist (two frames), the waves count as still: the camera's motion only.
248
- m[35] = this.motion && this.#motionFrames >= 2 && this.frozenAt === null ? 1 : 0;
400
+ // Until last frame's waves exist (two frames), the waves count as still: the camera's motion only. Held waves move
401
+ // when their time changes (a probe stepping them frame by frame, ADR-0228), and are still while it stands.
402
+ m[35] = this.motion && this.#motionFrames >= 2 && this.#seconds !== this.#motionSeconds ? 1 : 0;
403
+ this.#motionSeconds = this.#seconds;
249
404
  this.device.queue.writeBuffer(this.#motionParams, 0, m);
250
405
  for (let i = 0; i < 16; i++)
251
406
  this.#prevVP[i] = vp[i];
@@ -253,6 +408,94 @@ export class Water {
253
408
  this.#prevEye[i] = eye[i];
254
409
  }
255
410
  }
411
+ /** The surf field's textures (ADR-0223), made with the seabed: four rgba16float targets, A and D in pairs. */
412
+ #makeSurfField(texels) {
413
+ this.#surfN = texels;
414
+ for (const t of [...this.#surfA, ...this.#surfD, this.#surfB, this.#surfC])
415
+ t?.destroy();
416
+ const make = (label) => this.device.createTexture({ label, size: [texels, texels], format: 'rgba16float', usage: 0x08 | 0x04 });
417
+ this.#surfA = [make('yali:water:surf-a0'), make('yali:water:surf-a1')];
418
+ this.#surfB = make('yali:water:surf-b');
419
+ this.#surfC = make('yali:water:surf-c');
420
+ this.#surfD = [make('yali:water:surf-d0'), make('yali:water:surf-d1')];
421
+ }
422
+ /** The surf field as the surface, motion and spray passes read it: this frame's half `p` and last frame's. */
423
+ #surfEntries(p) {
424
+ if (!this.#surfB)
425
+ return [];
426
+ return [
427
+ { binding: 17, resource: this.#surfA[p].createView() },
428
+ { binding: 18, resource: this.#surfB.createView() },
429
+ { binding: 19, resource: this.#surfC.createView() },
430
+ { binding: 20, resource: this.#surfD[p].createView() },
431
+ // Last frame's displacement (the motion pass's; auto layouts keep it wherever `surf_tex` is compiled in). Last
432
+ // frame's whitewater (22) only the surf field reads.
433
+ { binding: 21, resource: this.#surfA[1 - p].createView() },
434
+ ];
435
+ }
436
+ /**
437
+ * The surf field for this frame (ADR-0223), in its own submission before the frame's passes (TAA's motion pass may come
438
+ * before a feature's own): `surf_field` writes A and D's other half from last frame's D.
439
+ */
440
+ #runSurfField() {
441
+ if (!this.#surfPipeline || !this.#surfB || !this.#shoreField)
442
+ return;
443
+ const p = 1 - this.#surfParity;
444
+ this.#surfParity = p;
445
+ this.#surfGroups[p] ??= this.device.createBindGroup({
446
+ label: 'yali:water:surf-field',
447
+ layout: this.#surfPipeline.getBindGroupLayout(0),
448
+ entries: [
449
+ { binding: 2, resource: { buffer: this.#params } },
450
+ { binding: 9, resource: this.#clamp },
451
+ { binding: 14, resource: this.#shoreTexture.createView() },
452
+ { binding: 15, resource: this.#spillTexture.createView() },
453
+ { binding: 22, resource: this.#surfD[1 - p].createView() },
454
+ { binding: 23, resource: this.#surfA[p].createView() },
455
+ { binding: 24, resource: this.#surfB.createView() },
456
+ { binding: 25, resource: this.#surfC.createView() },
457
+ { binding: 26, resource: this.#surfD[p].createView() },
458
+ ],
459
+ });
460
+ const enc = this.device.createCommandEncoder({ label: 'yali:water:surf-field' });
461
+ const pass = enc.beginComputePass({ label: 'yali:water:surf-field' });
462
+ pass.setPipeline(this.#surfPipeline);
463
+ pass.setBindGroup(0, this.#surfGroups[p]);
464
+ pass.dispatchWorkgroups(this.#surfN / 8, this.#surfN / 8);
465
+ pass.end();
466
+ this.device.queue.submit([enc.finish()]);
467
+ }
468
+ /**
469
+ * The surf field's GPU cost, ms a frame (ADR-0223): one submission of `frames` + 1 dispatches less one of 1, over
470
+ * `frames`, each timed to `onSubmittedWorkDone` (the render loop's own frames land in either window, and the difference
471
+ * cancels them; the field runs in its own submission, which the frame graph's timers do not see). Null without a seabed.
472
+ *
473
+ * @example
474
+ * const ms = await ocean.measureSurf();
475
+ */
476
+ async measureSurf(frames = 20) {
477
+ const p = this.#surfParity;
478
+ const group = this.#surfGroups[p];
479
+ if (!this.#surfPipeline || !group)
480
+ return null;
481
+ const run = async (n) => {
482
+ const enc = this.device.createCommandEncoder({ label: 'yali:water:surf-measure' });
483
+ const pass = enc.beginComputePass();
484
+ pass.setPipeline(this.#surfPipeline);
485
+ pass.setBindGroup(0, group);
486
+ for (let i = 0; i < n; i++)
487
+ pass.dispatchWorkgroups(this.#surfN / 8, this.#surfN / 8);
488
+ pass.end();
489
+ await this.device.queue.onSubmittedWorkDone();
490
+ const t0 = performance.now();
491
+ this.device.queue.submit([enc.finish()]);
492
+ await this.device.queue.onSubmittedWorkDone();
493
+ return performance.now() - t0;
494
+ };
495
+ await run(2);
496
+ const small = await run(1);
497
+ return Math.max(0, (await run(frames + 1)) - small) / frames;
498
+ }
256
499
  /**
257
500
  * The seabed under the water (ADR-0213): bakes the shore field (`bakeShore`: depth, the crests' arrival phase for
258
501
  * `surfPeriod`, their direction) and uploads it; the surf then rises, bends to the beach and breaks. Call again after
@@ -281,32 +524,34 @@ export class Water {
281
524
  this.#spillTexture = this.device.createTexture({ label: 'yali:water:spill', size: [field.size, field.size], format: 'rg16float', usage: 0x04 | 0x02 });
282
525
  this.device.queue.writeTexture({ texture: this.#spillTexture }, spill, { bytesPerRow: field.size * 4 }, [field.size, field.size]);
283
526
  this.#shoreField = field;
284
- this.#group = null;
285
- this.#groupKey = '';
286
- this.#motionGroup = null;
287
- this.#sprayGroup = null;
527
+ this.#resetGroups();
528
+ this.#makeSurfField(this.#surfTexels());
288
529
  return field;
289
530
  }
290
531
  /** The baked shore field, or null before `setSeabed`. */
291
532
  get shoreField() {
292
533
  return this.#shoreField;
293
534
  }
294
- addMotionPasses(graph, motion, depth) {
535
+ addMotionPasses(graph, motion, depth, reactive = null) {
295
536
  const disp = this.waves.displacement;
296
537
  this.#prevDisplacement ??= this.device.createTexture({ label: 'yali:water:displacement-prev', size: [disp.width, disp.height, disp.depthOrArrayLayers], format: disp.format, usage: 0x04 | 0x02 });
297
- this.#motionGroup = null;
538
+ this.#motionGroups = [null, null];
298
539
  graph.addPass({
299
540
  name: 'water.motion',
300
541
  kind: 'render',
301
- reads: ['water.waves'],
542
+ // With TAA's reactive mask (ADR-0229) the mask is a second target, written after its clear.
543
+ reads: ['water.waves', ...(reactive ? ['post.reactiveClear'] : [])],
302
544
  color: { target: motion, clear: { r: 0, g: 0, b: 0, a: 0 }, load: true },
545
+ ...(reactive ? { extraColor: [{ target: reactive, clear: { r: 0, g: 0, b: 0, a: 0 }, load: true }] } : {}),
303
546
  depth: { target: depth, clear: 0, load: true, store: true },
304
547
  execute: (pass) => {
305
- if (!this.enabled || !this.#motionPipeline || !this.#scene)
548
+ const pipeline = reactive ? this.#motionReactivePipeline : this.#motionPipeline;
549
+ if (!this.enabled || !pipeline || !this.#scene)
306
550
  return;
307
- this.#motionGroup ??= this.device.createBindGroup({
551
+ const mp = this.#surfParity;
552
+ this.#motionGroups[mp] ??= this.device.createBindGroup({
308
553
  label: 'yali:water:motion',
309
- layout: this.#motionPipeline.getBindGroupLayout(0),
554
+ layout: pipeline.getBindGroupLayout(0),
310
555
  entries: [
311
556
  { binding: 0, resource: { buffer: this.#scene.cameraBuffer } },
312
557
  { binding: 2, resource: { buffer: this.#params } },
@@ -317,13 +562,50 @@ export class Water {
317
562
  { binding: 9, resource: this.#clamp },
318
563
  { binding: 14, resource: this.#shoreTexture.createView() },
319
564
  { binding: 15, resource: this.#spillTexture.createView() },
565
+ ...this.#surfEntries(mp),
320
566
  ],
321
567
  });
322
- pass.setPipeline(this.#motionPipeline);
323
- pass.setBindGroup(0, this.#motionGroup);
324
- pass.draw(this.rings * this.segments * 6);
568
+ pass.setPipeline(pipeline);
569
+ pass.setBindGroup(0, this.#motionGroups[mp]);
570
+ pass.setIndexBuffer(this.#indices, 'uint32');
571
+ pass.drawIndexed(this.rings * this.segments * 6);
325
572
  },
326
573
  });
574
+ // The spray's sprites into the reactive mask (ADR-0229), depth-tested against the motion pass's depth.
575
+ if (reactive) {
576
+ graph.addPass({
577
+ name: 'water.sprayReactive',
578
+ kind: 'render',
579
+ reads: ['water.motion', 'water.depth'],
580
+ color: { target: reactive, clear: { r: 0, g: 0, b: 0, a: 0 }, load: true },
581
+ depth: { target: depth, clear: 0, load: true, store: true },
582
+ execute: (pass, ctx) => {
583
+ if (!this.enabled || this.spray <= 0 || this.reactive <= 0 || !this.#shoreField || !this.#sprayReactivePipeline || !this.#scene)
584
+ return;
585
+ const sq = this.#surfParity;
586
+ if (this.#sprayReactiveKey !== ctx.generation) {
587
+ this.#sprayReactiveKey = ctx.generation;
588
+ this.#sprayReactiveGroups = [null, null];
589
+ }
590
+ this.#sprayReactiveGroups[sq] ??= this.device.createBindGroup({
591
+ label: 'yali:water:spray-reactive',
592
+ layout: this.#sprayReactivePipeline.getBindGroupLayout(0),
593
+ entries: [
594
+ { binding: 0, resource: { buffer: this.#scene.cameraBuffer } },
595
+ { binding: 2, resource: { buffer: this.#params } },
596
+ { binding: 9, resource: this.#clamp },
597
+ { binding: 13, resource: { buffer: this.#motionParams } },
598
+ { binding: 14, resource: this.#shoreTexture.createView() },
599
+ { binding: 15, resource: this.#spillTexture.createView() },
600
+ ...this.#surfEntries(sq),
601
+ ],
602
+ });
603
+ pass.setPipeline(this.#sprayReactivePipeline);
604
+ pass.setBindGroup(0, this.#sprayReactiveGroups[sq]);
605
+ pass.draw(6, SPRAY_SPRITES);
606
+ },
607
+ });
608
+ }
327
609
  // After TAA has read this frame's motion, keep this frame's waves for the next frame's.
328
610
  graph.addPass({
329
611
  name: 'water.keepWaves',
@@ -344,10 +626,42 @@ export class Water {
344
626
  this.#scene = scene;
345
627
  graph.transientTexture('water.under', { format: 'rgba16float', usage: 0x02 | 0x04 });
346
628
  graph.transientTexture('water.depth', { format: 'r32float', usage: 0x08 | 0x04 });
629
+ // Wet rocks (ADR-0228): before the copy, so the water sees them wet through itself too.
630
+ graph.addPass({
631
+ name: 'water.wet',
632
+ kind: 'render',
633
+ reads: [depth],
634
+ writes: ['water.wet'],
635
+ color: { target: color, clear: { r: 0, g: 0, b: 0, a: 0 }, load: true },
636
+ execute: (pass, ctx) => {
637
+ if (!this.enabled || !this.wetRocks || !this.#wetPipeline || !this.#shoreField)
638
+ return;
639
+ const wp = this.#surfParity;
640
+ if (this.#wetKey !== ctx.generation) {
641
+ this.#wetKey = ctx.generation;
642
+ this.#wetGroups = [null, null];
643
+ }
644
+ this.#wetGroups[wp] ??= this.device.createBindGroup({
645
+ label: 'yali:water:wet',
646
+ layout: this.#wetPipeline.getBindGroupLayout(0),
647
+ entries: [
648
+ { binding: 2, resource: { buffer: this.#params } },
649
+ { binding: 9, resource: this.#clamp },
650
+ { binding: 10, resource: ctx.view(depth) },
651
+ { binding: 14, resource: this.#shoreTexture.createView() },
652
+ { binding: 28, resource: { buffer: this.#wetParams } },
653
+ ...this.#surfEntries(wp).filter((e) => e.binding === 19 || e.binding === 20),
654
+ ],
655
+ });
656
+ pass.setPipeline(this.#wetPipeline);
657
+ pass.setBindGroup(0, this.#wetGroups[wp]);
658
+ pass.draw(3);
659
+ },
660
+ });
347
661
  graph.addPass({
348
662
  name: 'water.under',
349
663
  kind: 'encoder',
350
- reads: [],
664
+ reads: ['water.wet'],
351
665
  writes: ['water.under'],
352
666
  execute: (encoder, ctx) => {
353
667
  if (this.enabled)
@@ -392,10 +706,15 @@ export class Water {
392
706
  if (!this.enabled || !env)
393
707
  return;
394
708
  const pano = env.cloudPanorama;
395
- const key = `${env.version}:${ctx.generation}:${pano ? pano.label : '-'}`;
396
- if (!this.#group || key !== this.#groupKey) {
709
+ const shadow = env.cloudShadow?.texture ?? null;
710
+ const key = `${env.version}:${ctx.generation}:${pano ? pano.label : '-'}:${shadow ? shadow.label : '-'}`;
711
+ const gp = this.#surfParity;
712
+ if (key !== this.#groupKey) {
397
713
  this.#groupKey = key;
398
- this.#group = this.device.createBindGroup({
714
+ this.#groups = [null, null];
715
+ }
716
+ if (!this.#groups[gp]) {
717
+ this.#groups[gp] = this.device.createBindGroup({
399
718
  label: 'yali:water:surface',
400
719
  layout: this.#pipeline.getBindGroupLayout(0),
401
720
  entries: [
@@ -413,12 +732,16 @@ export class Water {
413
732
  { binding: 15, resource: this.#spillTexture.createView() },
414
733
  // The clouds' panorama (ADR-0219), or a stand-in the shader never reads (size.z = 0).
415
734
  { binding: 16, resource: (pano ?? this.#spillTexture).createView() },
735
+ // The clouds' shadow (ADR-0227), or a stand-in the shader never reads (cshadow.w = 0).
736
+ { binding: 27, resource: (shadow ?? this.#spillTexture).createView() },
737
+ ...this.#surfEntries(gp),
416
738
  ],
417
739
  });
418
740
  }
419
741
  pass.setPipeline(this.#pipeline);
420
- pass.setBindGroup(0, this.#group);
421
- pass.draw(this.rings * this.segments * 6);
742
+ pass.setBindGroup(0, this.#groups[gp]);
743
+ pass.setIndexBuffer(this.#indices, 'uint32');
744
+ pass.drawIndexed(this.rings * this.segments * 6);
422
745
  },
423
746
  });
424
747
  // Spray over the surface (ADR-0218): drawn only with a seabed (the surf), one instance per sprite.
@@ -431,10 +754,16 @@ export class Water {
431
754
  execute: (pass, ctx) => {
432
755
  if (!this.enabled || this.spray <= 0 || !this.#shoreField || !this.#sprayPipeline)
433
756
  return;
434
- const key = `${ctx.generation}`;
435
- if (!this.#sprayGroup || key !== this.#sprayKey) {
757
+ // The clouds' shadow dims the sprites' sun too (ADR-0227).
758
+ const shadow = this.#env?.cloudShadow?.texture ?? null;
759
+ const key = `${ctx.generation}:${shadow ? shadow.label : '-'}`;
760
+ const sq = this.#surfParity;
761
+ if (key !== this.#sprayKey) {
436
762
  this.#sprayKey = key;
437
- this.#sprayGroup = this.device.createBindGroup({
763
+ this.#sprayGroups = [null, null];
764
+ }
765
+ if (!this.#sprayGroups[sq]) {
766
+ this.#sprayGroups[sq] = this.device.createBindGroup({
438
767
  label: 'yali:water:spray',
439
768
  layout: this.#sprayPipeline.getBindGroupLayout(0),
440
769
  entries: [
@@ -445,11 +774,13 @@ export class Water {
445
774
  { binding: 9, resource: this.#clamp },
446
775
  { binding: 14, resource: this.#shoreTexture.createView() },
447
776
  { binding: 15, resource: this.#spillTexture.createView() },
777
+ { binding: 27, resource: (shadow ?? this.#spillTexture).createView() },
778
+ ...this.#surfEntries(sq),
448
779
  ],
449
780
  });
450
781
  }
451
782
  pass.setPipeline(this.#sprayPipeline);
452
- pass.setBindGroup(0, this.#sprayGroup);
783
+ pass.setBindGroup(0, this.#sprayGroups[sq]);
453
784
  pass.draw(6, SPRAY_SPRITES);
454
785
  },
455
786
  });
@@ -483,6 +814,9 @@ export class Water {
483
814
  tiles: [...this.waves.tiles],
484
815
  size: this.waves.size,
485
816
  gpuBytes: this.gpuBytes,
817
+ glints: { strength: this.glints, size: this.glintSize, life: this.glintLife },
818
+ wetRocks: this.wetRocks,
819
+ reactive: this.reactive,
486
820
  surf: { seabed: !!this.#shoreField, height: this.surfHeight, period: this.surfPeriod, breaking: this.surfBreaking, lean: this.surfLean, start: this.surfStart, calm: this.surfCalm, variety: this.surfVariety, lip: this.surfLip, spray: this.spray },
487
821
  });
488
822
  return [
@@ -507,12 +841,22 @@ export class Water {
507
841
  surfVariety: { type: 'number', description: 'How much waves differ: sets and sections along the shore, 0 (all alike) to 1.' },
508
842
  surfLip: { type: 'number', description: 'How far a breaking crest throws its lip forward, 0 to 1.' },
509
843
  spray: { type: 'number', description: 'Spray thrown up by breaking waves, 0 (none) to 4; 1 is the default.' },
844
+ glints: { type: 'number', description: 'Sun glints on far water, 0 (the smooth highlight only) to 1 (default).' },
845
+ glintSize: { type: 'number', description: 'The size of a glinting ripple, metres, 0.002 to 0.5 (default 0.02): smaller gives more, finer sparkles.' },
846
+ glintLife: { type: 'number', description: 'How long a glint lives, seconds, 0.05 to 3 (default 0.3).' },
847
+ reactive: { type: 'number', description: 'How strongly whitewater and spray mark TAA’s reactive mask, 0 to 1 (default 0.6; needs taa: { reactive: true }).' },
848
+ wetRocks: { type: 'boolean', description: 'Rocks the swash and the surf reach look wet: darker below the water line.' },
510
849
  follow: { type: 'boolean', description: 'true: the waves follow the app clock again.' },
511
850
  },
512
851
  },
513
852
  run: (args) => {
514
853
  if (args.enabled !== undefined)
515
854
  this.enabled = !!args.enabled;
855
+ if (args.wetRocks !== undefined)
856
+ this.wetRocks = !!args.wetRocks;
857
+ const rx = num(args, 'reactive', 0, 1);
858
+ if (rx !== undefined)
859
+ this.reactive = rx;
516
860
  if (args.quality !== undefined) {
517
861
  if (args.quality !== 'high' && args.quality !== 'medium' && args.quality !== 'low')
518
862
  throw bad('quality', 'must be high, medium or low');
@@ -556,6 +900,15 @@ export class Water {
556
900
  const sr = num(args, 'spray', 0, 4);
557
901
  if (sr !== undefined)
558
902
  this.spray = sr;
903
+ const gl = num(args, 'glints', 0, 1);
904
+ if (gl !== undefined)
905
+ this.glints = gl;
906
+ const gs = num(args, 'glintSize', 0.002, 0.5);
907
+ if (gs !== undefined)
908
+ this.glintSize = gs;
909
+ const gt = num(args, 'glintLife', 0.05, 3);
910
+ if (gt !== undefined)
911
+ this.glintLife = gt;
559
912
  return state();
560
913
  },
561
914
  },
@@ -588,6 +941,9 @@ export class Water {
588
941
  destroy() {
589
942
  this.waves.destroy();
590
943
  this.#params.destroy();
944
+ this.#indices.destroy();
945
+ for (const t of [...this.#surfA, ...this.#surfD, this.#surfB, this.#surfC])
946
+ t?.destroy();
591
947
  this.#motionParams.destroy();
592
948
  this.#shoreTexture.destroy();
593
949
  this.#spillTexture.destroy();