@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.
- package/dist/ablation.d.ts +74 -0
- package/dist/ablation.js +117 -0
- package/dist/atmosphere-nodes.js +100 -12
- package/dist/atmosphere-shaders.js +17 -5
- package/dist/baseline/exposure.d.ts +1 -1
- package/dist/baseline/exposure.js +18 -5
- package/dist/crowd.d.ts +21 -0
- package/dist/crowd.js +121 -0
- package/dist/decals-plugin.d.ts +5 -0
- package/dist/decals-plugin.js +54 -0
- package/dist/decals.d.ts +69 -0
- package/dist/decals.js +326 -0
- package/dist/deferred.js +4 -1
- package/dist/display-nodes.js +24 -11
- package/dist/foliage-budget.d.ts +64 -0
- package/dist/foliage-budget.js +108 -0
- package/dist/foliage-path.d.ts +19 -0
- package/dist/foliage-path.js +7 -0
- package/dist/foliage-shaders.d.ts +11 -0
- package/dist/foliage-shaders.js +504 -0
- package/dist/foliage.d.ts +194 -0
- package/dist/foliage.js +1027 -0
- package/dist/forward.js +26 -2
- package/dist/graph.d.ts +34 -0
- package/dist/graph.js +139 -14
- package/dist/index.d.ts +11 -1
- package/dist/index.js +57 -0
- package/dist/overlay-registry.d.ts +3 -1
- package/dist/overlay-registry.js +3 -1
- package/dist/overlays.js +5 -2
- package/dist/perf-overlay.d.ts +9 -0
- package/dist/perf-overlay.js +125 -0
- package/dist/plugin.d.ts +11 -1
- package/dist/plugin.js +33 -2
- package/dist/post-common.d.ts +20 -2
- package/dist/post-common.js +79 -2
- package/dist/post-nodes.d.ts +5 -0
- package/dist/post-nodes.js +136 -104
- package/dist/post-scene.d.ts +130 -0
- package/dist/post-scene.js +164 -0
- package/dist/post-shaders.js +41 -63
- package/dist/post.js +11 -1
- package/dist/ribbons-plugin.d.ts +5 -0
- package/dist/ribbons-plugin.js +59 -0
- package/dist/ribbons.d.ts +44 -0
- package/dist/ribbons.js +288 -0
- package/dist/scenario.d.ts +70 -0
- package/dist/scenario.js +125 -0
- package/dist/shaders.js +58 -5
- package/dist/shadows.d.ts +2 -1
- package/dist/shadows.js +3 -1
- package/dist/standard.js +6 -0
- package/dist/testing.d.ts +1 -0
- package/dist/testing.js +6 -0
- package/dist/timer.d.ts +16 -1
- package/dist/timer.js +66 -19
- package/dist/vfx-draw.d.ts +37 -0
- package/dist/vfx-draw.js +291 -0
- package/dist/vfx-state.d.ts +42 -0
- package/dist/vfx-state.js +81 -0
- package/dist/vfx.d.ts +69 -0
- package/dist/vfx.js +103 -0
- package/package.json +19 -11
package/dist/foliage.js
ADDED
|
@@ -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
|
+
};
|