@aethervtt/shard-render 0.1.0 → 0.2.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.
Files changed (63) hide show
  1. package/dist/ablation.d.ts +74 -0
  2. package/dist/ablation.js +117 -0
  3. package/dist/atmosphere-nodes.js +100 -12
  4. package/dist/atmosphere-shaders.js +17 -5
  5. package/dist/baseline/exposure.d.ts +1 -1
  6. package/dist/baseline/exposure.js +18 -5
  7. package/dist/crowd.d.ts +21 -0
  8. package/dist/crowd.js +121 -0
  9. package/dist/decals-plugin.d.ts +5 -0
  10. package/dist/decals-plugin.js +54 -0
  11. package/dist/decals.d.ts +69 -0
  12. package/dist/decals.js +326 -0
  13. package/dist/deferred.js +4 -1
  14. package/dist/display-nodes.js +24 -11
  15. package/dist/foliage-budget.d.ts +64 -0
  16. package/dist/foliage-budget.js +108 -0
  17. package/dist/foliage-path.d.ts +19 -0
  18. package/dist/foliage-path.js +7 -0
  19. package/dist/foliage-shaders.d.ts +11 -0
  20. package/dist/foliage-shaders.js +504 -0
  21. package/dist/foliage.d.ts +194 -0
  22. package/dist/foliage.js +1027 -0
  23. package/dist/forward.js +26 -2
  24. package/dist/graph.d.ts +34 -0
  25. package/dist/graph.js +139 -14
  26. package/dist/index.d.ts +11 -1
  27. package/dist/index.js +57 -0
  28. package/dist/overlay-registry.d.ts +3 -1
  29. package/dist/overlay-registry.js +3 -1
  30. package/dist/overlays.js +5 -2
  31. package/dist/perf-overlay.d.ts +9 -0
  32. package/dist/perf-overlay.js +125 -0
  33. package/dist/plugin.d.ts +11 -1
  34. package/dist/plugin.js +33 -2
  35. package/dist/post-common.d.ts +20 -2
  36. package/dist/post-common.js +79 -2
  37. package/dist/post-nodes.d.ts +5 -0
  38. package/dist/post-nodes.js +136 -104
  39. package/dist/post-scene.d.ts +130 -0
  40. package/dist/post-scene.js +164 -0
  41. package/dist/post-shaders.js +41 -63
  42. package/dist/post.js +11 -1
  43. package/dist/ribbons-plugin.d.ts +5 -0
  44. package/dist/ribbons-plugin.js +59 -0
  45. package/dist/ribbons.d.ts +44 -0
  46. package/dist/ribbons.js +288 -0
  47. package/dist/scenario.d.ts +70 -0
  48. package/dist/scenario.js +125 -0
  49. package/dist/shaders.js +58 -5
  50. package/dist/shadows.d.ts +2 -1
  51. package/dist/shadows.js +3 -1
  52. package/dist/standard.js +6 -0
  53. package/dist/testing.d.ts +1 -0
  54. package/dist/testing.js +6 -0
  55. package/dist/timer.d.ts +16 -1
  56. package/dist/timer.js +66 -19
  57. package/dist/vfx-draw.d.ts +37 -0
  58. package/dist/vfx-draw.js +291 -0
  59. package/dist/vfx-state.d.ts +42 -0
  60. package/dist/vfx-state.js +81 -0
  61. package/dist/vfx.d.ts +69 -0
  62. package/dist/vfx.js +103 -0
  63. package/package.json +19 -11
