@driftengine/core 4.13.1 → 4.13.2
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 +31 -0
- package/README.md +1 -1
- package/dist/index.d.ts +2 -1
- 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 +21 -0
- package/dist/render/backend/webgl2/renderer.js +41 -2
- 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 +31 -7
- package/dist/render/backend/webgpu/renderer.js +118 -47
- 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/vertexPacking.d.ts +16 -4
- package/dist/render/vertexPacking.js +10 -3
- package/package.json +6 -6
- package/src/index.ts +2 -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 +63 -2
- 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 +150 -82
- 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/vertexPacking.ts +25 -5
|
@@ -377,6 +377,13 @@ import type { SurfaceTextureHandle } from '../api.ts';
|
|
|
377
377
|
import { materialHasMaps, noteMapsWithoutUvs } from '../../mapsWithoutUvs.ts';
|
|
378
378
|
import { VIEW_MODEL_DEPTH_SHARE, viewModelDepthRange } from '../../viewModel.ts';
|
|
379
379
|
import { noLitSwitches, type LitSwitch } from '../../shaders/flat/glassTint.ts';
|
|
380
|
+
import {
|
|
381
|
+
poseBounds,
|
|
382
|
+
validatePointSkin,
|
|
383
|
+
validatePose,
|
|
384
|
+
type PointSkinData,
|
|
385
|
+
} from '../../pointSkin.ts';
|
|
386
|
+
import { GlPointSkin, createPointSkinPose, type PointSkinPose } from './pointSkinFeedback.ts';
|
|
380
387
|
|
|
381
388
|
/**
|
|
382
389
|
* How many frames `firstFrameSettled` will ask before it stops asking.
|
|
@@ -3624,7 +3631,13 @@ export class WebGL2Renderer implements RendererApi {
|
|
|
3624
3631
|
*/
|
|
3625
3632
|
const drawn = withLightmapUvs(data);
|
|
3626
3633
|
this.warmVariants(drawn);
|
|
3627
|
-
return new Mesh(
|
|
3634
|
+
return new Mesh(
|
|
3635
|
+
this.gl,
|
|
3636
|
+
drawn,
|
|
3637
|
+
options.dynamic === true,
|
|
3638
|
+
false,
|
|
3639
|
+
options.dynamicColors === true,
|
|
3640
|
+
);
|
|
3628
3641
|
}
|
|
3629
3642
|
|
|
3630
3643
|
/**
|
|
@@ -3639,7 +3652,12 @@ export class WebGL2Renderer implements RendererApi {
|
|
|
3639
3652
|
createMeshIncremental(data: MeshData, options: MeshOptions = {}): IncrementalMesh {
|
|
3640
3653
|
const drawn = withLightmapUvs(data);
|
|
3641
3654
|
this.warmVariants(drawn);
|
|
3642
|
-
const { mesh, upload } = createMeshIncremental(
|
|
3655
|
+
const { mesh, upload } = createMeshIncremental(
|
|
3656
|
+
this.gl,
|
|
3657
|
+
drawn,
|
|
3658
|
+
options.dynamic === true,
|
|
3659
|
+
options.dynamicColors === true,
|
|
3660
|
+
);
|
|
3643
3661
|
return {
|
|
3644
3662
|
mesh,
|
|
3645
3663
|
upload: {
|
|
@@ -3692,6 +3710,49 @@ export class WebGL2Renderer implements RendererApi {
|
|
|
3692
3710
|
mesh.update(this.gl, positions, normals);
|
|
3693
3711
|
}
|
|
3694
3712
|
|
|
3713
|
+
/** The program and feedback object every point skin poses with, made at the first one. */
|
|
3714
|
+
private pointSkinPose: PointSkinPose | null = null;
|
|
3715
|
+
|
|
3716
|
+
/**
|
|
3717
|
+
* A dynamic mesh posed on the device from weighted points in its joints' spaces, by transform
|
|
3718
|
+
* feedback. Refused in words on a mesh that is not dynamic, whose buffers are not hinted for a
|
|
3719
|
+
* rewrite every frame.
|
|
3720
|
+
*/
|
|
3721
|
+
createPointSkin(mesh: Mesh, data: PointSkinData): GlPointSkin {
|
|
3722
|
+
if (mesh.streams === null) {
|
|
3723
|
+
throw new Error(
|
|
3724
|
+
'a point skin poses a mesh created with { dynamic: true }, whose positions and normals ' +
|
|
3725
|
+
'are rewritten every frame. Say so at `createMesh`.',
|
|
3726
|
+
);
|
|
3727
|
+
}
|
|
3728
|
+
validatePointSkin(data, mesh.vertexCount);
|
|
3729
|
+
this.pointSkinPose ??= createPointSkinPose(this.gl);
|
|
3730
|
+
return new GlPointSkin(this.gl, mesh, data);
|
|
3731
|
+
}
|
|
3732
|
+
|
|
3733
|
+
/**
|
|
3734
|
+
* Pose a skin now: `matrices` holds each joint's transform into the mesh's model space, sixteen
|
|
3735
|
+
* floats, column-major. A command in the stream, so the draws after it read this pose.
|
|
3736
|
+
*/
|
|
3737
|
+
posePointSkin(skin: GlPointSkin, matrices: Float32Array): void {
|
|
3738
|
+
validatePose(matrices, skin.reach);
|
|
3739
|
+
if (this.contextLost || this.pointSkinPose === null) return;
|
|
3740
|
+
poseBounds(matrices, skin.reach, skin.mesh.bounds);
|
|
3741
|
+
skin.pose(this.gl, this.pointSkinPose, matrices);
|
|
3742
|
+
}
|
|
3743
|
+
|
|
3744
|
+
disposePointSkin(skin: GlPointSkin): void {
|
|
3745
|
+
skin.dispose(this.gl);
|
|
3746
|
+
}
|
|
3747
|
+
|
|
3748
|
+
/**
|
|
3749
|
+
* Rewrite a mesh's colours, on a mesh made `dynamicColors`. The VAO is left alone for the reason
|
|
3750
|
+
* `updateMesh` gives: the buffer is the same buffer.
|
|
3751
|
+
*/
|
|
3752
|
+
updateMeshColors(mesh: Mesh, colors: Float32Array): void {
|
|
3753
|
+
mesh.updateColors(this.gl, colors);
|
|
3754
|
+
}
|
|
3755
|
+
|
|
3695
3756
|
/**
|
|
3696
3757
|
* Compile the skinned flat program, once, on the first rigged mesh.
|
|
3697
3758
|
*
|
|
@@ -39,6 +39,7 @@ const USAGE = {
|
|
|
39
39
|
INDEX: 0x0010,
|
|
40
40
|
VERTEX: 0x0020,
|
|
41
41
|
UNIFORM: 0x0040,
|
|
42
|
+
STORAGE: 0x0080,
|
|
42
43
|
} as const;
|
|
43
44
|
|
|
44
45
|
/**
|
|
@@ -276,6 +277,14 @@ export interface GpuMesh {
|
|
|
276
277
|
staged?: StagedRewrite | null,
|
|
277
278
|
) => void)
|
|
278
279
|
| null;
|
|
280
|
+
/**
|
|
281
|
+
* Rewrite this mesh's colours, three floats a vertex.
|
|
282
|
+
*
|
|
283
|
+
* Absent unless the mesh was created `dynamicColors`, whose colours are a buffer of their own at
|
|
284
|
+
* the end of `vertexBuffers` (`vertexPacking.ts`), so this is one `writeBuffer` and nothing else
|
|
285
|
+
* moves: not the positions, not the bounds, and no motion, since a colour does not move.
|
|
286
|
+
*/
|
|
287
|
+
readonly updateColors: ((device: GPUDevice, colors: Float32Array) => void) | null;
|
|
279
288
|
/**
|
|
280
289
|
* A dynamic mesh's motion state, which the renderer owns: a buffer for last frame's positions and
|
|
281
290
|
* one the new positions are staged in, each made the first time a reconstructing renderer
|
|
@@ -328,8 +337,13 @@ export interface IncrementalGpuMesh {
|
|
|
328
337
|
* two paths cannot write different bytes. The one that spreads the work is the one with the
|
|
329
338
|
* interesting decisions in it; read that.
|
|
330
339
|
*/
|
|
331
|
-
export function createGpuMesh(
|
|
332
|
-
|
|
340
|
+
export function createGpuMesh(
|
|
341
|
+
device: GPUDevice,
|
|
342
|
+
data: MeshData,
|
|
343
|
+
dynamic = false,
|
|
344
|
+
dynamicColors = false,
|
|
345
|
+
): GpuMesh {
|
|
346
|
+
const { mesh, upload } = createGpuMeshIncremental(device, data, dynamic, dynamicColors);
|
|
333
347
|
while (upload.next().done !== true);
|
|
334
348
|
return mesh;
|
|
335
349
|
}
|
|
@@ -363,6 +377,7 @@ export function createGpuMeshIncremental(
|
|
|
363
377
|
device: GPUDevice,
|
|
364
378
|
data: MeshData,
|
|
365
379
|
dynamic = false,
|
|
380
|
+
dynamicColors = false,
|
|
366
381
|
): IncrementalGpuMesh {
|
|
367
382
|
/*
|
|
368
383
|
* The other backend's refusal, and the premise of the interleave below: every attribute holds
|
|
@@ -373,11 +388,11 @@ export function createGpuMeshIncremental(
|
|
|
373
388
|
const vertexCount = data.positions.length / 3;
|
|
374
389
|
const layered = data.layers !== undefined;
|
|
375
390
|
/* How each attribute travels, which the pipeline key says too: `vertexPacking.ts`. */
|
|
376
|
-
const packing = vertexPackingOf(data, dynamic);
|
|
391
|
+
const packing = vertexPackingOf(data, dynamic, dynamicColors);
|
|
377
392
|
|
|
378
393
|
/* Interleaved: one stride holding every attribute the mesh actually supplied — less a dynamic
|
|
379
|
-
mesh's positions and normals, which are buffers of their
|
|
380
|
-
shares, which is a constant. */
|
|
394
|
+
mesh's positions and normals and a `dynamicColors` mesh's colours, which are buffers of their
|
|
395
|
+
own, and any attribute every vertex shares, which is a constant. */
|
|
381
396
|
const fields: Field[] = [];
|
|
382
397
|
let stride = 0;
|
|
383
398
|
for (const declared of VERTEX_LAYOUT) {
|
|
@@ -385,7 +400,7 @@ export function createGpuMeshIncremental(
|
|
|
385
400
|
if (dynamic && isStreamed(declared)) continue;
|
|
386
401
|
const attribute = laidOut(declared, layered);
|
|
387
402
|
const how = packingFor(packing, attribute.name);
|
|
388
|
-
if (how === 'constant') continue;
|
|
403
|
+
if (how === 'constant' || how === 'stream') continue;
|
|
389
404
|
const format = packedFormat(attribute.format, how);
|
|
390
405
|
fields.push({ attribute, packing: how, format, offset: stride });
|
|
391
406
|
stride += formatBytes(format);
|
|
@@ -466,22 +481,32 @@ export function createGpuMeshIncremental(
|
|
|
466
481
|
usage: USAGE.INDEX | USAGE.COPY_DST,
|
|
467
482
|
});
|
|
468
483
|
|
|
469
|
-
/* A dynamic mesh's two moving streams: copied from as well as written, for last frame's positions
|
|
484
|
+
/* A dynamic mesh's two moving streams: copied from as well as written, for last frame's positions,
|
|
485
|
+
and written by a point skin's pose on the device (`pointSkin.ts`). */
|
|
470
486
|
const streamBytes = align4(vertexCount * 12);
|
|
471
487
|
const streams = dynamic
|
|
472
488
|
? {
|
|
473
489
|
positions: device.createBuffer({
|
|
474
490
|
label: 'mesh.positions',
|
|
475
491
|
size: streamBytes,
|
|
476
|
-
usage: USAGE.VERTEX | USAGE.COPY_DST | USAGE.COPY_SRC,
|
|
492
|
+
usage: USAGE.VERTEX | USAGE.COPY_DST | USAGE.COPY_SRC | USAGE.STORAGE,
|
|
477
493
|
}),
|
|
478
494
|
normals: device.createBuffer({
|
|
479
495
|
label: 'mesh.normals',
|
|
480
496
|
size: streamBytes,
|
|
481
|
-
usage: USAGE.VERTEX | USAGE.COPY_DST,
|
|
497
|
+
usage: USAGE.VERTEX | USAGE.COPY_DST | USAGE.STORAGE,
|
|
482
498
|
}),
|
|
483
499
|
}
|
|
484
500
|
: null;
|
|
501
|
+
/* And a `dynamicColors` mesh's colours, which only the caller's rewrites ever change. */
|
|
502
|
+
const colorStream =
|
|
503
|
+
packing.colors === 'stream'
|
|
504
|
+
? device.createBuffer({
|
|
505
|
+
label: 'mesh.colors',
|
|
506
|
+
size: streamBytes,
|
|
507
|
+
usage: USAGE.VERTEX | USAGE.COPY_DST,
|
|
508
|
+
})
|
|
509
|
+
: null;
|
|
485
510
|
|
|
486
511
|
/*
|
|
487
512
|
* The chunks, in whole vertices and whole indices: a step never writes half of either, so the
|
|
@@ -489,7 +514,9 @@ export function createGpuMeshIncremental(
|
|
|
489
514
|
*/
|
|
490
515
|
const verticesPerStep = Math.max(
|
|
491
516
|
1,
|
|
492
|
-
Math.floor(
|
|
517
|
+
Math.floor(
|
|
518
|
+
UPLOAD_BYTES_PER_STEP / (stride + (dynamic ? 24 : 0) + (colorStream === null ? 0 : 12)),
|
|
519
|
+
),
|
|
493
520
|
);
|
|
494
521
|
/* Whole four-byte words a step, which is all a queue writes: two sixteen-bit indices to one. */
|
|
495
522
|
const indexWidth = indices.BYTES_PER_ELEMENT;
|
|
@@ -505,13 +532,15 @@ export function createGpuMeshIncremental(
|
|
|
505
532
|
* gives every closure in a scope one context — so a getter returning a boolean held the whole
|
|
506
533
|
* `MeshData` and the interleaved rows for as long as the mesh lived. The voxel sandbox's forward
|
|
507
534
|
* path held 2,058 MB of ArrayBuffers at radius 32 against the second pipeline's 368.
|
|
508
|
-
* `meshRetention.test.ts` holds it: a mesh keeps its buffers
|
|
535
|
+
* `meshRetention.test.ts` holds it: a mesh keeps its buffers and none of its source, a dynamic
|
|
536
|
+
* one included.
|
|
509
537
|
*/
|
|
510
538
|
const progress = { uploaded: false };
|
|
511
539
|
const mesh = gpuMeshOf({
|
|
512
540
|
vertices,
|
|
513
541
|
constants,
|
|
514
542
|
streams,
|
|
543
|
+
colorStream,
|
|
515
544
|
indexBuffer,
|
|
516
545
|
indexFormat: indices instanceof Uint16Array ? 'uint16' : 'uint32',
|
|
517
546
|
stride: streams === null ? stride : 12,
|
|
@@ -538,6 +567,7 @@ export function createGpuMeshIncremental(
|
|
|
538
567
|
streams === null
|
|
539
568
|
? null
|
|
540
569
|
: streamUpdate(streams.positions, streams.normals, streamBytes, bounds, vertexCount),
|
|
570
|
+
updateColors: colorStream === null ? null : colorUpdate(colorStream, vertexCount),
|
|
541
571
|
});
|
|
542
572
|
|
|
543
573
|
const job: UploadJob = {
|
|
@@ -547,6 +577,7 @@ export function createGpuMeshIncremental(
|
|
|
547
577
|
device,
|
|
548
578
|
fields,
|
|
549
579
|
streams,
|
|
580
|
+
colorStream,
|
|
550
581
|
vertices,
|
|
551
582
|
indexBuffer,
|
|
552
583
|
vertexCount,
|
|
@@ -588,6 +619,7 @@ interface UploadJob {
|
|
|
588
619
|
readonly device: GPUDevice;
|
|
589
620
|
readonly fields: readonly Field[];
|
|
590
621
|
readonly streams: { readonly positions: GPUBuffer; readonly normals: GPUBuffer } | null;
|
|
622
|
+
readonly colorStream: GPUBuffer | null;
|
|
591
623
|
readonly vertices: GPUBuffer;
|
|
592
624
|
readonly indexBuffer: GPUBuffer;
|
|
593
625
|
readonly vertexCount: number;
|
|
@@ -662,6 +694,9 @@ function* uploadSteps(job: UploadJob): Generator<void, void, void> {
|
|
|
662
694
|
device.queue.writeBuffer(job.streams.positions, from * 12, data.positions, from * 3, count);
|
|
663
695
|
device.queue.writeBuffer(job.streams.normals, from * 12, data.normals, from * 3, count);
|
|
664
696
|
}
|
|
697
|
+
if (job.colorStream !== null) {
|
|
698
|
+
device.queue.writeBuffer(job.colorStream, from * 12, data.colors, from * 3, (to - from) * 3);
|
|
699
|
+
}
|
|
665
700
|
taken += 1;
|
|
666
701
|
if (taken < job.totalSteps) yield;
|
|
667
702
|
}
|
|
@@ -759,6 +794,7 @@ function gpuMeshOf(parts: {
|
|
|
759
794
|
readonly vertices: GPUBuffer;
|
|
760
795
|
readonly constants: GPUBuffer;
|
|
761
796
|
readonly streams: { readonly positions: GPUBuffer; readonly normals: GPUBuffer } | null;
|
|
797
|
+
readonly colorStream: GPUBuffer | null;
|
|
762
798
|
readonly indexBuffer: GPUBuffer;
|
|
763
799
|
readonly indexFormat: GPUIndexFormat;
|
|
764
800
|
readonly stride: number;
|
|
@@ -774,17 +810,21 @@ function gpuMeshOf(parts: {
|
|
|
774
810
|
readonly hasChannel: boolean;
|
|
775
811
|
readonly morph: MorphTexture | null;
|
|
776
812
|
readonly update: GpuMesh['update'];
|
|
813
|
+
readonly updateColors: GpuMesh['updateColors'];
|
|
777
814
|
}): GpuMesh {
|
|
778
815
|
const motion =
|
|
779
816
|
parts.update === null
|
|
780
817
|
? null
|
|
781
818
|
: { previous: null as GPUBuffer | null, staging: null as GPUBuffer | null, changed: -1 };
|
|
782
|
-
const { vertices, constants, streams, indexBuffer, progress } = parts;
|
|
819
|
+
const { vertices, constants, streams, colorStream, indexBuffer, progress } = parts;
|
|
820
|
+
const vertexBuffers =
|
|
821
|
+
streams === null
|
|
822
|
+
? [vertices, constants]
|
|
823
|
+
: [streams.positions, streams.normals, vertices, constants];
|
|
824
|
+
/* Last, behind the constants, where `vertexBufferLayouts` puts it: no other slot moves. */
|
|
825
|
+
if (colorStream !== null) vertexBuffers.push(colorStream);
|
|
783
826
|
return {
|
|
784
|
-
vertexBuffers
|
|
785
|
-
streams === null
|
|
786
|
-
? [vertices, constants]
|
|
787
|
-
: [streams.positions, streams.normals, vertices, constants],
|
|
827
|
+
vertexBuffers,
|
|
788
828
|
vertexStride: parts.stride,
|
|
789
829
|
skinOffsets: parts.skinOffsets,
|
|
790
830
|
indexBuffer,
|
|
@@ -802,12 +842,14 @@ function gpuMeshOf(parts: {
|
|
|
802
842
|
hasChannel: parts.hasChannel,
|
|
803
843
|
morph: parts.morph,
|
|
804
844
|
update: parts.update,
|
|
845
|
+
updateColors: parts.updateColors,
|
|
805
846
|
motion,
|
|
806
847
|
dispose(): void {
|
|
807
848
|
motion?.previous?.destroy();
|
|
808
849
|
motion?.staging?.destroy();
|
|
809
850
|
streams?.positions.destroy();
|
|
810
851
|
streams?.normals.destroy();
|
|
852
|
+
colorStream?.destroy();
|
|
811
853
|
vertices.destroy();
|
|
812
854
|
constants.destroy();
|
|
813
855
|
indexBuffer.destroy();
|
|
@@ -866,6 +908,26 @@ function streamUpdate(
|
|
|
866
908
|
};
|
|
867
909
|
}
|
|
868
910
|
|
|
911
|
+
/**
|
|
912
|
+
* A `dynamicColors` mesh's `updateColors`: the caller's colours, written as they came into the
|
|
913
|
+
* buffer they have to themselves.
|
|
914
|
+
*/
|
|
915
|
+
function colorUpdate(
|
|
916
|
+
colorBuffer: GPUBuffer,
|
|
917
|
+
vertexCount: number,
|
|
918
|
+
): NonNullable<GpuMesh['updateColors']> {
|
|
919
|
+
return (target: GPUDevice, colors: Float32Array): void => {
|
|
920
|
+
if (colors.length !== vertexCount * 3) {
|
|
921
|
+
throw new Error(
|
|
922
|
+
`this mesh has ${vertexCount} vertices and the colours have ${colors.length / 3}. ` +
|
|
923
|
+
"A mesh's vertex count is fixed at creation: the index buffer, the pipeline and " +
|
|
924
|
+
'every other attribute are sized against it.',
|
|
925
|
+
);
|
|
926
|
+
}
|
|
927
|
+
target.queue.writeBuffer(colorBuffer, 0, colors);
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
|
|
869
931
|
/**
|
|
870
932
|
* The buffer layouts a pipeline is built with, matching what `createGpuMesh` uploaded.
|
|
871
933
|
*
|
|
@@ -919,6 +981,8 @@ export function vertexBufferLayouts(
|
|
|
919
981
|
/* A dynamic mesh's positions and normals: buffers of their own, ahead of the rest. */
|
|
920
982
|
const dynamic = present['dynamic'] === true;
|
|
921
983
|
const layouts: GPUVertexBufferLayout[] = [];
|
|
984
|
+
/* Attributes a caller rewrites, each a buffer of its own behind the constants: `vertexPacking.ts`. */
|
|
985
|
+
const trailing: GPUVertexBufferLayout[] = [];
|
|
922
986
|
VERTEX_LAYOUT.forEach((declared, index) => {
|
|
923
987
|
const attribute = laidOut(declared, layered);
|
|
924
988
|
if (dynamic && isStreamed(attribute)) {
|
|
@@ -945,6 +1009,15 @@ export function vertexBufferLayouts(
|
|
|
945
1009
|
*/
|
|
946
1010
|
/* One value every vertex shares sits with the absent ones: `vertexPacking.ts`. */
|
|
947
1011
|
const packing = packingIn(present, String(attribute.name));
|
|
1012
|
+
if (packing === 'stream') {
|
|
1013
|
+
trailing.push({
|
|
1014
|
+
arrayStride: attribute.components * 4,
|
|
1015
|
+
attributes: [
|
|
1016
|
+
{ shaderLocation: attribute.shaderLocation, offset: 0, format: attribute.format },
|
|
1017
|
+
],
|
|
1018
|
+
});
|
|
1019
|
+
return;
|
|
1020
|
+
}
|
|
948
1021
|
const supplied =
|
|
949
1022
|
(!attribute.optional || present[String(attribute.name)] === true) && packing !== 'constant';
|
|
950
1023
|
if (instanced && INSTANCE_LOCATIONS.has(attribute.shaderLocation)) {
|
|
@@ -987,6 +1060,7 @@ export function vertexBufferLayouts(
|
|
|
987
1060
|
* need a different layout from one that does not.
|
|
988
1061
|
*/
|
|
989
1062
|
layouts.push({ arrayStride: 0, attributes: constants });
|
|
1063
|
+
for (const layout of trailing) layouts.push(layout);
|
|
990
1064
|
/*
|
|
991
1065
|
* The four matrix columns and the tint — the five the flat shader's INSTANCED variant declares
|
|
992
1066
|
* and exactly the five left once the base mesh has spent eleven of WebGL2's guaranteed sixteen.
|
|
@@ -105,8 +105,8 @@ export class GpuClothParticles {
|
|
|
105
105
|
private latest = 0;
|
|
106
106
|
/** The frame each texture was last written for, so a motion knows whether the other is last frame's. */
|
|
107
107
|
private readonly written = [-1, -1];
|
|
108
|
-
|
|
109
|
-
private
|
|
108
|
+
/** Each binding's views with these particles, one for each texture: see `viewsWith`. */
|
|
109
|
+
private readonly paired = new WeakMap<GpuClothBinding, readonly [ClothViews, ClothViews]>();
|
|
110
110
|
|
|
111
111
|
constructor(device: GPUDevice, count: number, storage = false) {
|
|
112
112
|
this.count = count;
|
|
@@ -178,17 +178,20 @@ export class GpuClothParticles {
|
|
|
178
178
|
|
|
179
179
|
/**
|
|
180
180
|
* The three views a draw of `binding` with these particles binds, kept so a cached bind group
|
|
181
|
-
* compares equal frame after frame. Two objects, one for each texture that is current
|
|
181
|
+
* compares equal frame after frame. Two objects, one for each texture that is current, made the
|
|
182
|
+
* first time a binding is drawn with these particles and kept **for every binding**: several
|
|
183
|
+
* meshes follow one cloth, each with a binding of its own, and keeping only the last made two
|
|
184
|
+
* objects a draw as they took turns.
|
|
182
185
|
*/
|
|
183
186
|
viewsWith(binding: GpuClothBinding): ClothViews {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
binding: binding.bindingView,
|
|
188
|
-
particles: this.views[
|
|
189
|
-
|
|
190
|
-
|
|
187
|
+
let pair = this.paired.get(binding);
|
|
188
|
+
if (pair === undefined) {
|
|
189
|
+
pair = [
|
|
190
|
+
{ binding: binding.bindingView, particles: this.views[0], rest: binding.restView },
|
|
191
|
+
{ binding: binding.bindingView, particles: this.views[1], rest: binding.restView },
|
|
192
|
+
];
|
|
193
|
+
this.paired.set(binding, pair);
|
|
191
194
|
}
|
|
192
|
-
return
|
|
195
|
+
return pair[this.latest === 0 ? 0 : 1];
|
|
193
196
|
}
|
|
194
197
|
}
|
|
@@ -12,9 +12,9 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
|
-
* WebGPU's guaranteed `maxVertexBuffers`, so no verb can outgrow it. The most any binds is
|
|
16
|
-
* dynamic mesh's positions, normals, rows and
|
|
17
|
-
* this said three, then four, while text and bolts already bound four.
|
|
15
|
+
* WebGPU's guaranteed `maxVertexBuffers`, so no verb can outgrow it. The most any binds is six — a
|
|
16
|
+
* dynamic mesh's positions, normals, rows, constants and rewritten colours, and an instanced
|
|
17
|
+
* batch's placements — and this said three, then four, while text and bolts already bound four.
|
|
18
18
|
*/
|
|
19
19
|
const VERTEX_BUFFER_SLOTS = 8;
|
|
20
20
|
|
|
@@ -338,6 +338,7 @@ export class GpuClothSet {
|
|
|
338
338
|
u[base + 32] = garment.range.colliderBase;
|
|
339
339
|
u[base + 33] = garment.range.colliders;
|
|
340
340
|
u[base + 34] = garment.particles.width;
|
|
341
|
+
f[base + 35] = 1 / (1 + p.pull * h * h);
|
|
341
342
|
f[base + 36] = exactExp(p.damping * h);
|
|
342
343
|
f[base + 37] = exactExp(p.drag * h);
|
|
343
344
|
f[base + 38] = p.maxDistanceScale;
|
|
@@ -0,0 +1,74 @@
|
|
|
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
|
+
|
|
20
|
+
/** Groups a shelf keeps before it replaces the oldest. See the module comment for the count. */
|
|
21
|
+
export const SHELF_CAPACITY = 16;
|
|
22
|
+
|
|
23
|
+
/** One key's groups and what each was built with, four identities a group, flat. */
|
|
24
|
+
export class GroupShelf {
|
|
25
|
+
private readonly groups: (GPUBindGroup | null)[] = new Array<GPUBindGroup | null>(
|
|
26
|
+
SHELF_CAPACITY,
|
|
27
|
+
).fill(null);
|
|
28
|
+
private readonly made: (object | null)[] = new Array<object | null>(SHELF_CAPACITY * 4).fill(
|
|
29
|
+
null,
|
|
30
|
+
);
|
|
31
|
+
/** Where the next group goes: the oldest, once the shelf is full. */
|
|
32
|
+
private next = 0;
|
|
33
|
+
|
|
34
|
+
/** The group built with exactly these four, or null. */
|
|
35
|
+
find(
|
|
36
|
+
a: object | null,
|
|
37
|
+
b: object | null,
|
|
38
|
+
c: object | null,
|
|
39
|
+
d: object | null,
|
|
40
|
+
): GPUBindGroup | null {
|
|
41
|
+
for (let slot = 0; slot < SHELF_CAPACITY; slot++) {
|
|
42
|
+
const group = this.groups[slot] ?? null;
|
|
43
|
+
if (group === null) return null;
|
|
44
|
+
const at = slot * 4;
|
|
45
|
+
if (
|
|
46
|
+
this.made[at] === a &&
|
|
47
|
+
this.made[at + 1] === b &&
|
|
48
|
+
this.made[at + 2] === c &&
|
|
49
|
+
this.made[at + 3] === d
|
|
50
|
+
) {
|
|
51
|
+
return group;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Keep `group` as the one built with these four. */
|
|
58
|
+
keep(
|
|
59
|
+
group: GPUBindGroup,
|
|
60
|
+
a: object | null,
|
|
61
|
+
b: object | null,
|
|
62
|
+
c: object | null,
|
|
63
|
+
d: object | null,
|
|
64
|
+
): void {
|
|
65
|
+
const slot = this.next;
|
|
66
|
+
this.groups[slot] = group;
|
|
67
|
+
const at = slot * 4;
|
|
68
|
+
this.made[at] = a;
|
|
69
|
+
this.made[at + 1] = b;
|
|
70
|
+
this.made[at + 2] = c;
|
|
71
|
+
this.made[at + 3] = d;
|
|
72
|
+
this.next = (slot + 1) % SHELF_CAPACITY;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
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, type PointSkinData, type PointSkinReach } from '../../pointSkin.ts';
|
|
12
|
+
import type { GpuMesh } from './buffers.ts';
|
|
13
|
+
import { POINT_SKIN_WGSL, POINT_SKIN_WORKGROUP } from './shaders/pointSkin.wgsl.ts';
|
|
14
|
+
|
|
15
|
+
const STORAGE = 0x0080;
|
|
16
|
+
const UNIFORM = 0x0040;
|
|
17
|
+
const COPY_DST = 0x0008;
|
|
18
|
+
|
|
19
|
+
/** The pipeline every skin on a device poses with, made at the first skin. */
|
|
20
|
+
export function createPointSkinPipeline(device: GPUDevice): GPUComputePipeline {
|
|
21
|
+
return device.createComputePipeline({
|
|
22
|
+
label: 'pointSkin.pose',
|
|
23
|
+
layout: 'auto',
|
|
24
|
+
compute: {
|
|
25
|
+
module: device.createShaderModule({ label: 'pointSkin.pose', code: POINT_SKIN_WGSL }),
|
|
26
|
+
entryPoint: 'posePoints',
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export class GpuPointSkin {
|
|
32
|
+
readonly mesh: GpuMesh;
|
|
33
|
+
readonly reach: PointSkinReach;
|
|
34
|
+
private readonly count: number;
|
|
35
|
+
private readonly pipeline: GPUComputePipeline;
|
|
36
|
+
private readonly joints: GPUBuffer;
|
|
37
|
+
private readonly buffers: readonly GPUBuffer[];
|
|
38
|
+
private readonly group: GPUBindGroup;
|
|
39
|
+
|
|
40
|
+
constructor(device: GPUDevice, pipeline: GPUComputePipeline, mesh: GpuMesh, data: PointSkinData) {
|
|
41
|
+
this.mesh = mesh;
|
|
42
|
+
this.pipeline = pipeline;
|
|
43
|
+
this.reach = reachOf(data);
|
|
44
|
+
const count = data.weights.length / 4;
|
|
45
|
+
this.count = count;
|
|
46
|
+
|
|
47
|
+
/* Each influence a vec4: its point, and its weight in w. */
|
|
48
|
+
const points = new Float32Array(count * 16);
|
|
49
|
+
for (let i = 0; i < count * 4; i++) {
|
|
50
|
+
points.set(data.points.subarray(i * 3, i * 3 + 3), i * 4);
|
|
51
|
+
points[i * 4 + 3] = data.weights[i] as number;
|
|
52
|
+
}
|
|
53
|
+
const named = Uint32Array.from(data.joints);
|
|
54
|
+
const normals = new Float32Array(count * 4);
|
|
55
|
+
for (let v = 0; v < count; v++) normals.set(data.normals.subarray(v * 3, v * 3 + 3), v * 4);
|
|
56
|
+
const params = new Uint32Array([count, 0, 0, 0]);
|
|
57
|
+
|
|
58
|
+
const made = (
|
|
59
|
+
label: string,
|
|
60
|
+
contents: Float32Array | Uint32Array,
|
|
61
|
+
usage: number,
|
|
62
|
+
): GPUBuffer => {
|
|
63
|
+
const buffer = device.createBuffer({
|
|
64
|
+
label,
|
|
65
|
+
size: Math.max(16, contents.byteLength),
|
|
66
|
+
usage: usage | COPY_DST,
|
|
67
|
+
});
|
|
68
|
+
device.queue.writeBuffer(buffer, 0, contents);
|
|
69
|
+
return buffer;
|
|
70
|
+
};
|
|
71
|
+
this.joints = device.createBuffer({
|
|
72
|
+
label: 'pointSkin.joints',
|
|
73
|
+
size: Math.max(1, this.reach.joints) * 64,
|
|
74
|
+
usage: STORAGE | COPY_DST,
|
|
75
|
+
});
|
|
76
|
+
const buffers = [
|
|
77
|
+
made('pointSkin.params', params, UNIFORM),
|
|
78
|
+
made('pointSkin.points', points, STORAGE),
|
|
79
|
+
made('pointSkin.named', named, STORAGE),
|
|
80
|
+
made('pointSkin.normals', normals, STORAGE),
|
|
81
|
+
];
|
|
82
|
+
this.buffers = buffers;
|
|
83
|
+
const [positionsOut, normalsOut] = mesh.vertexBuffers as [GPUBuffer, GPUBuffer];
|
|
84
|
+
this.group = device.createBindGroup({
|
|
85
|
+
label: 'pointSkin.pose',
|
|
86
|
+
layout: pipeline.getBindGroupLayout(0),
|
|
87
|
+
entries: [
|
|
88
|
+
{ binding: 0, resource: { buffer: buffers[0] as GPUBuffer } },
|
|
89
|
+
{ binding: 1, resource: { buffer: this.joints } },
|
|
90
|
+
{ binding: 2, resource: { buffer: buffers[1] as GPUBuffer } },
|
|
91
|
+
{ binding: 3, resource: { buffer: buffers[2] as GPUBuffer } },
|
|
92
|
+
{ binding: 4, resource: { buffer: buffers[3] as GPUBuffer } },
|
|
93
|
+
{ binding: 5, resource: { buffer: positionsOut } },
|
|
94
|
+
{ binding: 6, resource: { buffer: normalsOut } },
|
|
95
|
+
],
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Write the pose's joints and record the dispatch that poses every vertex into `encoder`. */
|
|
100
|
+
pose(device: GPUDevice, encoder: GPUCommandEncoder, matrices: Float32Array): void {
|
|
101
|
+
if (this.reach.joints > 0) {
|
|
102
|
+
device.queue.writeBuffer(this.joints, 0, matrices, 0, this.reach.joints * 16);
|
|
103
|
+
}
|
|
104
|
+
const pass = encoder.beginComputePass({ label: 'pointSkin.pose' });
|
|
105
|
+
pass.setPipeline(this.pipeline);
|
|
106
|
+
pass.setBindGroup(0, this.group);
|
|
107
|
+
pass.dispatchWorkgroups(Math.ceil(this.count / POINT_SKIN_WORKGROUP));
|
|
108
|
+
pass.end();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
dispose(): void {
|
|
112
|
+
this.joints.destroy();
|
|
113
|
+
for (const buffer of this.buffers) buffer.destroy();
|
|
114
|
+
}
|
|
115
|
+
}
|