@woosh/meep-engine 3.63.0 → 3.63.1
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/CHANGELOG.md +6 -3
- package/package.json +1 -1
- package/src/engine/graphics3/outline/GPUOutlineRenderer.js +124 -21
- package/src/engine/graphics3/outline/chunk_outline_classify_store.js +77 -0
- package/src/engine/graphics3/outline/chunk_outline_label.js +2 -9
- package/src/engine/graphics3/outline/chunk_outline_label_words.js +29 -0
- package/src/engine/graphics3/outline/shader_outline_classify.js +6 -23
- package/src/engine/graphics3/outline/shader_outline_classify_hidden.js +4 -20
- package/src/engine/graphics3/outline/shader_outline_composite.js +46 -27
- package/src/engine/graphics3/outline/shader_outline_fill.js +59 -0
- package/src/engine/graphics3/outline/shader_outline_fill_tiles.js +70 -0
- package/src/engine/graphics3/outline/shader_outline_flood_seed.js +6 -4
- package/src/engine/graphics3/outline/shader_outline_tile_edges.js +92 -0
package/CHANGELOG.md
CHANGED
|
@@ -12,11 +12,14 @@
|
|
|
12
12
|
`ShadedGeometryOutlineSystem` pair `Outline` with models and primitives, and `OutlineSerializationAdapter` is
|
|
13
13
|
registered. Each pixel belongs to one spec: the front surface's visible spec, otherwise the hidden spec of the
|
|
14
14
|
nearest row with one behind it. Outlines come from a jump flood that keeps each pixel's two nearest specs, run
|
|
15
|
-
only on the screen tiles
|
|
15
|
+
only on the screen tiles within reach of a silhouette, and the nearer outline is drawn on top; the inside of a
|
|
16
|
+
large object is drawn as its fill alone, and not at all when the fill is transparent. A spec shared by several
|
|
16
17
|
entities outlines them as one shape. Playground: `src/shade/playground/outline/`.
|
|
17
18
|
- **Changed:** outlines of hidden parts are back, for an entity whose `Outline` has a hidden spec. At 1898x982
|
|
18
|
-
with a 14 px reach and four outlined entities, one with a hidden spec, the passes cost 0.
|
|
19
|
-
an RTX 4090 and
|
|
19
|
+
with a 14 px reach and four outlined entities, one with a hidden spec, the passes cost 0.26 ms of frame time on
|
|
20
|
+
an RTX 4090 and 2.9 ms on an AMD RDNA2 iGPU (flood 1.6, classify 0.7, composite and fill 0.15). With the
|
|
21
|
+
removed highlight's look, a 3 px line around one object, they cost 0.6–1.3 ms of GPU time on that iGPU from an
|
|
22
|
+
object 50 px tall to one filling the screen, where the highlight pass cost 10.1–6.9 ms.
|
|
20
23
|
- **Added:** the scene database's `mesh_outlines` table; `GPUViewContext.follow(view)` and
|
|
21
24
|
`GPUViewContext.upscale_ratio`; `GPUSceneContext.graphics`; a `visible_meshlets` argument and result on
|
|
22
25
|
`graph_rasterize_scene_fast_raw`; `ScreenTileShader` and `chunk_screen_tile_corner`
|
package/package.json
CHANGED
|
@@ -39,9 +39,12 @@ import { OUTLINE_TILE_SIZE } from "./OUTLINE_TILE_SIZE.js";
|
|
|
39
39
|
import { shader_outline_classify } from "./shader_outline_classify.js";
|
|
40
40
|
import { shader_outline_classify_hidden } from "./shader_outline_classify_hidden.js";
|
|
41
41
|
import { shader_outline_composite } from "./shader_outline_composite.js";
|
|
42
|
+
import { shader_outline_fill } from "./shader_outline_fill.js";
|
|
43
|
+
import { shader_outline_fill_tiles } from "./shader_outline_fill_tiles.js";
|
|
42
44
|
import { shader_outline_flood_seed } from "./shader_outline_flood_seed.js";
|
|
43
45
|
import { shader_outline_flood_step } from "./shader_outline_flood_step.js";
|
|
44
46
|
import { OUTLINE_ROW_CULL_GROUP_SIZE, shader_outline_row_cull } from "./shader_outline_row_cull.js";
|
|
47
|
+
import { OUTLINE_TILE_GRID_GROUP_SIZE, shader_outline_tile_edges } from "./shader_outline_tile_edges.js";
|
|
45
48
|
|
|
46
49
|
/**
|
|
47
50
|
* Words a `COLLECTION_U32` keeps ahead of its elements: the count, padded to the elements' 16-byte
|
|
@@ -69,11 +72,13 @@ const TILE_DRAW_COMMAND_PATTERN = "6u, input, 0u, 0u";
|
|
|
69
72
|
/**
|
|
70
73
|
* Records the outline passes into a frame.
|
|
71
74
|
*
|
|
72
|
-
* In order: the outline raster of the rows with a hidden spec (only when there are any)
|
|
73
|
-
* full-screen classify that labels every pixel with the spec that owns it
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
75
|
+
* In order: the outline raster of the rows with a hidden spec (only when there are any); the
|
|
76
|
+
* full-screen classify that labels every pixel with the spec that owns it and every tile with the
|
|
77
|
+
* label its pixels share; the tiles a silhouette passes through, dilated by the outline's reach and
|
|
78
|
+
* listed — the flood's tiles; the list of the other tiles that show a fill alone; the jump-flood rounds
|
|
79
|
+
* over the flood's tiles; and tile quads blended into the scene colour, a composite over the flood's
|
|
80
|
+
* tiles and a fill over the rest. The flood follows the silhouettes' length, not the area they enclose.
|
|
81
|
+
* Every pass after the tile passes is sized by a live tile count, read on the device.
|
|
77
82
|
*/
|
|
78
83
|
export class GPUOutlineRenderer {
|
|
79
84
|
/**
|
|
@@ -186,7 +191,7 @@ export class GPUOutlineRenderer {
|
|
|
186
191
|
outline_visibility: back.visibility,
|
|
187
192
|
outline_meshlets: back.visible_meshlets,
|
|
188
193
|
labels: label_descriptor(resolution),
|
|
189
|
-
|
|
194
|
+
tile_labels: label_descriptor(tile_resolution),
|
|
190
195
|
}
|
|
191
196
|
});
|
|
192
197
|
} else {
|
|
@@ -200,18 +205,38 @@ export class GPUOutlineRenderer {
|
|
|
200
205
|
visible_meshlets: view_textures.visible_meshlets,
|
|
201
206
|
scene_database: scene.scene_database_buffer,
|
|
202
207
|
labels: label_descriptor(resolution),
|
|
203
|
-
|
|
208
|
+
tile_labels: label_descriptor(tile_resolution),
|
|
204
209
|
}
|
|
205
210
|
});
|
|
206
211
|
}
|
|
207
212
|
|
|
208
|
-
const { labels,
|
|
213
|
+
const { labels, tile_labels } = classify;
|
|
209
214
|
|
|
210
|
-
const
|
|
215
|
+
const tile_grid_group_count = [
|
|
216
|
+
Math.ceil(tile_resolution[0] / OUTLINE_TILE_GRID_GROUP_SIZE),
|
|
217
|
+
Math.ceil(tile_resolution[1] / OUTLINE_TILE_GRID_GROUP_SIZE),
|
|
218
|
+
1
|
|
219
|
+
];
|
|
220
|
+
|
|
221
|
+
const edges = graph_compute_pass({
|
|
222
|
+
graph,
|
|
223
|
+
shader: shader_outline_tile_edges,
|
|
224
|
+
group_count: tile_grid_group_count,
|
|
225
|
+
bindings: {
|
|
226
|
+
settings: { resolution: tile_resolution },
|
|
227
|
+
tile_labels,
|
|
228
|
+
edges: label_descriptor(tile_resolution),
|
|
229
|
+
}
|
|
230
|
+
}).edges;
|
|
231
|
+
|
|
232
|
+
const { tile_active, tiles } = this.#graph_reach({ graph, edges, tile_resolution, steps: plan.reach_steps });
|
|
233
|
+
|
|
234
|
+
const fill_tiles = this.#graph_fill_tiles({ graph, specs, tile_labels, tile_active, tile_resolution, group_count: tile_grid_group_count });
|
|
211
235
|
|
|
212
236
|
const dispatch = graph_build_indirect_dispatch_command(graph, tiles, 1, true);
|
|
213
237
|
|
|
214
|
-
const draw = this.#graph_tile_draw_command(graph, tiles);
|
|
238
|
+
const draw = this.#graph_tile_draw_command(graph, tiles, 'outline / tile draw command');
|
|
239
|
+
const fill_draw = this.#graph_tile_draw_command(graph, fill_tiles, 'outline / fill draw command');
|
|
215
240
|
|
|
216
241
|
let state = graph_compute_indirect_pass({
|
|
217
242
|
graph,
|
|
@@ -220,7 +245,7 @@ export class GPUOutlineRenderer {
|
|
|
220
245
|
bindings: {
|
|
221
246
|
settings: { resolution, step: plan.flood_steps[0] },
|
|
222
247
|
labels,
|
|
223
|
-
|
|
248
|
+
tile_labels,
|
|
224
249
|
tiles,
|
|
225
250
|
state_out: state_descriptor(resolution),
|
|
226
251
|
}
|
|
@@ -242,7 +267,8 @@ export class GPUOutlineRenderer {
|
|
|
242
267
|
}).state_out;
|
|
243
268
|
}
|
|
244
269
|
|
|
245
|
-
this.#graph_composite({ frame, resolution, jitter, upscale, specs, tiles, draw, state, labels,
|
|
270
|
+
this.#graph_composite({ frame, resolution, jitter, upscale, specs, tiles, draw, state, labels, tile_labels });
|
|
271
|
+
this.#graph_fill({ frame, resolution, jitter, upscale, specs, fill_tiles, fill_draw, tile_labels });
|
|
246
272
|
}
|
|
247
273
|
|
|
248
274
|
/**
|
|
@@ -339,18 +365,19 @@ export class GPUOutlineRenderer {
|
|
|
339
365
|
}
|
|
340
366
|
|
|
341
367
|
/**
|
|
342
|
-
* Dilate the
|
|
368
|
+
* Dilate the edge tiles by the outline's reach, and list the tiles that come out set: the flood's
|
|
369
|
+
* tiles.
|
|
343
370
|
*
|
|
344
371
|
* @returns {{tile_active: number, tiles: number}}
|
|
345
372
|
*/
|
|
346
|
-
#graph_reach({ graph,
|
|
373
|
+
#graph_reach({ graph, edges, tile_resolution, steps }) {
|
|
347
374
|
const group_count = [
|
|
348
375
|
Math.ceil(tile_resolution[0] / JUMP_DILATE_GROUP_SIZE),
|
|
349
376
|
Math.ceil(tile_resolution[1] / JUMP_DILATE_GROUP_SIZE),
|
|
350
377
|
1
|
|
351
378
|
];
|
|
352
379
|
|
|
353
|
-
let mask =
|
|
380
|
+
let mask = edges;
|
|
354
381
|
|
|
355
382
|
for (let i = 0; i < steps.length - 1; i++) {
|
|
356
383
|
mask = graph_compute_pass({
|
|
@@ -384,15 +411,53 @@ export class GPUOutlineRenderer {
|
|
|
384
411
|
return { tile_active: last.mask_out, tiles: last.list };
|
|
385
412
|
}
|
|
386
413
|
|
|
414
|
+
/**
|
|
415
|
+
* List the tiles outside the flood that show a fill alone.
|
|
416
|
+
*
|
|
417
|
+
* @returns {number} the list, a `COLLECTION_U32`
|
|
418
|
+
*/
|
|
419
|
+
#graph_fill_tiles({ graph, specs, tile_labels, tile_active, tile_resolution, group_count }) {
|
|
420
|
+
const data = {};
|
|
421
|
+
|
|
422
|
+
const pass = graph.add(shader_outline_fill_tiles.descriptor.label, data, (data, resources, context) => {
|
|
423
|
+
shader_outline_fill_tiles.dispatch({
|
|
424
|
+
encoder: context.encoder,
|
|
425
|
+
group_count_x: group_count[0],
|
|
426
|
+
group_count_y: group_count[1],
|
|
427
|
+
group_count_z: group_count[2],
|
|
428
|
+
bindings: {
|
|
429
|
+
settings: { resolution: tile_resolution },
|
|
430
|
+
tile_labels: resources.get(data.tile_labels).obtainView(),
|
|
431
|
+
tile_active: resources.get(data.tile_active).obtainView(),
|
|
432
|
+
// read when the pass runs: the table replaces its buffer when it grows
|
|
433
|
+
specs: specs.buffer,
|
|
434
|
+
fill_tiles: resources.get(data.fill_tiles),
|
|
435
|
+
}
|
|
436
|
+
});
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
data.tile_labels = pass.read(tile_labels);
|
|
440
|
+
data.tile_active = pass.read(tile_active);
|
|
441
|
+
|
|
442
|
+
data.fill_tiles = pass.create('outline / fill tiles', BufferResourceDescriptor.from(
|
|
443
|
+
(COLLECTION_HEADER_WORDS + tile_resolution[0] * tile_resolution[1]) * 4,
|
|
444
|
+
GPUBufferUsage.STORAGE,
|
|
445
|
+
0, 16 // clear the count
|
|
446
|
+
));
|
|
447
|
+
|
|
448
|
+
return data.fill_tiles;
|
|
449
|
+
}
|
|
450
|
+
|
|
387
451
|
/**
|
|
388
452
|
* @param {FrameGraph} graph
|
|
389
453
|
* @param {number} tiles
|
|
454
|
+
* @param {string} label
|
|
390
455
|
* @returns {number} the tile quads' draw command
|
|
391
456
|
*/
|
|
392
|
-
#graph_tile_draw_command(graph, tiles) {
|
|
457
|
+
#graph_tile_draw_command(graph, tiles, label) {
|
|
393
458
|
const data = {};
|
|
394
459
|
|
|
395
|
-
const pass = graph.add(
|
|
460
|
+
const pass = graph.add(label, data, (data, resources, context) => {
|
|
396
461
|
const encoder = context.encoder;
|
|
397
462
|
|
|
398
463
|
build_indirect_draw_command(encoder.device, TILE_DRAW_COMMAND_PATTERN, encoder, resources.get(data.tiles), resources.get(data.command));
|
|
@@ -401,7 +466,7 @@ export class GPUOutlineRenderer {
|
|
|
401
466
|
data.tiles = pass.read(tiles);
|
|
402
467
|
|
|
403
468
|
// a buffer of its own: an indirect argument may not be bound writable in the scope that uses it
|
|
404
|
-
data.command = pass.create(
|
|
469
|
+
data.command = pass.create(label, BufferResourceDescriptor.from(
|
|
405
470
|
STRUCT_UNINDEXED_DRAW_COMMAND.aligned_size,
|
|
406
471
|
GPUBufferUsage.STORAGE | GPUBufferUsage.INDIRECT,
|
|
407
472
|
));
|
|
@@ -409,7 +474,7 @@ export class GPUOutlineRenderer {
|
|
|
409
474
|
return data.command;
|
|
410
475
|
}
|
|
411
476
|
|
|
412
|
-
#graph_composite({ frame, resolution, jitter, upscale, specs, tiles, draw, state, labels,
|
|
477
|
+
#graph_composite({ frame, resolution, jitter, upscale, specs, tiles, draw, state, labels, tile_labels }) {
|
|
413
478
|
const graph = frame.graph;
|
|
414
479
|
const scene = frame.get(SceneColor);
|
|
415
480
|
|
|
@@ -425,7 +490,7 @@ export class GPUOutlineRenderer {
|
|
|
425
490
|
tiles: resources.get(data.tiles),
|
|
426
491
|
state: resources.get(data.state).obtainView(),
|
|
427
492
|
labels: resources.get(data.labels).obtainView(),
|
|
428
|
-
|
|
493
|
+
tile_labels: resources.get(data.tile_labels).obtainView(),
|
|
429
494
|
// read when the pass runs: the table replaces its buffer when it grows
|
|
430
495
|
specs: specs.buffer,
|
|
431
496
|
mask_atlas: this.#atlas.obtainView(),
|
|
@@ -444,7 +509,7 @@ export class GPUOutlineRenderer {
|
|
|
444
509
|
data.draw = pass.read(draw);
|
|
445
510
|
data.state = pass.read(state);
|
|
446
511
|
data.labels = pass.read(labels);
|
|
447
|
-
data.
|
|
512
|
+
data.tile_labels = pass.read(tile_labels);
|
|
448
513
|
|
|
449
514
|
// read and written, because the outline blends over what is already there
|
|
450
515
|
data.color = pass.write(pass.read(scene.color));
|
|
@@ -452,6 +517,44 @@ export class GPUOutlineRenderer {
|
|
|
452
517
|
scene.color = data.color;
|
|
453
518
|
}
|
|
454
519
|
|
|
520
|
+
#graph_fill({ frame, resolution, jitter, upscale, specs, fill_tiles, fill_draw, tile_labels }) {
|
|
521
|
+
const graph = frame.graph;
|
|
522
|
+
const scene = frame.get(SceneColor);
|
|
523
|
+
|
|
524
|
+
const data = {};
|
|
525
|
+
|
|
526
|
+
const pass = graph.add('outline / fill', data, (data, resources, context) => {
|
|
527
|
+
const color = resources.get(data.color);
|
|
528
|
+
|
|
529
|
+
shader_outline_fill.draw({
|
|
530
|
+
encoder: context.encoder,
|
|
531
|
+
bindings: {
|
|
532
|
+
settings: { resolution, jitter, upscale },
|
|
533
|
+
tiles: resources.get(data.tiles),
|
|
534
|
+
tile_labels: resources.get(data.tile_labels).obtainView(),
|
|
535
|
+
// read when the pass runs: the table replaces its buffer when it grows
|
|
536
|
+
specs: specs.buffer,
|
|
537
|
+
mask_atlas: this.#atlas.obtainView(),
|
|
538
|
+
},
|
|
539
|
+
colorAttachments: [{
|
|
540
|
+
loadOp: "load",
|
|
541
|
+
storeOp: "store",
|
|
542
|
+
view: color.obtainView()
|
|
543
|
+
}],
|
|
544
|
+
color_formats: [color.descriptor.format],
|
|
545
|
+
command: resources.get(data.draw),
|
|
546
|
+
});
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
data.tiles = pass.read(fill_tiles);
|
|
550
|
+
data.draw = pass.read(fill_draw);
|
|
551
|
+
data.tile_labels = pass.read(tile_labels);
|
|
552
|
+
|
|
553
|
+
data.color = pass.write(pass.read(scene.color));
|
|
554
|
+
|
|
555
|
+
scene.color = data.color;
|
|
556
|
+
}
|
|
557
|
+
|
|
455
558
|
/**
|
|
456
559
|
* @param {GraphicsContext} graphics
|
|
457
560
|
* @param {GPUViewContext} view
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { WebGPUExtensionType } from "../../../shade/descriptor/WebGPUExtensionType.js";
|
|
2
|
+
import { CodeChunk } from "../../../shade/renderer/shader/compiler/CodeChunk.js";
|
|
3
|
+
import { chunk_outline_label_words } from "./chunk_outline_label_words.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The end of a classify pass: every lane of a tile's workgroup hands in its pixel's label, and the
|
|
7
|
+
* tile's label word and its labels are stored.
|
|
8
|
+
*
|
|
9
|
+
* - The tile's word, in `tile_labels`, is the label all its on-screen pixels hold, `OUTLINE_NONE`
|
|
10
|
+
* when that is no label, and `OUTLINE_TILE_MIXED` when they differ. A lane past the screen's edge
|
|
11
|
+
* takes no part.
|
|
12
|
+
* - The pixel's label, in `labels`, is stored only in a tile whose word is not `OUTLINE_NONE`, so
|
|
13
|
+
* `labels` is valid in those tiles and nowhere else: a reader checks the tile's word first.
|
|
14
|
+
*
|
|
15
|
+
* The word comes from the maximum and the minimum label over the tile, the minimum kept as the
|
|
16
|
+
* maximum of the complements so that zeroed workgroup memory is the identity of both. Each subgroup
|
|
17
|
+
* reduces first, so the workgroup's atomics are one per subgroup, not one per lane.
|
|
18
|
+
*
|
|
19
|
+
* Declares the workgroup variables it reduces through; reads `tile_labels` and `labels`, which every
|
|
20
|
+
* pass using it declares. Call from uniform control flow, by every lane.
|
|
21
|
+
*
|
|
22
|
+
* @type {CodeChunk}
|
|
23
|
+
*/
|
|
24
|
+
export const chunk_outline_classify_store = CodeChunk.from(
|
|
25
|
+
//language=WGSL
|
|
26
|
+
`
|
|
27
|
+
var<workgroup> outline_tile_highest: atomic<u32>;
|
|
28
|
+
var<workgroup> outline_tile_lowest_complement: atomic<u32>;
|
|
29
|
+
var<workgroup> outline_tile_word: u32;
|
|
30
|
+
|
|
31
|
+
fn outline_classify_store(tile: vec2<u32>, pixel: vec2<u32>, lane: u32, inside: bool, label: u32) {
|
|
32
|
+
// 0 is the identity of a maximum, so a lane off the screen contributes nothing
|
|
33
|
+
var highest = 0u;
|
|
34
|
+
var lowest_complement = 0u;
|
|
35
|
+
|
|
36
|
+
if (inside) {
|
|
37
|
+
highest = label;
|
|
38
|
+
lowest_complement = ~label;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let subgroup_highest = subgroupMax(highest);
|
|
42
|
+
let subgroup_lowest_complement = subgroupMax(lowest_complement);
|
|
43
|
+
|
|
44
|
+
if (subgroupElect()) {
|
|
45
|
+
atomicMax(&outline_tile_highest, subgroup_highest);
|
|
46
|
+
atomicMax(&outline_tile_lowest_complement, subgroup_lowest_complement);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
workgroupBarrier();
|
|
50
|
+
|
|
51
|
+
// one lane reads the reduction and hands the word on: an atomic load is a read-modify-write on
|
|
52
|
+
// some backends, and every lane loading the same word serialises on it
|
|
53
|
+
if (lane == 0u) {
|
|
54
|
+
let tile_highest = atomicLoad(&outline_tile_highest);
|
|
55
|
+
let tile_lowest = ~atomicLoad(&outline_tile_lowest_complement);
|
|
56
|
+
|
|
57
|
+
var tile_word = OUTLINE_TILE_MIXED;
|
|
58
|
+
|
|
59
|
+
if (tile_highest == tile_lowest) {
|
|
60
|
+
tile_word = tile_highest;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
outline_tile_word = tile_word;
|
|
64
|
+
|
|
65
|
+
textureStore(tile_labels, vec2<i32>(tile), vec4<u32>(tile_word, 0u, 0u, 0u));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
let word = workgroupUniformLoad(&outline_tile_word);
|
|
69
|
+
|
|
70
|
+
if (word != OUTLINE_NONE && inside) {
|
|
71
|
+
textureStore(labels, vec2<i32>(pixel), vec4<u32>(label, 0u, 0u, 0u));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
`, [chunk_outline_label_words]
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
chunk_outline_classify_store.addExtension(WebGPUExtensionType.Subgroups);
|
|
@@ -5,19 +5,13 @@ import {
|
|
|
5
5
|
} from "../../../shade/renderer/scene/MESH_OUTLINE_STRUCT.js";
|
|
6
6
|
import { SCENE_DATABASE_SPEC } from "../../../shade/renderer/scene/SCENE_DATABASE_SPEC.js";
|
|
7
7
|
import { CodeChunk } from "../../../shade/renderer/shader/compiler/CodeChunk.js";
|
|
8
|
+
import { chunk_outline_label_words } from "./chunk_outline_label_words.js";
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* The scene database's `mesh_outlines` table.
|
|
11
12
|
*/
|
|
12
13
|
export const MESH_OUTLINES_TABLE = SCENE_DATABASE_SPEC.get('mesh_outlines');
|
|
13
14
|
|
|
14
|
-
/**
|
|
15
|
-
* The word for "no label" and for "no row".
|
|
16
|
-
*
|
|
17
|
-
* @type {number}
|
|
18
|
-
*/
|
|
19
|
-
export const OUTLINE_NONE = 0xFFFFFFFF;
|
|
20
|
-
|
|
21
15
|
/**
|
|
22
16
|
* Which spec owns a pixel, if any.
|
|
23
17
|
*
|
|
@@ -40,7 +34,6 @@ export const OUTLINE_NONE = 0xFFFFFFFF;
|
|
|
40
34
|
export const chunk_outline_label = CodeChunk.from(
|
|
41
35
|
//language=WGSL
|
|
42
36
|
`
|
|
43
|
-
const OUTLINE_NONE: u32 = ${OUTLINE_NONE}u;
|
|
44
37
|
const MESH_OUTLINE_FLAG_VISIBLE: u32 = ${MESH_OUTLINE_FLAG_VISIBLE}u;
|
|
45
38
|
const MESH_OUTLINE_FLAG_HIDDEN: u32 = ${MESH_OUTLINE_FLAG_HIDDEN}u;
|
|
46
39
|
|
|
@@ -81,5 +74,5 @@ fn outline_record_of_row(database: ptr<storage, array<u32>>, row: u32) -> ${MESH
|
|
|
81
74
|
|
|
82
75
|
return ${MESH_OUTLINES_TABLE.marshalling_method_read}(database, row);
|
|
83
76
|
}
|
|
84
|
-
`, [MESH_OUTLINES_TABLE.chunk_read]
|
|
77
|
+
`, [chunk_outline_label_words, MESH_OUTLINES_TABLE.chunk_read]
|
|
85
78
|
);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { CodeChunk } from "../../../shade/renderer/shader/compiler/CodeChunk.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The word for "no label" and for "no row".
|
|
5
|
+
*
|
|
6
|
+
* @type {number}
|
|
7
|
+
*/
|
|
8
|
+
export const OUTLINE_NONE = 0xFFFFFFFF;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The label word of a tile whose pixels do not all hold the same label, "no label" counting as one.
|
|
12
|
+
* Any other tile's word is the label all its pixels hold, or {@link OUTLINE_NONE}. Spec indices stay
|
|
13
|
+
* below both reserved words.
|
|
14
|
+
*
|
|
15
|
+
* @type {number}
|
|
16
|
+
*/
|
|
17
|
+
export const OUTLINE_TILE_MIXED = 0xFFFFFFFE;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The reserved label words, for shaders.
|
|
21
|
+
*
|
|
22
|
+
* @type {CodeChunk}
|
|
23
|
+
*/
|
|
24
|
+
export const chunk_outline_label_words = CodeChunk.from(
|
|
25
|
+
//language=WGSL
|
|
26
|
+
`
|
|
27
|
+
const OUTLINE_NONE: u32 = ${OUTLINE_NONE}u;
|
|
28
|
+
const OUTLINE_TILE_MIXED: u32 = ${OUTLINE_TILE_MIXED}u;
|
|
29
|
+
`);
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
VISIBLE_MESHLETS_RESOURCE
|
|
10
10
|
} from "../../../shade/renderer/rasterize/visibility/chunk_visibility_decode.js";
|
|
11
11
|
import { MESH_OUTLINE_STRUCT } from "../../../shade/renderer/scene/MESH_OUTLINE_STRUCT.js";
|
|
12
|
+
import { chunk_outline_classify_store } from "./chunk_outline_classify_store.js";
|
|
12
13
|
import { chunk_outline_label } from "./chunk_outline_label.js";
|
|
13
14
|
import { OUTLINE_TILE_SIZE } from "./OUTLINE_TILE_SIZE.js";
|
|
14
15
|
|
|
@@ -27,15 +28,14 @@ resources.createGroup()
|
|
|
27
28
|
|
|
28
29
|
resources.createGroup()
|
|
29
30
|
.addStorageTexture("labels", "r32uint", "2d", "write-only")
|
|
30
|
-
.addStorageTexture("
|
|
31
|
+
.addStorageTexture("tile_labels", "r32uint", "2d", "write-only");
|
|
31
32
|
|
|
32
33
|
/**
|
|
33
34
|
* Labels every pixel with the spec that owns it, for a frame with no hidden specs: only the front
|
|
34
35
|
* surface's visible spec can own a pixel. See {@link chunk_outline_label}.
|
|
35
36
|
*
|
|
36
|
-
* One workgroup per screen tile, over the whole screen. Every tile writes
|
|
37
|
-
*
|
|
38
|
-
* nowhere else — a reader checks the tile first.
|
|
37
|
+
* One workgroup per screen tile, over the whole screen. Every tile writes its label word, and only a
|
|
38
|
+
* tile with a label writes its labels; see {@link chunk_outline_classify_store}.
|
|
39
39
|
*
|
|
40
40
|
* {@link shader_outline_classify_hidden} is the same pass for a frame that also drew the outline
|
|
41
41
|
* raster.
|
|
@@ -48,8 +48,6 @@ export const shader_outline_classify = ComputeShader.from({
|
|
|
48
48
|
body: CodeChunk.from(
|
|
49
49
|
//language=WGSL
|
|
50
50
|
`
|
|
51
|
-
var<workgroup> tile_labelled: atomic<u32>;
|
|
52
|
-
|
|
53
51
|
@compute @workgroup_size(${OUTLINE_TILE_SIZE}, ${OUTLINE_TILE_SIZE}, 1)
|
|
54
52
|
fn main(
|
|
55
53
|
@builtin(workgroup_id) tile: vec3<u32>,
|
|
@@ -76,23 +74,8 @@ fn main(
|
|
|
76
74
|
label = outline_label(front_row, outline_record_of_row(&scene_database, front_row), OUTLINE_NONE, nothing_behind);
|
|
77
75
|
}
|
|
78
76
|
|
|
79
|
-
|
|
80
|
-
if (label != OUTLINE_NONE) {
|
|
81
|
-
atomicOr(&tile_labelled, 1u);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
workgroupBarrier();
|
|
85
|
-
|
|
86
|
-
let labelled = atomicLoad(&tile_labelled);
|
|
87
|
-
|
|
88
|
-
if (lane == 0u) {
|
|
89
|
-
textureStore(tile_seeds, vec2<i32>(tile.xy), vec4<u32>(labelled, 0u, 0u, 0u));
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
if (labelled != 0u && inside) {
|
|
93
|
-
textureStore(labels, vec2<i32>(pixel), vec4<u32>(label, 0u, 0u, 0u));
|
|
94
|
-
}
|
|
77
|
+
outline_classify_store(tile.xy, pixel, lane, inside, label);
|
|
95
78
|
}
|
|
96
|
-
`, [chunk_visibility_decode, chunk_outline_label]
|
|
79
|
+
`, [chunk_visibility_decode, chunk_outline_label, chunk_outline_classify_store]
|
|
97
80
|
)
|
|
98
81
|
});
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
} from "../../../shade/renderer/rasterize/visibility/chunk_visibility_decode.js";
|
|
9
9
|
import { MESHLET_COLLECTION_STRUCT } from "../../../shade/renderer/rasterize/MESHLET_COLLECTION_STRUCT.js";
|
|
10
10
|
import { VISIBILITY_TRIANGLE_BITS } from "../../../shade/renderer/rasterize/visibility/VISIBILITY_BUFFER.js";
|
|
11
|
+
import { chunk_outline_classify_store } from "./chunk_outline_classify_store.js";
|
|
11
12
|
import { chunk_outline_label } from "./chunk_outline_label.js";
|
|
12
13
|
import { OUTLINE_TILE_SIZE } from "./OUTLINE_TILE_SIZE.js";
|
|
13
14
|
import { OUTLINE_CLASSIFY_SETTINGS } from "./shader_outline_classify.js";
|
|
@@ -27,7 +28,7 @@ resources.createGroup()
|
|
|
27
28
|
|
|
28
29
|
resources.createGroup()
|
|
29
30
|
.addStorageTexture("labels", "r32uint", "2d", "write-only")
|
|
30
|
-
.addStorageTexture("
|
|
31
|
+
.addStorageTexture("tile_labels", "r32uint", "2d", "write-only");
|
|
31
32
|
|
|
32
33
|
/**
|
|
33
34
|
* {@link shader_outline_classify} for a frame that drew the outline raster: a pixel the front surface
|
|
@@ -45,8 +46,6 @@ export const shader_outline_classify_hidden = ComputeShader.from({
|
|
|
45
46
|
body: CodeChunk.from(
|
|
46
47
|
//language=WGSL
|
|
47
48
|
`
|
|
48
|
-
var<workgroup> tile_labelled: atomic<u32>;
|
|
49
|
-
|
|
50
49
|
@compute @workgroup_size(${OUTLINE_TILE_SIZE}, ${OUTLINE_TILE_SIZE}, 1)
|
|
51
50
|
fn main(
|
|
52
51
|
@builtin(workgroup_id) tile: vec3<u32>,
|
|
@@ -86,23 +85,8 @@ fn main(
|
|
|
86
85
|
);
|
|
87
86
|
}
|
|
88
87
|
|
|
89
|
-
|
|
90
|
-
if (label != OUTLINE_NONE) {
|
|
91
|
-
atomicOr(&tile_labelled, 1u);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
workgroupBarrier();
|
|
95
|
-
|
|
96
|
-
let labelled = atomicLoad(&tile_labelled);
|
|
97
|
-
|
|
98
|
-
if (lane == 0u) {
|
|
99
|
-
textureStore(tile_seeds, vec2<i32>(tile.xy), vec4<u32>(labelled, 0u, 0u, 0u));
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
if (labelled != 0u && inside) {
|
|
103
|
-
textureStore(labels, vec2<i32>(pixel), vec4<u32>(label, 0u, 0u, 0u));
|
|
104
|
-
}
|
|
88
|
+
outline_classify_store(tile.xy, pixel, lane, inside, label);
|
|
105
89
|
}
|
|
106
|
-
`, [chunk_visibility_decode, chunk_outline_label]
|
|
90
|
+
`, [chunk_visibility_decode, chunk_outline_label, chunk_outline_classify_store]
|
|
107
91
|
)
|
|
108
92
|
});
|
|
@@ -9,6 +9,7 @@ import { ScreenTileShader } from "../../../shade/renderer/shader/tile/ScreenTile
|
|
|
9
9
|
import { vec2f, vec2u } from "../../../shade/renderer/shader/type/WEBGPU_WGSL_PRIMITIVE_TYPES.js";
|
|
10
10
|
import { WebGPUArray } from "../../../shade/renderer/shader/type/WebGPUArray.js";
|
|
11
11
|
import { WebGPUStruct } from "../../../shade/renderer/shader/type/WebGPUStruct.js";
|
|
12
|
+
import { chunk_outline_label_words } from "./chunk_outline_label_words.js";
|
|
12
13
|
import { chunk_outline_profile } from "./chunk_outline_profile.js";
|
|
13
14
|
import { OUTLINE_SPEC_STRUCT } from "./OUTLINE_SPEC_STRUCT.js";
|
|
14
15
|
import { OUTLINE_TILE_SIZE } from "./OUTLINE_TILE_SIZE.js";
|
|
@@ -33,13 +34,13 @@ resources.createGroup()
|
|
|
33
34
|
.addStorageBuffer("tiles", COLLECTION_U32_STRUCT_READ)
|
|
34
35
|
.addTexture("state", "uint")
|
|
35
36
|
.addTexture("labels", "uint")
|
|
36
|
-
.addTexture("
|
|
37
|
+
.addTexture("tile_labels", "uint")
|
|
37
38
|
.addStorageBuffer("specs", WebGPUArray.from(OUTLINE_SPEC_STRUCT))
|
|
38
39
|
.addTexture("mask_atlas", "unfilterable-float");
|
|
39
40
|
|
|
40
41
|
/**
|
|
41
|
-
* Places instance `i` on the `i`-th
|
|
42
|
-
* pixels do, so clip-space y is flipped.
|
|
42
|
+
* Places instance `i` on the `i`-th listed tile. Outline tiles count rows down from the top, as
|
|
43
|
+
* pixels do, so clip-space y is flipped. Reads `tiles` and `settings.resolution`.
|
|
43
44
|
*
|
|
44
45
|
* @type {CodeChunk}
|
|
45
46
|
*/
|
|
@@ -60,7 +61,13 @@ fn main(
|
|
|
60
61
|
`, [chunk_screen_tile_corner]
|
|
61
62
|
);
|
|
62
63
|
|
|
63
|
-
|
|
64
|
+
/**
|
|
65
|
+
* `outline_mask`: the factor a spec's mask multiplies its part by at an output pixel, read from
|
|
66
|
+
* `mask_atlas`, which every pass using it declares. See {@link chunk_outline_profile}.
|
|
67
|
+
*
|
|
68
|
+
* @type {CodeChunk}
|
|
69
|
+
*/
|
|
70
|
+
export const chunk_outline_mask = CodeChunk.from(
|
|
64
71
|
//language=WGSL
|
|
65
72
|
`
|
|
66
73
|
fn outline_mask(rect: vec2<u32>, scale_offset: vec2<f32>, at: vec2<u32>) -> f32 {
|
|
@@ -68,7 +75,12 @@ fn outline_mask(rect: vec2<u32>, scale_offset: vec2<f32>, at: vec2<u32>) -> f32
|
|
|
68
75
|
|
|
69
76
|
return outline_mask_factor(m, scale_offset);
|
|
70
77
|
}
|
|
78
|
+
`, [chunk_outline_profile]
|
|
79
|
+
);
|
|
71
80
|
|
|
81
|
+
const body = CodeChunk.from(
|
|
82
|
+
//language=WGSL
|
|
83
|
+
`
|
|
72
84
|
fn outline_label_of(seed: u32) -> u32 {
|
|
73
85
|
return textureLoad(labels, jump_flood_seed_unpack(seed), 0).r;
|
|
74
86
|
}
|
|
@@ -95,7 +107,7 @@ fn main(
|
|
|
95
107
|
let delta = 0.5 * (settings.upscale.x + settings.upscale.y);
|
|
96
108
|
|
|
97
109
|
// a covered pixel shows its own fill and nothing else
|
|
98
|
-
if (textureLoad(
|
|
110
|
+
if (textureLoad(tile_labels, pixel / ${OUTLINE_TILE_SIZE}, 0).r != OUTLINE_NONE) {
|
|
99
111
|
let label = textureLoad(labels, pixel, 0).r;
|
|
100
112
|
|
|
101
113
|
if (label != JUMP_FLOOD_NONE) {
|
|
@@ -129,15 +141,39 @@ fn main(
|
|
|
129
141
|
|
|
130
142
|
return outline_nearer_over(near, far, near_distance, far_distance, delta);
|
|
131
143
|
}
|
|
132
|
-
`, [chunk_jump_flood_seed_pair, chunk_outline_profile]
|
|
144
|
+
`, [chunk_jump_flood_seed_pair, chunk_outline_profile, chunk_outline_mask, chunk_outline_label_words]
|
|
133
145
|
);
|
|
134
146
|
|
|
135
147
|
/**
|
|
136
|
-
*
|
|
148
|
+
* The scene colour, blended premultiplied: `one, one-minus-src-alpha` over what is there.
|
|
149
|
+
*
|
|
150
|
+
* @type {GPUColorTargetState[]}
|
|
151
|
+
*/
|
|
152
|
+
export const OUTLINE_COMPOSITE_TARGETS = [
|
|
153
|
+
{
|
|
154
|
+
label: "color",
|
|
155
|
+
format: "rgba16float",
|
|
156
|
+
blend: BlendState.from({
|
|
157
|
+
color: {
|
|
158
|
+
operation: "add",
|
|
159
|
+
srcFactor: "one",
|
|
160
|
+
dstFactor: "one-minus-src-alpha"
|
|
161
|
+
},
|
|
162
|
+
alpha: {
|
|
163
|
+
operation: "add",
|
|
164
|
+
srcFactor: "one",
|
|
165
|
+
dstFactor: "one-minus-src-alpha"
|
|
166
|
+
}
|
|
167
|
+
})
|
|
168
|
+
}
|
|
169
|
+
];
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Paints the outlines and fills over the scene colour, on the flood's tiles.
|
|
137
173
|
*
|
|
138
174
|
* A pixel some spec covers shows that spec's fill; any other pixel shows the outlines of the two
|
|
139
|
-
* nearest specs, nearer on top (see {@link chunk_outline_profile}). Both are premultiplied
|
|
140
|
-
*
|
|
175
|
+
* nearest specs, nearer on top (see {@link chunk_outline_profile}). Both are premultiplied. The tiles
|
|
176
|
+
* the flood left out show a fill alone, and are drawn by `shader_outline_fill`.
|
|
141
177
|
*
|
|
142
178
|
* @type {ScreenTileShader}
|
|
143
179
|
*/
|
|
@@ -148,22 +184,5 @@ export const shader_outline_composite = new ScreenTileShader({
|
|
|
148
184
|
body
|
|
149
185
|
}),
|
|
150
186
|
vertex: chunk_outline_tile_vertex,
|
|
151
|
-
targets:
|
|
152
|
-
{
|
|
153
|
-
label: "color",
|
|
154
|
-
format: "rgba16float",
|
|
155
|
-
blend: BlendState.from({
|
|
156
|
-
color: {
|
|
157
|
-
operation: "add",
|
|
158
|
-
srcFactor: "one",
|
|
159
|
-
dstFactor: "one-minus-src-alpha"
|
|
160
|
-
},
|
|
161
|
-
alpha: {
|
|
162
|
-
operation: "add",
|
|
163
|
-
srcFactor: "one",
|
|
164
|
-
dstFactor: "one-minus-src-alpha"
|
|
165
|
-
}
|
|
166
|
-
})
|
|
167
|
-
}
|
|
168
|
-
]
|
|
187
|
+
targets: OUTLINE_COMPOSITE_TARGETS
|
|
169
188
|
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { COLLECTION_U32_STRUCT_READ } from "../../../shade/renderer/gpu_primitive/COLLECTION_U32_STRUCT_READ.js";
|
|
2
|
+
import { CodeChunk } from "../../../shade/renderer/shader/compiler/CodeChunk.js";
|
|
3
|
+
import { ShaderResourceSetDescriptor } from "../../../shade/renderer/shader/resource/ShaderResourceSetDescriptor.js";
|
|
4
|
+
import { ShaderDescriptor } from "../../../shade/renderer/shader/ShaderDescriptor.js";
|
|
5
|
+
import { ScreenTileShader } from "../../../shade/renderer/shader/tile/ScreenTileShader.js";
|
|
6
|
+
import { WebGPUArray } from "../../../shade/renderer/shader/type/WebGPUArray.js";
|
|
7
|
+
import { chunk_outline_profile } from "./chunk_outline_profile.js";
|
|
8
|
+
import { OUTLINE_SPEC_STRUCT } from "./OUTLINE_SPEC_STRUCT.js";
|
|
9
|
+
import { OUTLINE_TILE_SIZE } from "./OUTLINE_TILE_SIZE.js";
|
|
10
|
+
import {
|
|
11
|
+
chunk_outline_mask,
|
|
12
|
+
chunk_outline_tile_vertex,
|
|
13
|
+
OUTLINE_COMPOSITE_SETTINGS,
|
|
14
|
+
OUTLINE_COMPOSITE_TARGETS
|
|
15
|
+
} from "./shader_outline_composite.js";
|
|
16
|
+
|
|
17
|
+
const resources = new ShaderResourceSetDescriptor();
|
|
18
|
+
|
|
19
|
+
resources.setImmediate("settings", OUTLINE_COMPOSITE_SETTINGS);
|
|
20
|
+
|
|
21
|
+
resources.createGroup()
|
|
22
|
+
.addStorageBuffer("tiles", COLLECTION_U32_STRUCT_READ)
|
|
23
|
+
.addTexture("tile_labels", "uint")
|
|
24
|
+
.addStorageBuffer("specs", WebGPUArray.from(OUTLINE_SPEC_STRUCT))
|
|
25
|
+
.addTexture("mask_atlas", "unfilterable-float");
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Paints the fill over the tiles the flood left out, listed by `shader_outline_fill_tiles`: every
|
|
29
|
+
* pixel of such a tile holds the tile's one label and no other label is within reach, so it shows
|
|
30
|
+
* that label's fill and nothing else — what `shader_outline_composite` would draw there, without the
|
|
31
|
+
* flood's state.
|
|
32
|
+
*
|
|
33
|
+
* @type {ScreenTileShader}
|
|
34
|
+
*/
|
|
35
|
+
export const shader_outline_fill = new ScreenTileShader({
|
|
36
|
+
descriptor: ShaderDescriptor.from({
|
|
37
|
+
label: "outline / fill",
|
|
38
|
+
resources,
|
|
39
|
+
body: CodeChunk.from(
|
|
40
|
+
//language=WGSL
|
|
41
|
+
`
|
|
42
|
+
@fragment
|
|
43
|
+
fn main(
|
|
44
|
+
@builtin(position) coord : vec4<f32>,
|
|
45
|
+
) -> @location(0) vec4<f32> {
|
|
46
|
+
let pixel = vec2<i32>(coord.xy);
|
|
47
|
+
|
|
48
|
+
let spec = specs[textureLoad(tile_labels, pixel / ${OUTLINE_TILE_SIZE}, 0).r];
|
|
49
|
+
|
|
50
|
+
let mask_at = outline_mask_coordinate(pixel, settings.jitter, settings.upscale);
|
|
51
|
+
|
|
52
|
+
return outline_fill(spec) * outline_mask(spec.fill_mask_rect, spec.fill_mask_fma, mask_at);
|
|
53
|
+
}
|
|
54
|
+
`, [chunk_outline_profile, chunk_outline_mask]
|
|
55
|
+
)
|
|
56
|
+
}),
|
|
57
|
+
vertex: chunk_outline_tile_vertex,
|
|
58
|
+
targets: OUTLINE_COMPOSITE_TARGETS
|
|
59
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { COLLECTION_U32_STRUCT_WRITE } from "../../../shade/renderer/gpu_primitive/COLLECTION_U32_STRUCT_WRITE.js";
|
|
2
|
+
import { CodeChunk } from "../../../shade/renderer/shader/compiler/CodeChunk.js";
|
|
3
|
+
import { ComputeShader } from "../../../shade/renderer/shader/ComputeShader.js";
|
|
4
|
+
import { ShaderResourceSetDescriptor } from "../../../shade/renderer/shader/resource/ShaderResourceSetDescriptor.js";
|
|
5
|
+
import { WebGPUArray } from "../../../shade/renderer/shader/type/WebGPUArray.js";
|
|
6
|
+
import { chunk_outline_label_words } from "./chunk_outline_label_words.js";
|
|
7
|
+
import { OUTLINE_SPEC_STRUCT } from "./OUTLINE_SPEC_STRUCT.js";
|
|
8
|
+
import { OUTLINE_TILE_GRID_GROUP_SIZE, OUTLINE_TILE_GRID_SETTINGS } from "./shader_outline_tile_edges.js";
|
|
9
|
+
|
|
10
|
+
const resources = new ShaderResourceSetDescriptor();
|
|
11
|
+
|
|
12
|
+
resources.setImmediate("settings", OUTLINE_TILE_GRID_SETTINGS);
|
|
13
|
+
|
|
14
|
+
resources.createGroup()
|
|
15
|
+
.addTexture("tile_labels", "uint")
|
|
16
|
+
.addTexture("tile_active", "uint")
|
|
17
|
+
.addStorageBuffer("specs", WebGPUArray.from(OUTLINE_SPEC_STRUCT));
|
|
18
|
+
|
|
19
|
+
resources.createGroup()
|
|
20
|
+
.addStorageBuffer("fill_tiles", COLLECTION_U32_STRUCT_WRITE, true);
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Lists the tiles the fill pass draws: those the flood left out whose one label has a fill to draw,
|
|
24
|
+
* packed `x | y << 16`, in no particular order.
|
|
25
|
+
*
|
|
26
|
+
* A tile outside the flood has no other label within reach (else an edge tile of that label would be
|
|
27
|
+
* within reach of it, and the flood would hold it), so its pixels show their own label's fill and
|
|
28
|
+
* nothing else. A mixed tile is always an edge, so always in the flood; a tile with no label shows
|
|
29
|
+
* nothing; and a fill of zero alpha draws nothing, so its tiles are not listed at all.
|
|
30
|
+
*
|
|
31
|
+
* The list's count has to start at zero, and the list must hold every tile of the grid.
|
|
32
|
+
*
|
|
33
|
+
* @type {ComputeShader}
|
|
34
|
+
*/
|
|
35
|
+
export const shader_outline_fill_tiles = ComputeShader.from({
|
|
36
|
+
label: "outline / fill tiles",
|
|
37
|
+
resources,
|
|
38
|
+
body: CodeChunk.from(
|
|
39
|
+
//language=WGSL
|
|
40
|
+
`
|
|
41
|
+
@compute @workgroup_size(${OUTLINE_TILE_GRID_GROUP_SIZE}, ${OUTLINE_TILE_GRID_GROUP_SIZE}, 1)
|
|
42
|
+
fn main(@builtin(global_invocation_id) id: vec3<u32>) {
|
|
43
|
+
if (any(id.xy >= settings.resolution)) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
let cell = vec2<i32>(id.xy);
|
|
48
|
+
|
|
49
|
+
if (textureLoad(tile_active, cell, 0).r != 0u) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let label = textureLoad(tile_labels, cell, 0).r;
|
|
54
|
+
|
|
55
|
+
// one label: the reserved words are the two largest
|
|
56
|
+
if (label >= OUTLINE_TILE_MIXED) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (specs[label].fill_color.a <= 0.0) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
let slot = atomicAdd(&fill_tiles.count, 1u);
|
|
65
|
+
|
|
66
|
+
fill_tiles.elements[slot] = id.x | (id.y << 16u);
|
|
67
|
+
}
|
|
68
|
+
`, [chunk_outline_label_words]
|
|
69
|
+
)
|
|
70
|
+
});
|
|
@@ -6,6 +6,7 @@ import { ComputeShader } from "../../../shade/renderer/shader/ComputeShader.js";
|
|
|
6
6
|
import { ShaderResourceSetDescriptor } from "../../../shade/renderer/shader/resource/ShaderResourceSetDescriptor.js";
|
|
7
7
|
import { u32, vec2u } from "../../../shade/renderer/shader/type/WEBGPU_WGSL_PRIMITIVE_TYPES.js";
|
|
8
8
|
import { WebGPUStruct } from "../../../shade/renderer/shader/type/WebGPUStruct.js";
|
|
9
|
+
import { chunk_outline_label_words } from "./chunk_outline_label_words.js";
|
|
9
10
|
import { chunk_outline_tile_pixel } from "./chunk_outline_tile_pixel.js";
|
|
10
11
|
import { OUTLINE_TILE_SIZE } from "./OUTLINE_TILE_SIZE.js";
|
|
11
12
|
|
|
@@ -23,7 +24,7 @@ resources.setImmediate("settings", OUTLINE_FLOOD_SETTINGS);
|
|
|
23
24
|
|
|
24
25
|
resources.createGroup()
|
|
25
26
|
.addTexture("labels", "uint")
|
|
26
|
-
.addTexture("
|
|
27
|
+
.addTexture("tile_labels", "uint")
|
|
27
28
|
.addStorageBuffer("tiles", COLLECTION_U32_STRUCT_READ);
|
|
28
29
|
|
|
29
30
|
resources.createGroup()
|
|
@@ -34,7 +35,8 @@ resources.createGroup()
|
|
|
34
35
|
* is a seed, at its own position. Writes each active pixel's two seeds (see
|
|
35
36
|
* {@link chunk_jump_flood_seed_pair}) for {@link shader_outline_flood_step} to refine.
|
|
36
37
|
*
|
|
37
|
-
* Labels are valid only inside tiles that hold one, so a tap is read only where its tile
|
|
38
|
+
* Labels are valid only inside tiles that hold one, so a tap is read only where its tile's label word
|
|
39
|
+
* says so.
|
|
38
40
|
*
|
|
39
41
|
* @type {ComputeShader}
|
|
40
42
|
*/
|
|
@@ -66,7 +68,7 @@ fn main(
|
|
|
66
68
|
continue;
|
|
67
69
|
}
|
|
68
70
|
|
|
69
|
-
if (textureLoad(
|
|
71
|
+
if (textureLoad(tile_labels, at / ${OUTLINE_TILE_SIZE}, 0).r == OUTLINE_NONE) {
|
|
70
72
|
continue;
|
|
71
73
|
}
|
|
72
74
|
|
|
@@ -83,6 +85,6 @@ fn main(
|
|
|
83
85
|
|
|
84
86
|
textureStore(state_out, pixel, vec4<u32>(pair.seed_0, pair.seed_1, 0u, 0u));
|
|
85
87
|
}
|
|
86
|
-
`, [chunk_jump_flood_tap, chunk_jump_flood_seed_pair, chunk_outline_tile_pixel]
|
|
88
|
+
`, [chunk_jump_flood_tap, chunk_jump_flood_seed_pair, chunk_outline_tile_pixel, chunk_outline_label_words]
|
|
87
89
|
)
|
|
88
90
|
});
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { chunk_jump_flood_tap } from "../../../shade/renderer/gpu_primitive/jump_flood/chunk_jump_flood_tap.js";
|
|
2
|
+
import { CodeChunk } from "../../../shade/renderer/shader/compiler/CodeChunk.js";
|
|
3
|
+
import { ComputeShader } from "../../../shade/renderer/shader/ComputeShader.js";
|
|
4
|
+
import { ShaderResourceSetDescriptor } from "../../../shade/renderer/shader/resource/ShaderResourceSetDescriptor.js";
|
|
5
|
+
import { vec2u } from "../../../shade/renderer/shader/type/WEBGPU_WGSL_PRIMITIVE_TYPES.js";
|
|
6
|
+
import { WebGPUStruct } from "../../../shade/renderer/shader/type/WebGPUStruct.js";
|
|
7
|
+
import { chunk_outline_label_words } from "./chunk_outline_label_words.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Tiles per workgroup side, for the passes that run over the tile grid one tile a lane.
|
|
11
|
+
*
|
|
12
|
+
* @type {number}
|
|
13
|
+
*/
|
|
14
|
+
export const OUTLINE_TILE_GRID_GROUP_SIZE = 8;
|
|
15
|
+
|
|
16
|
+
export const OUTLINE_TILE_GRID_SETTINGS = WebGPUStruct.from({
|
|
17
|
+
/**
|
|
18
|
+
* The tile grid's size, in tiles.
|
|
19
|
+
*/
|
|
20
|
+
resolution: vec2u,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const resources = new ShaderResourceSetDescriptor();
|
|
24
|
+
|
|
25
|
+
resources.setImmediate("settings", OUTLINE_TILE_GRID_SETTINGS);
|
|
26
|
+
|
|
27
|
+
resources.createGroup()
|
|
28
|
+
.addTexture("tile_labels", "uint");
|
|
29
|
+
|
|
30
|
+
resources.createGroup()
|
|
31
|
+
.addStorageTexture("edges", "r32uint", "2d", "write-only");
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Marks the tiles a silhouette passes through, from the classify pass's tile label words: the tiles
|
|
35
|
+
* the jump flood has to start from.
|
|
36
|
+
*
|
|
37
|
+
* The flood looks for each pixel's nearest pixel of a label, and that pixel always lies on the
|
|
38
|
+
* label's boundary. Were all eight neighbours of a pixel `q` of label `L` also `L`, then for any pixel
|
|
39
|
+
* `p` outside `L` the neighbour one step from `q` towards `p` would be `L` and nearer to `p`; so the
|
|
40
|
+
* nearest `q` has a neighbour of another label, and that neighbour lies in the box between `q` and `p`,
|
|
41
|
+
* so on the screen. The tile holding `q` therefore holds another label too, or touches a tile that
|
|
42
|
+
* does:
|
|
43
|
+
*
|
|
44
|
+
* - a tile whose word is `OUTLINE_TILE_MIXED` is an edge;
|
|
45
|
+
* - a tile of one label is an edge when any of its eight neighbours on the grid has another word —
|
|
46
|
+
* including a silhouette that runs along tile borders, where neither side is mixed;
|
|
47
|
+
* - a tile with no label holds no seed and is never an edge.
|
|
48
|
+
*
|
|
49
|
+
* Tiles off the grid do not count: the neighbour above lies on the screen.
|
|
50
|
+
*
|
|
51
|
+
* @type {ComputeShader}
|
|
52
|
+
*/
|
|
53
|
+
export const shader_outline_tile_edges = ComputeShader.from({
|
|
54
|
+
label: "outline / tile edges",
|
|
55
|
+
resources,
|
|
56
|
+
body: CodeChunk.from(
|
|
57
|
+
//language=WGSL
|
|
58
|
+
`
|
|
59
|
+
@compute @workgroup_size(${OUTLINE_TILE_GRID_GROUP_SIZE}, ${OUTLINE_TILE_GRID_GROUP_SIZE}, 1)
|
|
60
|
+
fn main(@builtin(global_invocation_id) id: vec3<u32>) {
|
|
61
|
+
if (any(id.xy >= settings.resolution)) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let cell = vec2<i32>(id.xy);
|
|
66
|
+
|
|
67
|
+
let own = textureLoad(tile_labels, cell, 0).r;
|
|
68
|
+
|
|
69
|
+
var edge = select(0u, 1u, own == OUTLINE_TILE_MIXED);
|
|
70
|
+
|
|
71
|
+
// one label: the reserved words are the two largest
|
|
72
|
+
if (own < OUTLINE_TILE_MIXED) {
|
|
73
|
+
let extent = vec2<i32>(settings.resolution);
|
|
74
|
+
|
|
75
|
+
for (var tap = 0u; tap < JUMP_FLOOD_TAP_COUNT; tap++) {
|
|
76
|
+
let at = cell + jump_flood_tap(tap, 1u);
|
|
77
|
+
|
|
78
|
+
if (any(at < vec2<i32>(0)) || any(at >= extent)) {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (textureLoad(tile_labels, at, 0).r != own) {
|
|
83
|
+
edge = 1u;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
textureStore(edges, cell, vec4<u32>(edge, 0u, 0u, 0u));
|
|
89
|
+
}
|
|
90
|
+
`, [chunk_jump_flood_tap, chunk_outline_label_words]
|
|
91
|
+
)
|
|
92
|
+
});
|