@@ -0,0 +1,1027 @@
1
+ import {
2
+ defineResource,
3
+ defineSystem,
4
+ PostUpdate,
5
+ ProfilerResource,
6
+ ShardError
7
+ } from "@aethervtt/shard-core";
8
+ import {
9
+ definePlugin,
10
+ PerfBudgets,
11
+ PerfScenario,
12
+ scenarioSliceMs
13
+ } from "@aethervtt/shard-runtime";
14
+ import { Materials, Meshes } from "./assets.js";
15
+ import { addRenderFeatures } from "./features.js";
16
+ import {
17
+ CoveredGpuTime,
18
+ FOLIAGE_SPAN_KEY,
19
+ FoliageBudget,
20
+ FoliageController
21
+ } from "./foliage-budget.js";
22
+ import { FoliagePath } from "./foliage-path.js";
23
+ import {
24
+ CHUNK_FLOATS,
25
+ CULL_PARAMS_FLOATS,
26
+ FOLIAGE_SHADERS,
27
+ MAX_FOLIAGE_MESHES,
28
+ PATCH_FLOATS,
29
+ PLACE_PARAMS_BYTES
30
+ } from "./foliage-shaders.js";
31
+ import {
32
+ ForwardStateResource,
33
+ INTERIOR_DEFINES,
34
+ PASS_GBUFFER,
35
+ PASS_OPAQUE,
36
+ sceneColor,
37
+ VERTEX_BUFFERS,
38
+ viewBindGroup
39
+ } from "./forward.js";
40
+ import { GpuAssetsResource } from "./gpu-assets.js";
41
+ import { RenderPhase } from "./graph.js";
42
+ import { materialVariant, typeOrdinal, variantBlend, variantCull } from "./material-pipelines.js";
43
+ import { Graph, Shaders } from "./plugin.js";
44
+ import { registerShaders } from "./shaders.js";
45
+ import { cameraOf } from "./view.js";
46
+ const scratchParams = new ArrayBuffer(PLACE_PARAMS_BYTES);
47
+ const paramsU32 = new Uint32Array(scratchParams);
48
+ const paramsI32 = new Int32Array(scratchParams);
49
+ const paramsF32 = new Float32Array(scratchParams);
50
+ const cullData = new Float32Array(CULL_PARAMS_FLOATS);
51
+ const cullU32 = new Uint32Array(cullData.buffer);
52
+ const drawData = new Float32Array(8);
53
+ class FoliageLayer {
54
+ options;
55
+ perChunk;
56
+ /** What draws: each mesh's levels in order (`meshes[m]` then `lods[m]`), and where each mesh's start. */
57
+ drawables = [];
58
+ levels;
59
+ /** Chunks in the layer: their inputs (kept to place again after growth or device loss). */
60
+ chunks = /* @__PURE__ */ new Map();
61
+ /** Per slot: chunk → origin-relative world rows (12), bounds (min xyz, size xyz), active. */
62
+ records = new Float32Array(CHUNK_FLOATS * 8);
63
+ slots = 0;
64
+ free = [];
65
+ pending = [];
66
+ /** Buffers, made in the place node. */
67
+ instances;
68
+ chunkBuffer;
69
+ meshInfo;
70
+ levelBuffer;
71
+ /** Slots the instance buffer holds, and its device generation. */
72
+ capacitySlots = 0;
73
+ generation = -1;
74
+ views = /* @__PURE__ */ new Map();
75
+ /** Largest mesh bounding radius (m, unscaled): culling's sphere. */
76
+ meshRadius = 1;
77
+ disposed = false;
78
+ constructor(options) {
79
+ if (options.meshes.length === 0 || options.meshes.length > MAX_FOLIAGE_MESHES) {
80
+ throw new ShardError(
81
+ "render/foliage-meshes",
82
+ `A foliage layer draws 1 to ${MAX_FOLIAGE_MESHES} meshes, not ${options.meshes.length}`,
83
+ { hint: "Fewer variants: 4\u20138 read as unique once scaled and rotated." }
84
+ );
85
+ }
86
+ this.options = options;
87
+ this.perChunk = Math.max(1, options.cells * options.cells);
88
+ this.levels = new Uint32Array(options.meshes.length * 2);
89
+ for (let m = 0; m < options.meshes.length; m++) {
90
+ const lods = options.lods?.[m] ?? [];
91
+ this.levels[m * 2] = this.drawables.length;
92
+ this.levels[m * 2 + 1] = 1 + lods.length;
93
+ this.drawables.push(options.meshes[m], ...lods);
94
+ }
95
+ }
96
+ /** A free chunk slot. */
97
+ allocate() {
98
+ const slot = this.free.length > 0 ? this.free.pop() : this.slots++;
99
+ const need = (slot + 1) * CHUNK_FLOATS;
100
+ if (this.records.length < need) {
101
+ const grown = new Float32Array(Math.max(need, this.records.length * 2));
102
+ grown.set(this.records);
103
+ this.records = grown;
104
+ }
105
+ return slot;
106
+ }
107
+ /** Places a chunk in a slot (on the GPU, in the next frame's place pass). */
108
+ setChunk(slot, chunk) {
109
+ const n = chunk.grid * chunk.grid;
110
+ if (chunk.positions.length < n * 3 || chunk.normals.length < n * 3 || chunk.density.length < n) {
111
+ throw new ShardError(
112
+ "render/foliage-patch",
113
+ `A foliage chunk's patch has fewer than ${n} vertices`,
114
+ {
115
+ hint: "positions and normals: 3 floats per grid point; density: 1."
116
+ }
117
+ );
118
+ }
119
+ this.chunks.set(slot, chunk);
120
+ let x0 = Infinity;
121
+ let y0 = Infinity;
122
+ let z0 = Infinity;
123
+ let x1 = -Infinity;
124
+ let y1 = -Infinity;
125
+ let z1 = -Infinity;
126
+ const p = chunk.positions;
127
+ for (let i = 0; i < n; i++) {
128
+ x0 = Math.min(x0, p[i * 3]);
129
+ y0 = Math.min(y0, p[i * 3 + 1]);
130
+ z0 = Math.min(z0, p[i * 3 + 2]);
131
+ x1 = Math.max(x1, p[i * 3]);
132
+ y1 = Math.max(y1, p[i * 3 + 1]);
133
+ z1 = Math.max(z1, p[i * 3 + 2]);
134
+ }
135
+ const r = this.records;
136
+ const o = slot * CHUNK_FLOATS;
137
+ r[o + 12] = x0 - 0.01;
138
+ r[o + 13] = y0 - 0.01;
139
+ r[o + 14] = z0 - 0.01;
140
+ r[o + 15] = 0;
141
+ r[o + 16] = x1 - x0 + 0.02;
142
+ r[o + 17] = y1 - y0 + 0.02;
143
+ r[o + 18] = z1 - z0 + 0.02;
144
+ this.queue(slot, chunk);
145
+ }
146
+ queue(slot, chunk) {
147
+ for (const p of this.pending) {
148
+ if (p.slot === slot) {
149
+ p.chunk = chunk;
150
+ return;
151
+ }
152
+ }
153
+ this.pending.push({ slot, chunk });
154
+ }
155
+ /** Frees a slot: its instances stop drawing at once. */
156
+ removeChunk(slot) {
157
+ if (!this.chunks.delete(slot)) return;
158
+ this.records[slot * CHUNK_FLOATS + 15] = 0;
159
+ for (let i = this.pending.length - 1; i >= 0; i--)
160
+ if (this.pending[i].slot === slot) this.pending.splice(i, 1);
161
+ this.free.push(slot);
162
+ }
163
+ /**
164
+ * The chunk's frame in the world, origin-relative, as an affine's three rows (12 floats,
165
+ * row-major: rotation and translation). Set it every frame the frame moves (an origin shift).
166
+ */
167
+ setTransform(slot, rows) {
168
+ const o = slot * CHUNK_FLOATS;
169
+ for (let i = 0; i < 12; i++) this.records[o + i] = rows[i];
170
+ }
171
+ /** Chunk slots in use. */
172
+ get chunkCount() {
173
+ return this.chunks.size;
174
+ }
175
+ /** Instances this layer can hold. */
176
+ get capacity() {
177
+ return this.capacitySlots * this.perChunk;
178
+ }
179
+ /** The last read-back instance counts for a camera: in view, and casting shadows. */
180
+ visible(view) {
181
+ const v = this.views.get(view);
182
+ if (!v) return { drawn: 0, shadows: 0 };
183
+ const m = this.drawables.length;
184
+ let drawn = 0;
185
+ let shadows = 0;
186
+ for (let i = 0; i < m; i++) {
187
+ drawn += v.read_[i] ?? 0;
188
+ shadows += v.read_[m + i] ?? 0;
189
+ }
190
+ return { drawn, shadows };
191
+ }
192
+ /**
193
+ * The last read-back counts per drawable for a camera (`drawables[d]`: a mesh or one of its
194
+ * LODs), in view and casting shadows.
195
+ */
196
+ visibleByDrawable(view) {
197
+ const v = this.views.get(view);
198
+ const m = this.drawables.length;
199
+ const drawn = [];
200
+ const shadows = [];
201
+ for (let i = 0; i < m; i++) {
202
+ drawn.push(v?.read_[i] ?? 0);
203
+ shadows.push(v?.read_[m + i] ?? 0);
204
+ }
205
+ return { drawn, shadows };
206
+ }
207
+ /** Slot records for the GPU this frame. */
208
+ get chunkRecords() {
209
+ return this.records;
210
+ }
211
+ get slotCount() {
212
+ return this.slots;
213
+ }
214
+ /** Releases its GPU buffers (the layer stops drawing). */
215
+ dispose() {
216
+ this.disposed = true;
217
+ this.instances?.destroy();
218
+ this.chunkBuffer?.destroy();
219
+ this.meshInfo?.destroy();
220
+ this.levelBuffer?.destroy();
221
+ for (const v of this.views.values()) {
222
+ v.visible.destroy();
223
+ v.args.destroy();
224
+ v.classes.destroy();
225
+ v.counts.destroy();
226
+ v.params.destroy();
227
+ v.draw.destroy();
228
+ v.readback?.destroy();
229
+ }
230
+ this.views.clear();
231
+ }
232
+ }
233
+ class FoliageLayerSet {
234
+ layers = /* @__PURE__ */ new Set();
235
+ /** Density and range under the budget (0075, `FoliageBudget`): full detail without a target. */
236
+ steer = new FoliageController();
237
+ /** Foliage's GPU time a frame: every `gpu:foliage/*` pass (0074's covers rule). */
238
+ gpuTime = new CoveredGpuTime(FOLIAGE_SPAN_KEY);
239
+ /** The scenario slice the target came from, kept until the scenario or budgets change. */
240
+ sliceFor = null;
241
+ sliceData;
242
+ sliceMs = 0;
243
+ /** The shared GPU objects (pipelines, staging buffers), made on first use. */
244
+ gpu;
245
+ add(layer) {
246
+ this.layers.add(layer);
247
+ return layer;
248
+ }
249
+ remove(layer) {
250
+ if (!this.layers.delete(layer)) return;
251
+ layer.dispose();
252
+ }
253
+ /** Releases every layer and the shared buffers (the app is going away). */
254
+ dispose() {
255
+ for (const layer of this.layers) layer.dispose();
256
+ this.layers.clear();
257
+ this.gpu?.params.destroy();
258
+ this.gpu?.patch.destroy();
259
+ this.gpu?.avoid.destroy();
260
+ this.gpu = void 0;
261
+ }
262
+ }
263
+ const FoliageLayers = defineResource("render/FoliageLayers", {
264
+ description: "GPU foliage layers (0045): instances placed, culled and drawn on the GPU.",
265
+ init: () => new FoliageLayerSet()
266
+ });
267
+ function foliageGpu(gpu, set) {
268
+ if (set.gpu && set.gpu.generation === gpu.generation) return set.gpu;
269
+ const d = gpu.device;
270
+ const storage = (binding, type, visibility) => ({
271
+ binding,
272
+ visibility,
273
+ buffer: { type }
274
+ });
275
+ const C = GPUShaderStage.COMPUTE;
276
+ const V = GPUShaderStage.VERTEX;
277
+ set.gpu = {
278
+ generation: gpu.generation,
279
+ placeLayout: gpu.layouts.bindGroupLayout({
280
+ label: "foliage/place",
281
+ entries: [
282
+ { binding: 0, visibility: C, buffer: { type: "uniform", hasDynamicOffset: true } },
283
+ storage(1, "read-only-storage", C),
284
+ storage(2, "read-only-storage", C),
285
+ storage(3, "storage", C)
286
+ ]
287
+ }),
288
+ cullLayout: gpu.layouts.bindGroupLayout({
289
+ label: "foliage/cull",
290
+ entries: [
291
+ { binding: 0, visibility: C, buffer: { type: "uniform" } },
292
+ storage(1, "read-only-storage", C),
293
+ storage(2, "read-only-storage", C),
294
+ storage(3, "storage", C),
295
+ storage(4, "storage", C),
296
+ storage(5, "read-only-storage", C),
297
+ storage(6, "storage", C),
298
+ storage(7, "storage", C),
299
+ storage(8, "read-only-storage", C)
300
+ ]
301
+ }),
302
+ drawLayout: gpu.layouts.bindGroupLayout({
303
+ label: "foliage/draw",
304
+ entries: [
305
+ storage(0, "read-only-storage", V),
306
+ storage(1, "read-only-storage", V),
307
+ storage(2, "read-only-storage", V),
308
+ { binding: 3, visibility: V, buffer: { type: "uniform" } }
309
+ ]
310
+ }),
311
+ place: void 0,
312
+ reset: void 0,
313
+ cull: void 0,
314
+ scan: void 0,
315
+ scatter: void 0,
316
+ params: d.createBuffer({
317
+ label: "foliage/place-params",
318
+ size: PLACE_PARAMS_BYTES * 16,
319
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
320
+ }),
321
+ paramsSize: PLACE_PARAMS_BYTES * 16,
322
+ patch: d.createBuffer({
323
+ label: "foliage/patch",
324
+ size: 4096,
325
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
326
+ }),
327
+ patchSize: 4096,
328
+ avoid: d.createBuffer({
329
+ label: "foliage/avoid",
330
+ size: 256,
331
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
332
+ }),
333
+ avoidSize: 256
334
+ };
335
+ return set.gpu;
336
+ }
337
+ function pipelines(ctx, f) {
338
+ if (f.place && f.reset && f.cull && f.scan && f.scatter) return true;
339
+ const shaders = ctx.world.resource(Shaders);
340
+ const gpu = ctx.gpu;
341
+ const place = shaders.module(gpu, { root: "shard::foliage::place", label: "foliage place" });
342
+ const cull = shaders.module(gpu, { root: "shard::foliage::cull", label: "foliage cull" });
343
+ if (!place || !cull) return false;
344
+ f.place = gpu.pipelines.compute({
345
+ label: "foliage/place",
346
+ layout: gpu.layouts.pipelineLayout({
347
+ label: "foliage/place",
348
+ bindGroupLayouts: [f.placeLayout]
349
+ }),
350
+ compute: { module: place, entryPoint: "place" }
351
+ });
352
+ const cullLayout = gpu.layouts.pipelineLayout({
353
+ label: "foliage/cull",
354
+ bindGroupLayouts: [f.cullLayout]
355
+ });
356
+ f.reset = gpu.pipelines.compute({
357
+ label: "foliage/reset",
358
+ layout: cullLayout,
359
+ compute: { module: cull, entryPoint: "reset" }
360
+ });
361
+ f.cull = gpu.pipelines.compute({
362
+ label: "foliage/classify",
363
+ layout: cullLayout,
364
+ compute: { module: cull, entryPoint: "classify" }
365
+ });
366
+ f.scan = gpu.pipelines.compute({
367
+ label: "foliage/scan",
368
+ layout: cullLayout,
369
+ compute: { module: cull, entryPoint: "scan" }
370
+ });
371
+ f.scatter = gpu.pipelines.compute({
372
+ label: "foliage/scatter",
373
+ layout: cullLayout,
374
+ compute: { module: cull, entryPoint: "scatter" }
375
+ });
376
+ return f.place !== void 0 && f.reset !== void 0 && f.cull !== void 0 && f.scan !== void 0 && f.scatter !== void 0;
377
+ }
378
+ function grow(gpu, buffer, size, need, label, usage) {
379
+ if (need <= size) return { buffer, size };
380
+ let s = size;
381
+ while (s < need) s *= 2;
382
+ buffer.destroy();
383
+ return { buffer: gpu.device.createBuffer({ label, size: s, usage }), size: s };
384
+ }
385
+ function ensureLayer(gpu, layer, world) {
386
+ const d = gpu.device;
387
+ const lost = layer.generation !== gpu.generation;
388
+ if (lost || layer.slotCount > layer.capacitySlots) {
389
+ let slots = Math.max(8, layer.capacitySlots);
390
+ while (slots < layer.slotCount) slots *= 2;
391
+ if (!lost) layer.instances?.destroy();
392
+ if (!lost) layer.chunkBuffer?.destroy();
393
+ layer.instances = d.createBuffer({
394
+ label: `foliage/${layer.options.label}/instances`,
395
+ size: slots * layer.perChunk * 16,
396
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
397
+ });
398
+ layer.chunkBuffer = d.createBuffer({
399
+ label: `foliage/${layer.options.label}/chunks`,
400
+ size: slots * CHUNK_FLOATS * 4,
401
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
402
+ });
403
+ layer.capacitySlots = slots;
404
+ layer.generation = gpu.generation;
405
+ layer.views.clear();
406
+ layer.meshInfo = void 0;
407
+ layer.levelBuffer = void 0;
408
+ for (const [slot, chunk] of layer.chunks) layer.pending.push({ slot, chunk });
409
+ dedupePending(layer);
410
+ }
411
+ if (!layer.meshInfo) {
412
+ layer.meshInfo = d.createBuffer({
413
+ label: `foliage/${layer.options.label}/meshes`,
414
+ size: Math.max(16, layer.drawables.length * 16),
415
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
416
+ });
417
+ layer.levelBuffer = d.createBuffer({
418
+ label: `foliage/${layer.options.label}/levels`,
419
+ size: Math.max(16, layer.levels.byteLength),
420
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
421
+ });
422
+ d.queue.writeBuffer(layer.levelBuffer, 0, layer.levels);
423
+ }
424
+ const meshes = world.resource(Meshes);
425
+ const assets = world.resource(GpuAssetsResource);
426
+ const info = new Uint32Array(layer.drawables.length * 4);
427
+ let radius = 0;
428
+ for (let m = 0; m < layer.drawables.length; m++) {
429
+ const mesh = meshes.get(layer.drawables[m]);
430
+ if (!mesh) continue;
431
+ const gm = assets.mesh(mesh);
432
+ info[m * 4] = gm.count;
433
+ info[m * 4 + 2] = 0;
434
+ info[m * 4 + 3] = gm.baseVertex;
435
+ const b = mesh.bounds;
436
+ radius = Math.max(radius, Math.hypot(b[3] - b[0], b[4] - b[1], b[5] - b[2]) / 2);
437
+ }
438
+ layer.meshRadius = radius || 1;
439
+ d.queue.writeBuffer(layer.meshInfo, 0, info);
440
+ d.queue.writeBuffer(
441
+ layer.chunkBuffer,
442
+ 0,
443
+ layer.chunkRecords,
444
+ 0,
445
+ layer.capacitySlots * CHUNK_FLOATS
446
+ );
447
+ }
448
+ function dedupePending(layer) {
449
+ const seen = /* @__PURE__ */ new Set();
450
+ for (let i = layer.pending.length - 1; i >= 0; i--) {
451
+ const slot = layer.pending[i].slot;
452
+ if (seen.has(slot)) layer.pending.splice(i, 1);
453
+ else seen.add(slot);
454
+ }
455
+ }
456
+ const PLACE_PER_FRAME = 16;
457
+ function placeChunks(ctx, f, layers) {
458
+ const gpu = ctx.gpu;
459
+ const d = gpu.device;
460
+ const work = [];
461
+ for (const layer of layers) {
462
+ while (layer.pending.length > 0 && work.length < PLACE_PER_FRAME)
463
+ work.push({ layer, p: layer.pending.shift() });
464
+ }
465
+ if (work.length === 0) return;
466
+ let patchFloats = 0;
467
+ let avoidFloats = 0;
468
+ for (const w of work) {
469
+ patchFloats += w.p.chunk.grid * w.p.chunk.grid * PATCH_FLOATS;
470
+ avoidFloats += w.p.chunk.avoid?.length ?? 0;
471
+ }
472
+ const patch = new Float32Array(Math.max(4, patchFloats));
473
+ const avoid = new Float32Array(Math.max(4, avoidFloats));
474
+ const params = new Uint8Array(work.length * PLACE_PARAMS_BYTES);
475
+ let pb = 0;
476
+ let ab = 0;
477
+ for (const [k, { layer, p }] of work.entries()) {
478
+ const c = p.chunk;
479
+ const n = c.grid * c.grid;
480
+ for (let i = 0; i < n; i++) {
481
+ const o = pb + i * PATCH_FLOATS;
482
+ patch[o] = c.positions[i * 3];
483
+ patch[o + 1] = c.positions[i * 3 + 1];
484
+ patch[o + 2] = c.positions[i * 3 + 2];
485
+ patch[o + 3] = c.normals[i * 3];
486
+ patch[o + 4] = c.normals[i * 3 + 1];
487
+ patch[o + 5] = c.normals[i * 3 + 2];
488
+ patch[o + 6] = c.density[i];
489
+ }
490
+ const avoidCount = c.avoid ? c.avoid.length / 4 : 0;
491
+ if (c.avoid) avoid.set(c.avoid, ab);
492
+ const opts = layer.options;
493
+ paramsU32.fill(0);
494
+ paramsU32[0] = opts.cells;
495
+ paramsU32[1] = c.grid;
496
+ paramsU32[2] = p.slot;
497
+ paramsU32[3] = layer.perChunk;
498
+ paramsI32[4] = c.cell0[0];
499
+ paramsI32[5] = c.cell0[1];
500
+ paramsU32[6] = c.domain >>> 0;
501
+ paramsU32[7] = opts.seed >>> 0;
502
+ paramsF32[8] = c.accept;
503
+ paramsF32[9] = c.jitter;
504
+ paramsF32[10] = opts.scale[0];
505
+ paramsF32[11] = opts.scale[1];
506
+ paramsF32[12] = opts.align;
507
+ paramsU32[13] = opts.meshes.length;
508
+ paramsU32[14] = avoidCount;
509
+ paramsU32[15] = c.radial ? 1 : 0;
510
+ paramsU32[16] = pb;
511
+ paramsU32[17] = ab / 4;
512
+ if (c.radial) {
513
+ paramsF32[20] = c.radial[0];
514
+ paramsF32[21] = c.radial[1];
515
+ paramsF32[22] = c.radial[2];
516
+ }
517
+ const r = layer.chunkRecords;
518
+ const ro = p.slot * CHUNK_FLOATS;
519
+ paramsF32[24] = r[ro + 12];
520
+ paramsF32[25] = r[ro + 13];
521
+ paramsF32[26] = r[ro + 14];
522
+ paramsF32[28] = r[ro + 16];
523
+ paramsF32[29] = r[ro + 17];
524
+ paramsF32[30] = r[ro + 18];
525
+ for (let m = 0; m < opts.meshes.length; m++) paramsF32[32 + m] = opts.weights[m];
526
+ params.set(new Uint8Array(scratchParams), k * PLACE_PARAMS_BYTES);
527
+ pb += n * PATCH_FLOATS;
528
+ ab += avoidCount * 4;
529
+ }
530
+ const usage = GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST;
531
+ const pg = grow(gpu, f.patch, f.patchSize, patch.byteLength, "foliage/patch", usage);
532
+ f.patch = pg.buffer;
533
+ f.patchSize = pg.size;
534
+ const ag = grow(gpu, f.avoid, f.avoidSize, avoid.byteLength, "foliage/avoid", usage);
535
+ f.avoid = ag.buffer;
536
+ f.avoidSize = ag.size;
537
+ const prm = grow(
538
+ gpu,
539
+ f.params,
540
+ f.paramsSize,
541
+ params.byteLength,
542
+ "foliage/place-params",
543
+ GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
544
+ );
545
+ f.params = prm.buffer;
546
+ f.paramsSize = prm.size;
547
+ d.queue.writeBuffer(f.patch, 0, patch);
548
+ d.queue.writeBuffer(f.avoid, 0, avoid);
549
+ d.queue.writeBuffer(f.params, 0, params);
550
+ const pass = ctx.encoder.beginComputePass({
551
+ label: "foliage/place",
552
+ timestampWrites: ctx.timestamps("foliage/place")
553
+ });
554
+ pass.setPipeline(f.place);
555
+ let bound;
556
+ let group;
557
+ for (const [k, { layer, p }] of work.entries()) {
558
+ if (layer !== bound) {
559
+ group = d.createBindGroup({
560
+ label: "foliage/place",
561
+ layout: f.placeLayout,
562
+ entries: [
563
+ { binding: 0, resource: { buffer: f.params, size: PLACE_PARAMS_BYTES } },
564
+ { binding: 1, resource: { buffer: f.patch } },
565
+ { binding: 2, resource: { buffer: f.avoid } },
566
+ { binding: 3, resource: { buffer: layer.instances } }
567
+ ]
568
+ });
569
+ bound = layer;
570
+ }
571
+ pass.setBindGroup(0, group, [k * PLACE_PARAMS_BYTES]);
572
+ pass.dispatchWorkgroups(Math.ceil(layer.options.cells * layer.options.cells / 64));
573
+ const records = layer.chunkRecords;
574
+ records[p.slot * CHUNK_FLOATS + 15] = 1;
575
+ d.queue.writeBuffer(
576
+ layer.chunkBuffer,
577
+ p.slot * CHUNK_FLOATS * 4,
578
+ records,
579
+ p.slot * CHUNK_FLOATS,
580
+ CHUNK_FLOATS
581
+ );
582
+ }
583
+ pass.end();
584
+ }
585
+ function viewBuffers(gpu, f, layer, name) {
586
+ const capacity = layer.capacity;
587
+ const drawables = layer.drawables.length;
588
+ let v = layer.views.get(name);
589
+ if (v && v.capacity === capacity && v.drawables === drawables && v.generation === gpu.generation)
590
+ return v;
591
+ if (v) {
592
+ v.visible.destroy();
593
+ v.args.destroy();
594
+ v.classes.destroy();
595
+ v.counts.destroy();
596
+ v.params.destroy();
597
+ v.draw.destroy();
598
+ v.readback?.destroy();
599
+ }
600
+ const d = gpu.device;
601
+ v = {
602
+ // The camera's list and its shadow list, each at most every instance.
603
+ visible: d.createBuffer({
604
+ label: `foliage/${layer.options.label}/visible/${name}`,
605
+ size: Math.max(16, 2 * capacity * 4),
606
+ usage: GPUBufferUsage.STORAGE
607
+ }),
608
+ args: d.createBuffer({
609
+ label: `foliage/${layer.options.label}/args/${name}`,
610
+ size: Math.max(16, 2 * drawables * 20),
611
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.INDIRECT | GPUBufferUsage.COPY_SRC
612
+ }),
613
+ classes: d.createBuffer({
614
+ label: `foliage/${layer.options.label}/classes/${name}`,
615
+ size: Math.max(16, capacity * 4),
616
+ usage: GPUBufferUsage.STORAGE
617
+ }),
618
+ counts: d.createBuffer({
619
+ label: `foliage/${layer.options.label}/counts/${name}`,
620
+ size: Math.max(16, 2 * drawables * 4),
621
+ usage: GPUBufferUsage.STORAGE
622
+ }),
623
+ params: d.createBuffer({
624
+ label: `foliage/${layer.options.label}/cull/${name}`,
625
+ size: CULL_PARAMS_FLOATS * 4,
626
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
627
+ }),
628
+ draw: d.createBuffer({
629
+ label: `foliage/${layer.options.label}/draw/${name}`,
630
+ size: 32,
631
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
632
+ }),
633
+ cullGroup: void 0,
634
+ drawGroup: void 0,
635
+ capacity,
636
+ drawables,
637
+ generation: gpu.generation,
638
+ culled: -1,
639
+ read_: new Uint32Array(2 * drawables),
640
+ reading: false,
641
+ readback: void 0,
642
+ read: -READBACK_EVERY
643
+ };
644
+ v.cullGroup = d.createBindGroup({
645
+ label: "foliage/cull",
646
+ layout: f.cullLayout,
647
+ entries: [
648
+ { binding: 0, resource: { buffer: v.params } },
649
+ { binding: 1, resource: { buffer: layer.instances } },
650
+ { binding: 2, resource: { buffer: layer.chunkBuffer } },
651
+ { binding: 3, resource: { buffer: v.visible } },
652
+ { binding: 4, resource: { buffer: v.args } },
653
+ { binding: 5, resource: { buffer: layer.meshInfo } },
654
+ { binding: 6, resource: { buffer: v.classes } },
655
+ { binding: 7, resource: { buffer: v.counts } },
656
+ { binding: 8, resource: { buffer: layer.levelBuffer } }
657
+ ]
658
+ });
659
+ v.drawGroup = d.createBindGroup({
660
+ label: "foliage/draw",
661
+ layout: f.drawLayout,
662
+ entries: [
663
+ { binding: 0, resource: { buffer: layer.instances } },
664
+ { binding: 1, resource: { buffer: v.visible } },
665
+ { binding: 2, resource: { buffer: layer.chunkBuffer } },
666
+ { binding: 3, resource: { buffer: v.draw } }
667
+ ]
668
+ });
669
+ layer.views.set(name, v);
670
+ return v;
671
+ }
672
+ const DEFAULT_LODS = [0.3, 0.6];
673
+ const READBACK_EVERY = 30;
674
+ function cullLayer(ctx, f, layer, cam, frame, steer) {
675
+ const gpu = ctx.gpu;
676
+ const v = viewBuffers(gpu, f, layer, ctx.view.name);
677
+ const o = layer.options;
678
+ const range = o.range * steer.rangeScale;
679
+ const cascades = ctx.world.resource(ForwardStateResource).views.get(ctx.view.name)?.cascades;
680
+ const shadowRange = cascades && cascades.count > 0 ? Math.min(o.shadowRange, cascades.splits[0], range) : 0;
681
+ const lods = o.lodDistances ?? DEFAULT_LODS;
682
+ cullData.fill(0);
683
+ cullData.set(cam.frustum, 0);
684
+ cullData[24] = cam.position[0];
685
+ cullData[25] = cam.position[1];
686
+ cullData[26] = cam.position[2];
687
+ cullData[27] = range;
688
+ cullData[28] = shadowRange;
689
+ cullData[29] = o.thinFrom ?? 0.5;
690
+ cullData[30] = layer.meshRadius;
691
+ cullData[31] = steer.density;
692
+ for (let l = 0; l < 4; l++) cullData[32 + l] = lods[l] ?? 2;
693
+ cullU32[36] = layer.perChunk;
694
+ cullU32[37] = layer.capacitySlots;
695
+ cullU32[38] = o.meshes.length;
696
+ cullU32[39] = layer.drawables.length;
697
+ cullData[40] = o.scale[0];
698
+ cullData[41] = o.scale[1];
699
+ gpu.device.queue.writeBuffer(v.params, 0, cullData);
700
+ drawData[0] = cam.position[0];
701
+ drawData[1] = cam.position[1];
702
+ drawData[2] = cam.position[2];
703
+ drawData[3] = range;
704
+ drawData[4] = o.thinFrom ?? 0.5;
705
+ drawData[5] = o.scale[0];
706
+ drawData[6] = o.scale[1];
707
+ drawData[7] = layer.perChunk;
708
+ gpu.device.queue.writeBuffer(v.draw, 0, drawData);
709
+ const pass = ctx.encoder.beginComputePass({
710
+ label: "foliage/cull",
711
+ timestampWrites: ctx.timestamps("foliage/cull")
712
+ });
713
+ pass.setBindGroup(0, v.cullGroup);
714
+ pass.setPipeline(f.reset);
715
+ pass.dispatchWorkgroups(Math.ceil(2 * layer.drawables.length / 64));
716
+ const total = layer.capacity;
717
+ const groups = Math.ceil(total / 64);
718
+ const gx = Math.min(groups, 65535);
719
+ const gy = Math.ceil(groups / 65535);
720
+ if (total > 0) {
721
+ pass.setPipeline(f.cull);
722
+ pass.dispatchWorkgroups(gx, gy);
723
+ }
724
+ pass.setPipeline(f.scan);
725
+ pass.dispatchWorkgroups(1);
726
+ if (total > 0) {
727
+ pass.setPipeline(f.scatter);
728
+ pass.dispatchWorkgroups(gx, gy);
729
+ }
730
+ pass.end();
731
+ v.culled = frame;
732
+ if (!v.reading && frame - v.read >= READBACK_EVERY) {
733
+ v.read = frame;
734
+ readCounts(ctx, v);
735
+ }
736
+ }
737
+ function readCounts(ctx, v) {
738
+ const size = v.drawables * 2 * 20;
739
+ v.readback ??= ctx.gpu.device.createBuffer({
740
+ label: "foliage/readback",
741
+ size,
742
+ usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST
743
+ });
744
+ ctx.encoder.copyBufferToBuffer(v.args, 0, v.readback, 0, size);
745
+ v.reading = true;
746
+ const buffer = v.readback;
747
+ ctx.afterSubmit(() => {
748
+ buffer.mapAsync(GPUMapMode.READ).then(
749
+ () => {
750
+ const words = new Uint32Array(buffer.getMappedRange());
751
+ for (let k = 0; k < v.read_.length; k++) v.read_[k] = words[k * 5 + 1];
752
+ buffer.unmap();
753
+ v.reading = false;
754
+ },
755
+ () => {
756
+ v.reading = false;
757
+ }
758
+ );
759
+ });
760
+ }
761
+ const FOLIAGE_KEY = 2 ** 45;
762
+ const KIND_FORWARD = 0;
763
+ const KIND_GBUFFER = 1;
764
+ const KIND_SHADOW = 2;
765
+ function foliagePipeline(ctx, f, material, kind, msaa) {
766
+ const world = ctx.world;
767
+ const gpu = ctx.gpu;
768
+ const state = world.resource(ForwardStateResource);
769
+ const assets = world.resource(GpuAssetsResource);
770
+ const type = material.type;
771
+ const variant = materialVariant(material);
772
+ const mask = variantBlend(variant) === "mask";
773
+ const mode = kind === KIND_FORWARD ? state.interiorMode : 0;
774
+ const key = FOLIAGE_KEY + (((kind * 1024 + typeOrdinal(type)) * 16 + variant) * 8 + msaa) + mode * 2 ** 42;
775
+ const cached = state.pipelines.cached(key);
776
+ if (cached) return cached;
777
+ const slot = kind === KIND_FORWARD ? 24 + (mask ? 1 : 0) + mode * 2 : (kind === KIND_GBUFFER ? 9 : 11) + (mask ? 1 : 0);
778
+ const root = kind === KIND_FORWARD ? "shard::foliage::forward" : kind === KIND_GBUFFER ? "shard::foliage::gbuffer" : "shard::foliage::shadow";
779
+ const defines = kind === KIND_FORWARD ? { PREMULTIPLY: false, MASK: mask, OPAQUE: true, ...INTERIOR_DEFINES[mode] ?? {} } : { MASK: mask };
780
+ const module = state.pipelines.module(world, gpu, type, slot, root, defines);
781
+ if (!module) return void 0;
782
+ const layouts = kind === KIND_SHADOW ? [state.layouts.shadowView, assets.layoutOf(type), f.drawLayout] : [state.layouts.view, assets.layoutOf(type), f.drawLayout];
783
+ return state.pipelines.create(
784
+ gpu,
785
+ key,
786
+ {
787
+ label: `foliage/${["forward", "gbuffer", "shadow"][kind]}/${type.name}/${mask ? "mask" : "opaque"}`,
788
+ layout: gpu.layouts.pipelineLayout({
789
+ label: `foliage/${type.name}/${kind}`,
790
+ bindGroupLayouts: layouts
791
+ }),
792
+ vertex: { module, entryPoint: "vs", buffers: VERTEX_BUFFERS },
793
+ fragment: kind === KIND_SHADOW ? mask ? { module, entryPoint: "fs_mask", targets: [] } : void 0 : {
794
+ module,
795
+ entryPoint: "fs",
796
+ targets: kind === KIND_GBUFFER ? state.gbufferTargets : [{ format: "rgba16float" }]
797
+ },
798
+ primitive: { topology: "triangle-list", cullMode: variantCull(variant), frontFace: "ccw" },
799
+ depthStencil: { format: "depth32float", depthWriteEnabled: true, depthCompare: "greater" },
800
+ multisample: { count: kind === KIND_FORWARD ? msaa : 1 }
801
+ },
802
+ type
803
+ );
804
+ }
805
+ function drawLayer(ctx, f, layer, v, kind, msaa, pass) {
806
+ const world = ctx.world;
807
+ const material = world.resource(Materials).get(layer.options.material);
808
+ if (!material?.type.standard) return;
809
+ const pipeline = foliagePipeline(ctx, f, material, kind, msaa);
810
+ if (!pipeline) {
811
+ ctx.gpu.pipelines.skipped++;
812
+ return;
813
+ }
814
+ const assets = world.resource(GpuAssetsResource);
815
+ const gm = assets.material(world, material);
816
+ if (!gm?.bindGroup) return;
817
+ pass.setPipeline(pipeline);
818
+ pass.setBindGroup(1, gm.bindGroup);
819
+ pass.setBindGroup(2, v.drawGroup);
820
+ const meshes = world.resource(Meshes);
821
+ const list = kind === KIND_SHADOW ? 1 : 0;
822
+ const count = layer.drawables.length;
823
+ for (let m = 0; m < count; m++) {
824
+ const mesh = meshes.get(layer.drawables[m]);
825
+ if (!mesh) continue;
826
+ const g = assets.mesh(mesh);
827
+ if (!g.indices) continue;
828
+ pass.setVertexBuffer(0, g.positions);
829
+ pass.setVertexBuffer(1, g.normals);
830
+ pass.setVertexBuffer(2, g.uvs);
831
+ pass.setVertexBuffer(3, g.uvs1);
832
+ pass.setVertexBuffer(4, g.tangents);
833
+ pass.setIndexBuffer(g.indices, g.indexFormat);
834
+ pass.drawIndexedIndirect(v.args, (list * count + m) * 5 * 4);
835
+ }
836
+ }
837
+ function supported(gpu) {
838
+ return gpu.tier === "full" && gpu.features.has("indirect-first-instance");
839
+ }
840
+ function support(world) {
841
+ const ready = (ctx, cam) => {
842
+ const layers = world.tryResource(FoliageLayers);
843
+ if (!layers || layers.layers.size === 0 || !supported(ctx.gpu) || !layers.gpu) return void 0;
844
+ void cam;
845
+ return layers;
846
+ };
847
+ return {
848
+ draw(ctx, cam, pass, renderPass) {
849
+ const layers = ready(ctx, cam);
850
+ if (!layers) return;
851
+ const kind = pass === PASS_GBUFFER ? KIND_GBUFFER : pass === PASS_OPAQUE ? KIND_FORWARD : -1;
852
+ if (kind < 0) return;
853
+ const state = world.resource(ForwardStateResource);
854
+ const pv = state.views.get(ctx.view.name);
855
+ if (!pv) return;
856
+ renderPass.setBindGroup(0, viewBindGroup(ctx.gpu, world, pv, cam));
857
+ for (const layer of layers.layers) {
858
+ const v = layer.views.get(ctx.view.name);
859
+ if (!v || v.culled !== state.frame || layer.chunkCount === 0) continue;
860
+ drawLayer(
861
+ ctx,
862
+ layers.gpu,
863
+ layer,
864
+ v,
865
+ kind,
866
+ kind === KIND_FORWARD ? cam.msaa : 1,
867
+ renderPass
868
+ );
869
+ }
870
+ },
871
+ drawShadow(ctx, cam, renderPass, offset, cascade) {
872
+ if (cascade !== 0) return;
873
+ const layers = ready(ctx, cam);
874
+ if (!layers) return;
875
+ void offset;
876
+ const frame = world.resource(ForwardStateResource).frame;
877
+ for (const layer of layers.layers) {
878
+ if (!(layer.options.shadowRange > 0)) continue;
879
+ const v = layer.views.get(ctx.view.name);
880
+ if (!v || v.culled !== frame || layer.chunkCount === 0) continue;
881
+ drawLayer(ctx, layers.gpu, layer, v, KIND_SHADOW, 1, renderPass);
882
+ }
883
+ },
884
+ animatedShadows(cam) {
885
+ const layers = world.tryResource(FoliageLayers);
886
+ if (!layers) return false;
887
+ void cam;
888
+ for (const layer of layers.layers)
889
+ if (layer.options.shadowRange > 0 && layer.chunkCount > 0) return true;
890
+ return false;
891
+ }
892
+ };
893
+ }
894
+ function foliageTarget(world, budget, set) {
895
+ if (budget.ms > 0) return budget.ms;
896
+ const scenario = world.tryResource(PerfScenario)?.name ?? null;
897
+ if (scenario === null) return 0;
898
+ const data = world.tryResource(PerfBudgets);
899
+ if (scenario !== set.sliceFor || data !== set.sliceData) {
900
+ set.sliceFor = scenario;
901
+ set.sliceData = data;
902
+ set.sliceMs = scenarioSliceMs(world, scenario, "gpu", FOLIAGE_SPAN_KEY) ?? 0;
903
+ }
904
+ return set.sliceMs;
905
+ }
906
+ const steerFoliage = defineSystem({
907
+ name: "render/foliage-budget",
908
+ description: "Thins GPU foliage's density and range to keep its GPU time (gpu:foliage) inside FoliageBudget's target, or its scenario slice (0075).",
909
+ run: (_, world) => {
910
+ const set = world.tryResource(FoliageLayers);
911
+ const budget = world.tryResource(FoliageBudget);
912
+ const profiler = world.tryResource(ProfilerResource);
913
+ if (!set || !budget || !profiler) return;
914
+ const target = foliageTarget(world, budget, set);
915
+ budget.target = target;
916
+ const landed = set.gpuTime.read(profiler);
917
+ if (!(target > 0)) {
918
+ if (set.steer.detail !== 1) set.steer.reset();
919
+ if (landed) budget.measuredMs = set.gpuTime.ms;
920
+ budget.detail = 1;
921
+ return;
922
+ }
923
+ if (landed) set.steer.sample(set.gpuTime.ms, target, budget.minDetail);
924
+ budget.measuredMs = set.steer.smoothed;
925
+ budget.detail = set.steer.detail;
926
+ }
927
+ });
928
+ const FORWARD_READS = [
929
+ "clusters",
930
+ "shadow-cascades",
931
+ "shadow-local",
932
+ "environment",
933
+ "culled",
934
+ "ssao"
935
+ ];
936
+ const FORWARD_WRITES = ["scene-color", "scene-depth", "hdr"];
937
+ const foliagePlugin = definePlugin({
938
+ name: "render/foliage",
939
+ dependencies: ["render/forward"],
940
+ provides: [FoliageLayers, FoliagePath, FoliageBudget],
941
+ build(app) {
942
+ app.world.initResource(FoliageLayers);
943
+ app.world.initResource(FoliageBudget);
944
+ app.insertResource(FoliagePath, support(app.world));
945
+ app.addSystems(PostUpdate, steerFoliage);
946
+ },
947
+ dispose(app) {
948
+ app.world.tryResource(FoliageLayers)?.dispose();
949
+ },
950
+ ready(app) {
951
+ const world = app.world;
952
+ const graph = world.tryResource(Graph);
953
+ if (!graph) return;
954
+ registerShaders(world.resource(Shaders), FOLIAGE_SHADERS);
955
+ addRenderFeatures(world, {
956
+ name: "render/foliage",
957
+ description: "GPU foliage: instances placed and culled by compute, drawn indirectly.",
958
+ nodes: ["foliage/place", "foliage/cull", "foliage/draw"],
959
+ baseline: "unsupported"
960
+ });
961
+ let placed = -1;
962
+ graph.addNode("foliage/place", {
963
+ kind: "raw",
964
+ phase: RenderPhase.Setup + 5,
965
+ sideEffects: true,
966
+ enabled: (view) => cameraOf(view) !== void 0,
967
+ run(ctx) {
968
+ const state = world.resource(ForwardStateResource);
969
+ if (placed === state.frame) return;
970
+ placed = state.frame;
971
+ const set = world.resource(FoliageLayers);
972
+ const layers = set.layers;
973
+ if (layers.size === 0 || !supported(ctx.gpu)) return;
974
+ const f = foliageGpu(ctx.gpu, set);
975
+ if (!pipelines(ctx, f)) {
976
+ ctx.gpu.pipelines.skipped++;
977
+ return;
978
+ }
979
+ const t0 = performance.now();
980
+ for (const layer of layers) ensureLayer(ctx.gpu, layer, world);
981
+ placeChunks(ctx, f, layers);
982
+ world.tryResource(ProfilerResource)?.record("foliage/place", performance.now() - t0);
983
+ }
984
+ });
985
+ graph.addNode("foliage/cull", {
986
+ kind: "raw",
987
+ phase: RenderPhase.Setup + 6,
988
+ sideEffects: true,
989
+ after: ["foliage/place"],
990
+ enabled: (view) => cameraOf(view) !== void 0,
991
+ run(ctx) {
992
+ const set = world.resource(FoliageLayers);
993
+ const f = set.gpu;
994
+ if (set.layers.size === 0 || !supported(ctx.gpu) || !f?.cull) return;
995
+ const cam = cameraOf(ctx.view);
996
+ const frame = world.resource(ForwardStateResource).frame;
997
+ for (const layer of set.layers) {
998
+ if (layer.chunkCount === 0 || !layer.instances) continue;
999
+ cullLayer(ctx, f, layer, cam, frame, set.steer);
1000
+ }
1001
+ }
1002
+ });
1003
+ graph.addNode("foliage/draw", {
1004
+ kind: "render",
1005
+ ownPass: true,
1006
+ phase: RenderPhase.Opaque + 5,
1007
+ after: ["forward-opaque"],
1008
+ enabled: (view) => {
1009
+ const cam = cameraOf(view);
1010
+ return cam !== void 0 && !cam.deferred && world.resource(FoliageLayers).layers.size > 0;
1011
+ },
1012
+ reads: FORWARD_READS,
1013
+ writes: FORWARD_WRITES,
1014
+ color: (view) => sceneColor(view),
1015
+ depth: { resource: "scene-depth" },
1016
+ run(ctx) {
1017
+ world.tryResource(FoliagePath)?.draw(ctx, cameraOf(ctx.view), PASS_OPAQUE, ctx.renderPass);
1018
+ }
1019
+ });
1020
+ }
1021
+ });
1022
+ export {
1023
+ FoliageLayer,
1024
+ FoliageLayerSet,
1025
+ FoliageLayers,
1026
+ foliagePlugin
1027
+ };