@driftengine/core 4.13.1 → 4.13.3
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.json +42 -0
- package/README.md +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.js +1 -0
- package/dist/render/backend/api.d.ts +63 -8
- package/dist/render/backend/webgl2/pointSkinFeedback.d.ts +23 -0
- package/dist/render/backend/webgl2/pointSkinFeedback.js +159 -0
- package/dist/render/backend/webgl2/renderer.d.ts +33 -0
- package/dist/render/backend/webgl2/renderer.js +88 -4
- package/dist/render/backend/webgpu/buffers.d.ts +10 -2
- package/dist/render/backend/webgpu/buffers.js +66 -16
- package/dist/render/backend/webgpu/clothTextures.d.ts +6 -3
- package/dist/render/backend/webgpu/clothTextures.js +14 -11
- package/dist/render/backend/webgpu/drawCommand.js +3 -3
- package/dist/render/backend/webgpu/gpuClothSet.js +1 -0
- package/dist/render/backend/webgpu/groupShelf.d.ts +31 -0
- package/dist/render/backend/webgpu/groupShelf.js +54 -0
- package/dist/render/backend/webgpu/pointSkinPass.d.ts +27 -0
- package/dist/render/backend/webgpu/pointSkinPass.js +104 -0
- package/dist/render/backend/webgpu/renderer.d.ts +40 -7
- package/dist/render/backend/webgpu/renderer.js +151 -48
- package/dist/render/backend/webgpu/shaders/clothSolve.wgsl.d.ts +1 -1
- package/dist/render/backend/webgpu/shaders/clothSolve.wgsl.js +7 -2
- package/dist/render/backend/webgpu/shaders/pointSkin.wgsl.d.ts +13 -0
- package/dist/render/backend/webgpu/shaders/pointSkin.wgsl.js +57 -0
- package/dist/render/backend/webgpu/vertexFormats.js +1 -1
- package/dist/render/lightBudget.d.ts +6 -0
- package/dist/render/lightBudget.js +6 -0
- package/dist/render/mesh.d.ts +19 -5
- package/dist/render/mesh.js +39 -8
- package/dist/render/pointSkin.d.ts +55 -0
- package/dist/render/pointSkin.js +121 -0
- package/dist/render/rendererHarness.js +6 -0
- package/dist/render/shader.d.ts +3 -1
- package/dist/render/shader.js +6 -1
- package/dist/render/shaders/flat/detail.d.ts +21 -0
- package/dist/render/shaders/flat/detail.js +74 -0
- package/dist/render/shaders/flat/glassTint.d.ts +1 -1
- package/dist/render/shaders/flat/glassTint.js +1 -0
- package/dist/render/shaders/flat/index.d.ts +5 -0
- package/dist/render/shaders/flat/index.js +3 -0
- package/dist/render/shaders/flat/main.d.ts +1 -1
- package/dist/render/shaders/flat/main.js +2 -0
- package/dist/render/shaders/generated/flat.wgsl.d.ts +144 -16
- package/dist/render/shaders/generated/flat.wgsl.js +183 -55
- package/dist/render/surfaceDetail.d.ts +79 -0
- package/dist/render/surfaceDetail.js +86 -0
- package/dist/render/surfaceTexture.d.ts +9 -0
- package/dist/render/vertexPacking.d.ts +16 -4
- package/dist/render/vertexPacking.js +10 -3
- package/package.json +6 -6
- package/src/index.ts +4 -0
- package/src/render/backend/api.ts +70 -7
- package/src/render/backend/webgl2/pointSkinFeedback.ts +173 -0
- package/src/render/backend/webgl2/renderer.ts +121 -4
- package/src/render/backend/webgpu/buffers.ts +90 -16
- package/src/render/backend/webgpu/clothTextures.ts +14 -11
- package/src/render/backend/webgpu/drawCommand.ts +3 -3
- package/src/render/backend/webgpu/gpuClothSet.ts +1 -0
- package/src/render/backend/webgpu/groupShelf.ts +74 -0
- package/src/render/backend/webgpu/pointSkinPass.ts +115 -0
- package/src/render/backend/webgpu/renderer.ts +192 -83
- package/src/render/backend/webgpu/shaders/clothSolve.wgsl.ts +7 -2
- package/src/render/backend/webgpu/shaders/pointSkin.wgsl.ts +58 -0
- package/src/render/backend/webgpu/vertexFormats.ts +1 -1
- package/src/render/lightBudget.ts +7 -0
- package/src/render/mesh.ts +49 -7
- package/src/render/pointSkin.ts +178 -0
- package/src/render/rendererHarness.ts +6 -0
- package/src/render/shader.ts +5 -0
- package/src/render/shaders/flat/detail.ts +74 -0
- package/src/render/shaders/flat/glassTint.ts +3 -1
- package/src/render/shaders/flat/index.ts +8 -0
- package/src/render/shaders/flat/main.ts +2 -0
- package/src/render/shaders/generated/flat.wgsl.ts +183 -55
- package/src/render/surfaceDetail.ts +136 -0
- package/src/render/surfaceTexture.ts +9 -0
- package/src/render/vertexPacking.ts +25 -5
|
@@ -162,6 +162,14 @@ export interface GpuMesh {
|
|
|
162
162
|
* layout, keyed by `:dynamic`, and two more buffers bound per draw of such a mesh.
|
|
163
163
|
*/
|
|
164
164
|
readonly update: ((device: GPUDevice, positions: Float32Array, normals?: Float32Array, staged?: StagedRewrite | null) => void) | null;
|
|
165
|
+
/**
|
|
166
|
+
* Rewrite this mesh's colours, three floats a vertex.
|
|
167
|
+
*
|
|
168
|
+
* Absent unless the mesh was created `dynamicColors`, whose colours are a buffer of their own at
|
|
169
|
+
* the end of `vertexBuffers` (`vertexPacking.ts`), so this is one `writeBuffer` and nothing else
|
|
170
|
+
* moves: not the positions, not the bounds, and no motion, since a colour does not move.
|
|
171
|
+
*/
|
|
172
|
+
readonly updateColors: ((device: GPUDevice, colors: Float32Array) => void) | null;
|
|
165
173
|
/**
|
|
166
174
|
* A dynamic mesh's motion state, which the renderer owns: a buffer for last frame's positions and
|
|
167
175
|
* one the new positions are staged in, each made the first time a reconstructing renderer
|
|
@@ -211,7 +219,7 @@ export interface IncrementalGpuMesh {
|
|
|
211
219
|
* two paths cannot write different bytes. The one that spreads the work is the one with the
|
|
212
220
|
* interesting decisions in it; read that.
|
|
213
221
|
*/
|
|
214
|
-
export declare function createGpuMesh(device: GPUDevice, data: MeshData, dynamic?: boolean): GpuMesh;
|
|
222
|
+
export declare function createGpuMesh(device: GPUDevice, data: MeshData, dynamic?: boolean, dynamicColors?: boolean): GpuMesh;
|
|
215
223
|
/**
|
|
216
224
|
* Upload a mesh over as many frames as the caller gives it.
|
|
217
225
|
*
|
|
@@ -237,7 +245,7 @@ export declare function createGpuMesh(device: GPUDevice, data: MeshData, dynamic
|
|
|
237
245
|
* streamer places and culls a region from them before a byte of it has landed, so a handle whose
|
|
238
246
|
* bounds were not yet known would not be usable at once, which is the whole promise here.
|
|
239
247
|
*/
|
|
240
|
-
export declare function createGpuMeshIncremental(device: GPUDevice, data: MeshData, dynamic?: boolean): IncrementalGpuMesh;
|
|
248
|
+
export declare function createGpuMeshIncremental(device: GPUDevice, data: MeshData, dynamic?: boolean, dynamicColors?: boolean): IncrementalGpuMesh;
|
|
241
249
|
export declare function vertexBufferLayouts(present: Readonly<Record<string, boolean>>,
|
|
242
250
|
/**
|
|
243
251
|
* Whether a third buffer carries one placement and colour per instance.
|
|
@@ -28,6 +28,7 @@ const USAGE = {
|
|
|
28
28
|
INDEX: 0x0010,
|
|
29
29
|
VERTEX: 0x0020,
|
|
30
30
|
UNIFORM: 0x0040,
|
|
31
|
+
STORAGE: 0x0080,
|
|
31
32
|
};
|
|
32
33
|
/**
|
|
33
34
|
* Bytes reserved per absent attribute in the constants buffer.
|
|
@@ -123,8 +124,8 @@ function laidOut(attribute, layered) {
|
|
|
123
124
|
* two paths cannot write different bytes. The one that spreads the work is the one with the
|
|
124
125
|
* interesting decisions in it; read that.
|
|
125
126
|
*/
|
|
126
|
-
export function createGpuMesh(device, data, dynamic = false) {
|
|
127
|
-
const { mesh, upload } = createGpuMeshIncremental(device, data, dynamic);
|
|
127
|
+
export function createGpuMesh(device, data, dynamic = false, dynamicColors = false) {
|
|
128
|
+
const { mesh, upload } = createGpuMeshIncremental(device, data, dynamic, dynamicColors);
|
|
128
129
|
while (upload.next().done !== true)
|
|
129
130
|
;
|
|
130
131
|
return mesh;
|
|
@@ -154,7 +155,7 @@ export function createGpuMesh(device, data, dynamic = false) {
|
|
|
154
155
|
* streamer places and culls a region from them before a byte of it has landed, so a handle whose
|
|
155
156
|
* bounds were not yet known would not be usable at once, which is the whole promise here.
|
|
156
157
|
*/
|
|
157
|
-
export function createGpuMeshIncremental(device, data, dynamic = false) {
|
|
158
|
+
export function createGpuMeshIncremental(device, data, dynamic = false, dynamicColors = false) {
|
|
158
159
|
/*
|
|
159
160
|
* The other backend's refusal, and the premise of the interleave below: every attribute holds
|
|
160
161
|
* exactly a vertex's worth for every vertex, so nothing reads past the end of its array.
|
|
@@ -164,10 +165,10 @@ export function createGpuMeshIncremental(device, data, dynamic = false) {
|
|
|
164
165
|
const vertexCount = data.positions.length / 3;
|
|
165
166
|
const layered = data.layers !== undefined;
|
|
166
167
|
/* How each attribute travels, which the pipeline key says too: `vertexPacking.ts`. */
|
|
167
|
-
const packing = vertexPackingOf(data, dynamic);
|
|
168
|
+
const packing = vertexPackingOf(data, dynamic, dynamicColors);
|
|
168
169
|
/* Interleaved: one stride holding every attribute the mesh actually supplied — less a dynamic
|
|
169
|
-
mesh's positions and normals, which are buffers of their
|
|
170
|
-
shares, which is a constant. */
|
|
170
|
+
mesh's positions and normals and a `dynamicColors` mesh's colours, which are buffers of their
|
|
171
|
+
own, and any attribute every vertex shares, which is a constant. */
|
|
171
172
|
const fields = [];
|
|
172
173
|
let stride = 0;
|
|
173
174
|
for (const declared of VERTEX_LAYOUT) {
|
|
@@ -177,7 +178,7 @@ export function createGpuMeshIncremental(device, data, dynamic = false) {
|
|
|
177
178
|
continue;
|
|
178
179
|
const attribute = laidOut(declared, layered);
|
|
179
180
|
const how = packingFor(packing, attribute.name);
|
|
180
|
-
if (how === 'constant')
|
|
181
|
+
if (how === 'constant' || how === 'stream')
|
|
181
182
|
continue;
|
|
182
183
|
const format = packedFormat(attribute.format, how);
|
|
183
184
|
fields.push({ attribute, packing: how, format, offset: stride });
|
|
@@ -255,27 +256,36 @@ export function createGpuMeshIncremental(device, data, dynamic = false) {
|
|
|
255
256
|
size: align4(indices.byteLength),
|
|
256
257
|
usage: USAGE.INDEX | USAGE.COPY_DST,
|
|
257
258
|
});
|
|
258
|
-
/* A dynamic mesh's two moving streams: copied from as well as written, for last frame's positions
|
|
259
|
+
/* A dynamic mesh's two moving streams: copied from as well as written, for last frame's positions,
|
|
260
|
+
and written by a point skin's pose on the device (`pointSkin.ts`). */
|
|
259
261
|
const streamBytes = align4(vertexCount * 12);
|
|
260
262
|
const streams = dynamic
|
|
261
263
|
? {
|
|
262
264
|
positions: device.createBuffer({
|
|
263
265
|
label: 'mesh.positions',
|
|
264
266
|
size: streamBytes,
|
|
265
|
-
usage: USAGE.VERTEX | USAGE.COPY_DST | USAGE.COPY_SRC,
|
|
267
|
+
usage: USAGE.VERTEX | USAGE.COPY_DST | USAGE.COPY_SRC | USAGE.STORAGE,
|
|
266
268
|
}),
|
|
267
269
|
normals: device.createBuffer({
|
|
268
270
|
label: 'mesh.normals',
|
|
269
271
|
size: streamBytes,
|
|
270
|
-
usage: USAGE.VERTEX | USAGE.COPY_DST,
|
|
272
|
+
usage: USAGE.VERTEX | USAGE.COPY_DST | USAGE.STORAGE,
|
|
271
273
|
}),
|
|
272
274
|
}
|
|
273
275
|
: null;
|
|
276
|
+
/* And a `dynamicColors` mesh's colours, which only the caller's rewrites ever change. */
|
|
277
|
+
const colorStream = packing.colors === 'stream'
|
|
278
|
+
? device.createBuffer({
|
|
279
|
+
label: 'mesh.colors',
|
|
280
|
+
size: streamBytes,
|
|
281
|
+
usage: USAGE.VERTEX | USAGE.COPY_DST,
|
|
282
|
+
})
|
|
283
|
+
: null;
|
|
274
284
|
/*
|
|
275
285
|
* The chunks, in whole vertices and whole indices: a step never writes half of either, so the
|
|
276
286
|
* buffer never holds a torn one even for the frame between two steps.
|
|
277
287
|
*/
|
|
278
|
-
const verticesPerStep = Math.max(1, Math.floor(UPLOAD_BYTES_PER_STEP / (stride + (dynamic ? 24 : 0))));
|
|
288
|
+
const verticesPerStep = Math.max(1, Math.floor(UPLOAD_BYTES_PER_STEP / (stride + (dynamic ? 24 : 0) + (colorStream === null ? 0 : 12))));
|
|
279
289
|
/* Whole four-byte words a step, which is all a queue writes: two sixteen-bit indices to one. */
|
|
280
290
|
const indexWidth = indices.BYTES_PER_ELEMENT;
|
|
281
291
|
const indicesPerStep = Math.max(1, Math.floor(UPLOAD_BYTES_PER_STEP / 4)) * (4 / indexWidth);
|
|
@@ -288,13 +298,15 @@ export function createGpuMeshIncremental(device, data, dynamic = false) {
|
|
|
288
298
|
* gives every closure in a scope one context — so a getter returning a boolean held the whole
|
|
289
299
|
* `MeshData` and the interleaved rows for as long as the mesh lived. The voxel sandbox's forward
|
|
290
300
|
* path held 2,058 MB of ArrayBuffers at radius 32 against the second pipeline's 368.
|
|
291
|
-
* `meshRetention.test.ts` holds it: a mesh keeps its buffers
|
|
301
|
+
* `meshRetention.test.ts` holds it: a mesh keeps its buffers and none of its source, a dynamic
|
|
302
|
+
* one included.
|
|
292
303
|
*/
|
|
293
304
|
const progress = { uploaded: false };
|
|
294
305
|
const mesh = gpuMeshOf({
|
|
295
306
|
vertices,
|
|
296
307
|
constants,
|
|
297
308
|
streams,
|
|
309
|
+
colorStream,
|
|
298
310
|
indexBuffer,
|
|
299
311
|
indexFormat: indices instanceof Uint16Array ? 'uint16' : 'uint32',
|
|
300
312
|
stride: streams === null ? stride : 12,
|
|
@@ -314,6 +326,7 @@ export function createGpuMeshIncremental(device, data, dynamic = false) {
|
|
|
314
326
|
update: streams === null
|
|
315
327
|
? null
|
|
316
328
|
: streamUpdate(streams.positions, streams.normals, streamBytes, bounds, vertexCount),
|
|
329
|
+
updateColors: colorStream === null ? null : colorUpdate(colorStream, vertexCount),
|
|
317
330
|
});
|
|
318
331
|
const job = {
|
|
319
332
|
data,
|
|
@@ -322,6 +335,7 @@ export function createGpuMeshIncremental(device, data, dynamic = false) {
|
|
|
322
335
|
device,
|
|
323
336
|
fields,
|
|
324
337
|
streams,
|
|
338
|
+
colorStream,
|
|
325
339
|
vertices,
|
|
326
340
|
indexBuffer,
|
|
327
341
|
vertexCount,
|
|
@@ -368,6 +382,9 @@ function* uploadSteps(job) {
|
|
|
368
382
|
device.queue.writeBuffer(job.streams.positions, from * 12, data.positions, from * 3, count);
|
|
369
383
|
device.queue.writeBuffer(job.streams.normals, from * 12, data.normals, from * 3, count);
|
|
370
384
|
}
|
|
385
|
+
if (job.colorStream !== null) {
|
|
386
|
+
device.queue.writeBuffer(job.colorStream, from * 12, data.colors, from * 3, (to - from) * 3);
|
|
387
|
+
}
|
|
371
388
|
taken += 1;
|
|
372
389
|
if (taken < job.totalSteps)
|
|
373
390
|
yield;
|
|
@@ -461,11 +478,15 @@ function gpuMeshOf(parts) {
|
|
|
461
478
|
const motion = parts.update === null
|
|
462
479
|
? null
|
|
463
480
|
: { previous: null, staging: null, changed: -1 };
|
|
464
|
-
const { vertices, constants, streams, indexBuffer, progress } = parts;
|
|
481
|
+
const { vertices, constants, streams, colorStream, indexBuffer, progress } = parts;
|
|
482
|
+
const vertexBuffers = streams === null
|
|
483
|
+
? [vertices, constants]
|
|
484
|
+
: [streams.positions, streams.normals, vertices, constants];
|
|
485
|
+
/* Last, behind the constants, where `vertexBufferLayouts` puts it: no other slot moves. */
|
|
486
|
+
if (colorStream !== null)
|
|
487
|
+
vertexBuffers.push(colorStream);
|
|
465
488
|
return {
|
|
466
|
-
vertexBuffers
|
|
467
|
-
? [vertices, constants]
|
|
468
|
-
: [streams.positions, streams.normals, vertices, constants],
|
|
489
|
+
vertexBuffers,
|
|
469
490
|
vertexStride: parts.stride,
|
|
470
491
|
skinOffsets: parts.skinOffsets,
|
|
471
492
|
indexBuffer,
|
|
@@ -483,12 +504,14 @@ function gpuMeshOf(parts) {
|
|
|
483
504
|
hasChannel: parts.hasChannel,
|
|
484
505
|
morph: parts.morph,
|
|
485
506
|
update: parts.update,
|
|
507
|
+
updateColors: parts.updateColors,
|
|
486
508
|
motion,
|
|
487
509
|
dispose() {
|
|
488
510
|
motion?.previous?.destroy();
|
|
489
511
|
motion?.staging?.destroy();
|
|
490
512
|
streams?.positions.destroy();
|
|
491
513
|
streams?.normals.destroy();
|
|
514
|
+
colorStream?.destroy();
|
|
492
515
|
vertices.destroy();
|
|
493
516
|
constants.destroy();
|
|
494
517
|
indexBuffer.destroy();
|
|
@@ -534,6 +557,20 @@ function streamUpdate(positionsBuffer, normalsBuffer, streamBytes, bounds, verte
|
|
|
534
557
|
target.queue.writeBuffer(normalsBuffer, 0, normals);
|
|
535
558
|
};
|
|
536
559
|
}
|
|
560
|
+
/**
|
|
561
|
+
* A `dynamicColors` mesh's `updateColors`: the caller's colours, written as they came into the
|
|
562
|
+
* buffer they have to themselves.
|
|
563
|
+
*/
|
|
564
|
+
function colorUpdate(colorBuffer, vertexCount) {
|
|
565
|
+
return (target, colors) => {
|
|
566
|
+
if (colors.length !== vertexCount * 3) {
|
|
567
|
+
throw new Error(`this mesh has ${vertexCount} vertices and the colours have ${colors.length / 3}. ` +
|
|
568
|
+
"A mesh's vertex count is fixed at creation: the index buffer, the pipeline and " +
|
|
569
|
+
'every other attribute are sized against it.');
|
|
570
|
+
}
|
|
571
|
+
target.queue.writeBuffer(colorBuffer, 0, colors);
|
|
572
|
+
};
|
|
573
|
+
}
|
|
537
574
|
/**
|
|
538
575
|
* The buffer layouts a pipeline is built with, matching what `createGpuMesh` uploaded.
|
|
539
576
|
*
|
|
@@ -580,6 +617,8 @@ instanced = false) {
|
|
|
580
617
|
/* A dynamic mesh's positions and normals: buffers of their own, ahead of the rest. */
|
|
581
618
|
const dynamic = present['dynamic'] === true;
|
|
582
619
|
const layouts = [];
|
|
620
|
+
/* Attributes a caller rewrites, each a buffer of its own behind the constants: `vertexPacking.ts`. */
|
|
621
|
+
const trailing = [];
|
|
583
622
|
VERTEX_LAYOUT.forEach((declared, index) => {
|
|
584
623
|
const attribute = laidOut(declared, layered);
|
|
585
624
|
if (dynamic && isStreamed(attribute)) {
|
|
@@ -606,6 +645,15 @@ instanced = false) {
|
|
|
606
645
|
*/
|
|
607
646
|
/* One value every vertex shares sits with the absent ones: `vertexPacking.ts`. */
|
|
608
647
|
const packing = packingIn(present, String(attribute.name));
|
|
648
|
+
if (packing === 'stream') {
|
|
649
|
+
trailing.push({
|
|
650
|
+
arrayStride: attribute.components * 4,
|
|
651
|
+
attributes: [
|
|
652
|
+
{ shaderLocation: attribute.shaderLocation, offset: 0, format: attribute.format },
|
|
653
|
+
],
|
|
654
|
+
});
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
609
657
|
const supplied = (!attribute.optional || present[String(attribute.name)] === true) && packing !== 'constant';
|
|
610
658
|
if (instanced && INSTANCE_LOCATIONS.has(attribute.shaderLocation)) {
|
|
611
659
|
/*
|
|
@@ -644,6 +692,8 @@ instanced = false) {
|
|
|
644
692
|
* need a different layout from one that does not.
|
|
645
693
|
*/
|
|
646
694
|
layouts.push({ arrayStride: 0, attributes: constants });
|
|
695
|
+
for (const layout of trailing)
|
|
696
|
+
layouts.push(layout);
|
|
647
697
|
/*
|
|
648
698
|
* The four matrix columns and the tint — the five the flat shader's INSTANCED variant declares
|
|
649
699
|
* and exactly the five left once the base mesh has spent eleven of WebGL2's guaranteed sixteen.
|
|
@@ -38,8 +38,8 @@ export declare class GpuClothParticles {
|
|
|
38
38
|
private latest;
|
|
39
39
|
/** The frame each texture was last written for, so a motion knows whether the other is last frame's. */
|
|
40
40
|
private readonly written;
|
|
41
|
-
|
|
42
|
-
private
|
|
41
|
+
/** Each binding's views with these particles, one for each texture: see `viewsWith`. */
|
|
42
|
+
private readonly paired;
|
|
43
43
|
constructor(device: GPUDevice, count: number, storage?: boolean);
|
|
44
44
|
/** This frame's particles. */
|
|
45
45
|
get current(): GPUTextureView;
|
|
@@ -62,7 +62,10 @@ export declare class GpuClothParticles {
|
|
|
62
62
|
update(device: GPUDevice, positions: Float32Array, frame: number): void;
|
|
63
63
|
/**
|
|
64
64
|
* The three views a draw of `binding` with these particles binds, kept so a cached bind group
|
|
65
|
-
* compares equal frame after frame. Two objects, one for each texture that is current
|
|
65
|
+
* compares equal frame after frame. Two objects, one for each texture that is current, made the
|
|
66
|
+
* first time a binding is drawn with these particles and kept **for every binding**: several
|
|
67
|
+
* meshes follow one cloth, each with a binding of its own, and keeping only the last made two
|
|
68
|
+
* objects a draw as they took turns.
|
|
66
69
|
*/
|
|
67
70
|
viewsWith(binding: GpuClothBinding): ClothViews;
|
|
68
71
|
}
|
|
@@ -72,8 +72,8 @@ export class GpuClothParticles {
|
|
|
72
72
|
latest = 0;
|
|
73
73
|
/** The frame each texture was last written for, so a motion knows whether the other is last frame's. */
|
|
74
74
|
written = [-1, -1];
|
|
75
|
-
|
|
76
|
-
|
|
75
|
+
/** Each binding's views with these particles, one for each texture: see `viewsWith`. */
|
|
76
|
+
paired = new WeakMap();
|
|
77
77
|
constructor(device, count, storage = false) {
|
|
78
78
|
this.count = count;
|
|
79
79
|
const size = clothTextureSize(count);
|
|
@@ -129,17 +129,20 @@ export class GpuClothParticles {
|
|
|
129
129
|
}
|
|
130
130
|
/**
|
|
131
131
|
* The three views a draw of `binding` with these particles binds, kept so a cached bind group
|
|
132
|
-
* compares equal frame after frame. Two objects, one for each texture that is current
|
|
132
|
+
* compares equal frame after frame. Two objects, one for each texture that is current, made the
|
|
133
|
+
* first time a binding is drawn with these particles and kept **for every binding**: several
|
|
134
|
+
* meshes follow one cloth, each with a binding of its own, and keeping only the last made two
|
|
135
|
+
* objects a draw as they took turns.
|
|
133
136
|
*/
|
|
134
137
|
viewsWith(binding) {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
binding: binding.bindingView,
|
|
139
|
-
particles: this.views[
|
|
140
|
-
|
|
141
|
-
|
|
138
|
+
let pair = this.paired.get(binding);
|
|
139
|
+
if (pair === undefined) {
|
|
140
|
+
pair = [
|
|
141
|
+
{ binding: binding.bindingView, particles: this.views[0], rest: binding.restView },
|
|
142
|
+
{ binding: binding.bindingView, particles: this.views[1], rest: binding.restView },
|
|
143
|
+
];
|
|
144
|
+
this.paired.set(binding, pair);
|
|
142
145
|
}
|
|
143
|
-
return
|
|
146
|
+
return pair[this.latest === 0 ? 0 : 1];
|
|
144
147
|
}
|
|
145
148
|
}
|
|
@@ -11,9 +11,9 @@
|
|
|
11
11
|
* edits rather than twenty-three implementations.
|
|
12
12
|
*/
|
|
13
13
|
/**
|
|
14
|
-
* WebGPU's guaranteed `maxVertexBuffers`, so no verb can outgrow it. The most any binds is
|
|
15
|
-
* dynamic mesh's positions, normals, rows and
|
|
16
|
-
* this said three, then four, while text and bolts already bound four.
|
|
14
|
+
* WebGPU's guaranteed `maxVertexBuffers`, so no verb can outgrow it. The most any binds is six — a
|
|
15
|
+
* dynamic mesh's positions, normals, rows, constants and rewritten colours, and an instanced
|
|
16
|
+
* batch's placements — and this said three, then four, while text and bolts already bound four.
|
|
17
17
|
*/
|
|
18
18
|
const VERTEX_BUFFER_SLOTS = 8;
|
|
19
19
|
function emptyCommand() {
|
|
@@ -278,6 +278,7 @@ export class GpuClothSet {
|
|
|
278
278
|
u[base + 32] = garment.range.colliderBase;
|
|
279
279
|
u[base + 33] = garment.range.colliders;
|
|
280
280
|
u[base + 34] = garment.particles.width;
|
|
281
|
+
f[base + 35] = 1 / (1 + p.pull * h * h);
|
|
281
282
|
f[base + 36] = exactExp(p.damping * h);
|
|
282
283
|
f[base + 37] = exactExp(p.drag * h);
|
|
283
284
|
f[base + 38] = p.maxDistanceScale;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A few bind groups kept under one key, each found again by the four resources it was built with,
|
|
3
|
+
* compared by identity.
|
|
4
|
+
*
|
|
5
|
+
* **Why more than one.** A skinned draw's group was kept one a palette slot and material and made
|
|
6
|
+
* again whenever a resource in it differed from the last draw's. A garment's particles live in two
|
|
7
|
+
* textures that take turns, a step each, so a draw following cloth found the other texture every
|
|
8
|
+
* frame; and several meshes following one cloth bind one particle texture each with a binding of
|
|
9
|
+
* its own, so they replaced each other's group draw by draw. Reported at twenty-four groups made a
|
|
10
|
+
* frame for two characters, and none with cloth off. A shelf keeps each combination it has seen,
|
|
11
|
+
* so a steady garment's draws bind the groups of two frames ago and make none.
|
|
12
|
+
*
|
|
13
|
+
* **What it gives up** is a bound: `SHELF_CAPACITY` groups a key, replaced oldest first. Eight
|
|
14
|
+
* meshes following cloth under one palette slot and material fit, two groups each; a ninth makes
|
|
15
|
+
* groups again every frame, as every draw did before, and nothing worse. What would make it wrong
|
|
16
|
+
* is a draw whose resources change every frame for another reason, which a shelf cannot help and
|
|
17
|
+
* which would cycle it.
|
|
18
|
+
*/
|
|
19
|
+
/** Groups a shelf keeps before it replaces the oldest. See the module comment for the count. */
|
|
20
|
+
export declare const SHELF_CAPACITY = 16;
|
|
21
|
+
/** One key's groups and what each was built with, four identities a group, flat. */
|
|
22
|
+
export declare class GroupShelf {
|
|
23
|
+
private readonly groups;
|
|
24
|
+
private readonly made;
|
|
25
|
+
/** Where the next group goes: the oldest, once the shelf is full. */
|
|
26
|
+
private next;
|
|
27
|
+
/** The group built with exactly these four, or null. */
|
|
28
|
+
find(a: object | null, b: object | null, c: object | null, d: object | null): GPUBindGroup | null;
|
|
29
|
+
/** Keep `group` as the one built with these four. */
|
|
30
|
+
keep(group: GPUBindGroup, a: object | null, b: object | null, c: object | null, d: object | null): void;
|
|
31
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A few bind groups kept under one key, each found again by the four resources it was built with,
|
|
3
|
+
* compared by identity.
|
|
4
|
+
*
|
|
5
|
+
* **Why more than one.** A skinned draw's group was kept one a palette slot and material and made
|
|
6
|
+
* again whenever a resource in it differed from the last draw's. A garment's particles live in two
|
|
7
|
+
* textures that take turns, a step each, so a draw following cloth found the other texture every
|
|
8
|
+
* frame; and several meshes following one cloth bind one particle texture each with a binding of
|
|
9
|
+
* its own, so they replaced each other's group draw by draw. Reported at twenty-four groups made a
|
|
10
|
+
* frame for two characters, and none with cloth off. A shelf keeps each combination it has seen,
|
|
11
|
+
* so a steady garment's draws bind the groups of two frames ago and make none.
|
|
12
|
+
*
|
|
13
|
+
* **What it gives up** is a bound: `SHELF_CAPACITY` groups a key, replaced oldest first. Eight
|
|
14
|
+
* meshes following cloth under one palette slot and material fit, two groups each; a ninth makes
|
|
15
|
+
* groups again every frame, as every draw did before, and nothing worse. What would make it wrong
|
|
16
|
+
* is a draw whose resources change every frame for another reason, which a shelf cannot help and
|
|
17
|
+
* which would cycle it.
|
|
18
|
+
*/
|
|
19
|
+
/** Groups a shelf keeps before it replaces the oldest. See the module comment for the count. */
|
|
20
|
+
export const SHELF_CAPACITY = 16;
|
|
21
|
+
/** One key's groups and what each was built with, four identities a group, flat. */
|
|
22
|
+
export class GroupShelf {
|
|
23
|
+
groups = new Array(SHELF_CAPACITY).fill(null);
|
|
24
|
+
made = new Array(SHELF_CAPACITY * 4).fill(null);
|
|
25
|
+
/** Where the next group goes: the oldest, once the shelf is full. */
|
|
26
|
+
next = 0;
|
|
27
|
+
/** The group built with exactly these four, or null. */
|
|
28
|
+
find(a, b, c, d) {
|
|
29
|
+
for (let slot = 0; slot < SHELF_CAPACITY; slot++) {
|
|
30
|
+
const group = this.groups[slot] ?? null;
|
|
31
|
+
if (group === null)
|
|
32
|
+
return null;
|
|
33
|
+
const at = slot * 4;
|
|
34
|
+
if (this.made[at] === a &&
|
|
35
|
+
this.made[at + 1] === b &&
|
|
36
|
+
this.made[at + 2] === c &&
|
|
37
|
+
this.made[at + 3] === d) {
|
|
38
|
+
return group;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
/** Keep `group` as the one built with these four. */
|
|
44
|
+
keep(group, a, b, c, d) {
|
|
45
|
+
const slot = this.next;
|
|
46
|
+
this.groups[slot] = group;
|
|
47
|
+
const at = slot * 4;
|
|
48
|
+
this.made[at] = a;
|
|
49
|
+
this.made[at + 1] = b;
|
|
50
|
+
this.made[at + 2] = c;
|
|
51
|
+
this.made[at + 3] = d;
|
|
52
|
+
this.next = (slot + 1) % SHELF_CAPACITY;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A point skin on WebGPU: its points, joints and normals in storage buffers made once, and a pose
|
|
3
|
+
* recorded as one compute dispatch into the dynamic mesh's own position and normal buffers. What a
|
|
4
|
+
* point skin is, and why it poses into a mesh rather than in a vertex stage, is `pointSkin.ts`.
|
|
5
|
+
*
|
|
6
|
+
* **Each skin owns its joints buffer**, so two skins posed in one frame read their own: a write to
|
|
7
|
+
* one buffer between two dispatches would hand both the last, the per-draw rule `AGENTS.md` keeps
|
|
8
|
+
* for every resource a later write in the frame overwrites. A skin posed twice in a frame draws its
|
|
9
|
+
* last pose everywhere, as a mesh rewritten twice with `updateMesh` does.
|
|
10
|
+
*/
|
|
11
|
+
import { type PointSkinData, type PointSkinReach } from '../../pointSkin.ts';
|
|
12
|
+
import type { GpuMesh } from './buffers.ts';
|
|
13
|
+
/** The pipeline every skin on a device poses with, made at the first skin. */
|
|
14
|
+
export declare function createPointSkinPipeline(device: GPUDevice): GPUComputePipeline;
|
|
15
|
+
export declare class GpuPointSkin {
|
|
16
|
+
readonly mesh: GpuMesh;
|
|
17
|
+
readonly reach: PointSkinReach;
|
|
18
|
+
private readonly count;
|
|
19
|
+
private readonly pipeline;
|
|
20
|
+
private readonly joints;
|
|
21
|
+
private readonly buffers;
|
|
22
|
+
private readonly group;
|
|
23
|
+
constructor(device: GPUDevice, pipeline: GPUComputePipeline, mesh: GpuMesh, data: PointSkinData);
|
|
24
|
+
/** Write the pose's joints and record the dispatch that poses every vertex into `encoder`. */
|
|
25
|
+
pose(device: GPUDevice, encoder: GPUCommandEncoder, matrices: Float32Array): void;
|
|
26
|
+
dispose(): void;
|
|
27
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A point skin on WebGPU: its points, joints and normals in storage buffers made once, and a pose
|
|
3
|
+
* recorded as one compute dispatch into the dynamic mesh's own position and normal buffers. What a
|
|
4
|
+
* point skin is, and why it poses into a mesh rather than in a vertex stage, is `pointSkin.ts`.
|
|
5
|
+
*
|
|
6
|
+
* **Each skin owns its joints buffer**, so two skins posed in one frame read their own: a write to
|
|
7
|
+
* one buffer between two dispatches would hand both the last, the per-draw rule `AGENTS.md` keeps
|
|
8
|
+
* for every resource a later write in the frame overwrites. A skin posed twice in a frame draws its
|
|
9
|
+
* last pose everywhere, as a mesh rewritten twice with `updateMesh` does.
|
|
10
|
+
*/
|
|
11
|
+
import { reachOf } from '../../pointSkin.js';
|
|
12
|
+
import { POINT_SKIN_WGSL, POINT_SKIN_WORKGROUP } from './shaders/pointSkin.wgsl.js';
|
|
13
|
+
const STORAGE = 0x0080;
|
|
14
|
+
const UNIFORM = 0x0040;
|
|
15
|
+
const COPY_DST = 0x0008;
|
|
16
|
+
/** The pipeline every skin on a device poses with, made at the first skin. */
|
|
17
|
+
export function createPointSkinPipeline(device) {
|
|
18
|
+
return device.createComputePipeline({
|
|
19
|
+
label: 'pointSkin.pose',
|
|
20
|
+
layout: 'auto',
|
|
21
|
+
compute: {
|
|
22
|
+
module: device.createShaderModule({ label: 'pointSkin.pose', code: POINT_SKIN_WGSL }),
|
|
23
|
+
entryPoint: 'posePoints',
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
export class GpuPointSkin {
|
|
28
|
+
mesh;
|
|
29
|
+
reach;
|
|
30
|
+
count;
|
|
31
|
+
pipeline;
|
|
32
|
+
joints;
|
|
33
|
+
buffers;
|
|
34
|
+
group;
|
|
35
|
+
constructor(device, pipeline, mesh, data) {
|
|
36
|
+
this.mesh = mesh;
|
|
37
|
+
this.pipeline = pipeline;
|
|
38
|
+
this.reach = reachOf(data);
|
|
39
|
+
const count = data.weights.length / 4;
|
|
40
|
+
this.count = count;
|
|
41
|
+
/* Each influence a vec4: its point, and its weight in w. */
|
|
42
|
+
const points = new Float32Array(count * 16);
|
|
43
|
+
for (let i = 0; i < count * 4; i++) {
|
|
44
|
+
points.set(data.points.subarray(i * 3, i * 3 + 3), i * 4);
|
|
45
|
+
points[i * 4 + 3] = data.weights[i];
|
|
46
|
+
}
|
|
47
|
+
const named = Uint32Array.from(data.joints);
|
|
48
|
+
const normals = new Float32Array(count * 4);
|
|
49
|
+
for (let v = 0; v < count; v++)
|
|
50
|
+
normals.set(data.normals.subarray(v * 3, v * 3 + 3), v * 4);
|
|
51
|
+
const params = new Uint32Array([count, 0, 0, 0]);
|
|
52
|
+
const made = (label, contents, usage) => {
|
|
53
|
+
const buffer = device.createBuffer({
|
|
54
|
+
label,
|
|
55
|
+
size: Math.max(16, contents.byteLength),
|
|
56
|
+
usage: usage | COPY_DST,
|
|
57
|
+
});
|
|
58
|
+
device.queue.writeBuffer(buffer, 0, contents);
|
|
59
|
+
return buffer;
|
|
60
|
+
};
|
|
61
|
+
this.joints = device.createBuffer({
|
|
62
|
+
label: 'pointSkin.joints',
|
|
63
|
+
size: Math.max(1, this.reach.joints) * 64,
|
|
64
|
+
usage: STORAGE | COPY_DST,
|
|
65
|
+
});
|
|
66
|
+
const buffers = [
|
|
67
|
+
made('pointSkin.params', params, UNIFORM),
|
|
68
|
+
made('pointSkin.points', points, STORAGE),
|
|
69
|
+
made('pointSkin.named', named, STORAGE),
|
|
70
|
+
made('pointSkin.normals', normals, STORAGE),
|
|
71
|
+
];
|
|
72
|
+
this.buffers = buffers;
|
|
73
|
+
const [positionsOut, normalsOut] = mesh.vertexBuffers;
|
|
74
|
+
this.group = device.createBindGroup({
|
|
75
|
+
label: 'pointSkin.pose',
|
|
76
|
+
layout: pipeline.getBindGroupLayout(0),
|
|
77
|
+
entries: [
|
|
78
|
+
{ binding: 0, resource: { buffer: buffers[0] } },
|
|
79
|
+
{ binding: 1, resource: { buffer: this.joints } },
|
|
80
|
+
{ binding: 2, resource: { buffer: buffers[1] } },
|
|
81
|
+
{ binding: 3, resource: { buffer: buffers[2] } },
|
|
82
|
+
{ binding: 4, resource: { buffer: buffers[3] } },
|
|
83
|
+
{ binding: 5, resource: { buffer: positionsOut } },
|
|
84
|
+
{ binding: 6, resource: { buffer: normalsOut } },
|
|
85
|
+
],
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
/** Write the pose's joints and record the dispatch that poses every vertex into `encoder`. */
|
|
89
|
+
pose(device, encoder, matrices) {
|
|
90
|
+
if (this.reach.joints > 0) {
|
|
91
|
+
device.queue.writeBuffer(this.joints, 0, matrices, 0, this.reach.joints * 16);
|
|
92
|
+
}
|
|
93
|
+
const pass = encoder.beginComputePass({ label: 'pointSkin.pose' });
|
|
94
|
+
pass.setPipeline(this.pipeline);
|
|
95
|
+
pass.setBindGroup(0, this.group);
|
|
96
|
+
pass.dispatchWorkgroups(Math.ceil(this.count / POINT_SKIN_WORKGROUP));
|
|
97
|
+
pass.end();
|
|
98
|
+
}
|
|
99
|
+
dispose() {
|
|
100
|
+
this.joints.destroy();
|
|
101
|
+
for (const buffer of this.buffers)
|
|
102
|
+
buffer.destroy();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -22,6 +22,8 @@ import { GpuText } from './textPass.ts';
|
|
|
22
22
|
import type { TextStyle } from '../../textLayout.ts';
|
|
23
23
|
import { GpuSurfaceTexture } from './surfaceTexturePass.ts';
|
|
24
24
|
import { GpuClothBinding, GpuClothParticles } from './clothTextures.ts';
|
|
25
|
+
import { GpuPointSkin } from './pointSkinPass.ts';
|
|
26
|
+
import { type PointSkinData } from '../../pointSkin.ts';
|
|
25
27
|
import type { ComputeHost } from './computeHost.ts';
|
|
26
28
|
import { type SurfaceOverlay } from '../../surfaceOverlay.ts';
|
|
27
29
|
import type { ClothBindingData } from '../../clothBindingData.ts';
|
|
@@ -1206,6 +1208,10 @@ export declare class WebGPURenderer implements RendererApi {
|
|
|
1206
1208
|
*/
|
|
1207
1209
|
private readonly transmission;
|
|
1208
1210
|
private warnedLayersModelMap;
|
|
1211
|
+
/** `uDetail`, the material's tiled detail packed by `packSurfaceDetail`. */
|
|
1212
|
+
private readonly detailFloats;
|
|
1213
|
+
/** The detail refusals already said, each once. See `detailOfMaterial`. */
|
|
1214
|
+
private readonly warnedDetail;
|
|
1209
1215
|
private warnedLayersTriplanar;
|
|
1210
1216
|
/** Its numbers, `uModelParams`, packed by `packModel`. */
|
|
1211
1217
|
private readonly modelParams;
|
|
@@ -1951,17 +1957,14 @@ export declare class WebGPURenderer implements RendererApi {
|
|
|
1951
1957
|
/** The same layout plus the joint palette. See `createDepthBindGroupLayout`. */
|
|
1952
1958
|
private readonly shadowSkinnedLayout;
|
|
1953
1959
|
/**
|
|
1954
|
-
* The skinned shadow bind groups,
|
|
1960
|
+
* The skinned shadow bind groups, a shelf per palette slot, each found by what it was built with.
|
|
1955
1961
|
*
|
|
1956
1962
|
* The same argument `flatBindGroupFor` makes, and per slot for the same reason: the group it was
|
|
1957
1963
|
* built beside, and the palette view, which now differs between two characters casting into one
|
|
1958
|
-
* bake
|
|
1964
|
+
* bake, and the cloth's particles and binding, which take turns. Comparing object identity makes
|
|
1965
|
+
* a missed invalidation impossible rather than unlikely. See `groupShelf.ts`.
|
|
1959
1966
|
*/
|
|
1960
|
-
private readonly
|
|
1961
|
-
private readonly shadowSkinnedGroupsBeside;
|
|
1962
|
-
private readonly shadowSkinnedGroupsPalette;
|
|
1963
|
-
private readonly shadowSkinnedGroupsCloth;
|
|
1964
|
-
private readonly shadowSkinnedGroupsClothBinding;
|
|
1967
|
+
private readonly shadowSkinnedShelves;
|
|
1965
1968
|
/** Held from construction so the skinned group can be rebuilt without them being threaded. */
|
|
1966
1969
|
private shadowBlankView;
|
|
1967
1970
|
private shadowBlankSampler;
|
|
@@ -2113,6 +2116,11 @@ export declare class WebGPURenderer implements RendererApi {
|
|
|
2113
2116
|
* A material's layers where they can be drawn: none asked, or a model's map in the mask's place,
|
|
2114
2117
|
* said once, leave one layer.
|
|
2115
2118
|
*/
|
|
2119
|
+
/**
|
|
2120
|
+
* A material's detail, or null where it is refused: its slot holds another array, or it is placed
|
|
2121
|
+
* by the world. Each said once. This backend has room for its vectors wherever it runs.
|
|
2122
|
+
*/
|
|
2123
|
+
private detailOfMaterial;
|
|
2116
2124
|
private layersOf;
|
|
2117
2125
|
/** Says once that a layered material's triplanar projection is laid on the horizontal plane. */
|
|
2118
2126
|
private warnLayersTriplanar;
|
|
@@ -2384,6 +2392,31 @@ export declare class WebGPURenderer implements RendererApi {
|
|
|
2384
2392
|
* creation. See `GpuMesh.update`.
|
|
2385
2393
|
*/
|
|
2386
2394
|
updateMesh(mesh: GpuMesh, positions: Float32Array, normals?: Float32Array): void;
|
|
2395
|
+
/** The pipeline every point skin poses with, made at the first one. See `pointSkinPass.ts`. */
|
|
2396
|
+
private pointSkinPipeline;
|
|
2397
|
+
/**
|
|
2398
|
+
* A dynamic mesh posed on the device from weighted points in its joints' spaces. Refused in words
|
|
2399
|
+
* on a mesh that is not dynamic, whose positions share one buffer with the rest of each vertex.
|
|
2400
|
+
*/
|
|
2401
|
+
createPointSkin(mesh: GpuMesh, data: PointSkinData): GpuPointSkin;
|
|
2402
|
+
/**
|
|
2403
|
+
* Pose a skin: `matrices` holds each joint's transform into the mesh's model space, sixteen floats,
|
|
2404
|
+
* column-major. Recorded into the encoder the frame's next submit leads with, behind a copy of
|
|
2405
|
+
* last frame's positions where a reconstruction keeps them, as `updateMesh` records its rewrite.
|
|
2406
|
+
*/
|
|
2407
|
+
posePointSkin(skin: GpuPointSkin, matrices: Float32Array): void;
|
|
2408
|
+
/**
|
|
2409
|
+
* **The pending rewrites go first**, since a pose recorded into them reads the skin's buffers and
|
|
2410
|
+
* a destroyed buffer in a submit loses the whole command buffer. Nothing else reads them, so once
|
|
2411
|
+
* the rewrites are submitted, which only moves them earlier, the buffers can go at once.
|
|
2412
|
+
*/
|
|
2413
|
+
disposePointSkin(skin: GpuPointSkin): void;
|
|
2414
|
+
/**
|
|
2415
|
+
* Rewrite a mesh's colours. Refused in words on a mesh that did not ask for `dynamicColors`,
|
|
2416
|
+
* whose colours are interleaved with the rest of a vertex or are one value with no buffer at all.
|
|
2417
|
+
* See `GpuMesh.updateColors`.
|
|
2418
|
+
*/
|
|
2419
|
+
updateMeshColors(mesh: GpuMesh, colors: Float32Array): void;
|
|
2387
2420
|
/**
|
|
2388
2421
|
* Settle everything the frame shares: the camera, the lights and the fog.
|
|
2389
2422
|
*
|