@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-shaders.gen.d.ts +2 -2
- package/dist/water-shaders.gen.js +2 -2
- package/dist/water-shaders.gen.js.map +1 -1
- package/dist/water.d.ts +31 -2
- package/dist/water.js +390 -34
- package/dist/water.js.map +1 -1
- package/dist/waves.d.ts +2 -0
- package/dist/waves.js +3 -0
- package/dist/waves.js.map +1 -1
- package/package.json +3 -2
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
|
|
8
|
-
const
|
|
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
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
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.#
|
|
285
|
-
this.#
|
|
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.#
|
|
538
|
+
this.#motionGroups = [null, null];
|
|
298
539
|
graph.addPass({
|
|
299
540
|
name: 'water.motion',
|
|
300
541
|
kind: 'render',
|
|
301
|
-
|
|
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
|
-
|
|
548
|
+
const pipeline = reactive ? this.#motionReactivePipeline : this.#motionPipeline;
|
|
549
|
+
if (!this.enabled || !pipeline || !this.#scene)
|
|
306
550
|
return;
|
|
307
|
-
|
|
551
|
+
const mp = this.#surfParity;
|
|
552
|
+
this.#motionGroups[mp] ??= this.device.createBindGroup({
|
|
308
553
|
label: 'yali:water:motion',
|
|
309
|
-
layout:
|
|
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(
|
|
323
|
-
pass.setBindGroup(0, this.#
|
|
324
|
-
pass.
|
|
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
|
|
396
|
-
|
|
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.#
|
|
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.#
|
|
421
|
-
pass.
|
|
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
|
-
|
|
435
|
-
|
|
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.#
|
|
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.#
|
|
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();
|