@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.
Files changed (57) hide show
  1. package/CHANGELOG.json +31 -0
  2. package/README.md +1 -1
  3. package/dist/index.d.ts +2 -1
  4. package/dist/render/backend/api.d.ts +63 -8
  5. package/dist/render/backend/webgl2/pointSkinFeedback.d.ts +23 -0
  6. package/dist/render/backend/webgl2/pointSkinFeedback.js +159 -0
  7. package/dist/render/backend/webgl2/renderer.d.ts +21 -0
  8. package/dist/render/backend/webgl2/renderer.js +41 -2
  9. package/dist/render/backend/webgpu/buffers.d.ts +10 -2
  10. package/dist/render/backend/webgpu/buffers.js +66 -16
  11. package/dist/render/backend/webgpu/clothTextures.d.ts +6 -3
  12. package/dist/render/backend/webgpu/clothTextures.js +14 -11
  13. package/dist/render/backend/webgpu/drawCommand.js +3 -3
  14. package/dist/render/backend/webgpu/gpuClothSet.js +1 -0
  15. package/dist/render/backend/webgpu/groupShelf.d.ts +31 -0
  16. package/dist/render/backend/webgpu/groupShelf.js +54 -0
  17. package/dist/render/backend/webgpu/pointSkinPass.d.ts +27 -0
  18. package/dist/render/backend/webgpu/pointSkinPass.js +104 -0
  19. package/dist/render/backend/webgpu/renderer.d.ts +31 -7
  20. package/dist/render/backend/webgpu/renderer.js +118 -47
  21. package/dist/render/backend/webgpu/shaders/clothSolve.wgsl.d.ts +1 -1
  22. package/dist/render/backend/webgpu/shaders/clothSolve.wgsl.js +7 -2
  23. package/dist/render/backend/webgpu/shaders/pointSkin.wgsl.d.ts +13 -0
  24. package/dist/render/backend/webgpu/shaders/pointSkin.wgsl.js +57 -0
  25. package/dist/render/backend/webgpu/vertexFormats.js +1 -1
  26. package/dist/render/lightBudget.d.ts +6 -0
  27. package/dist/render/lightBudget.js +6 -0
  28. package/dist/render/mesh.d.ts +19 -5
  29. package/dist/render/mesh.js +39 -8
  30. package/dist/render/pointSkin.d.ts +55 -0
  31. package/dist/render/pointSkin.js +121 -0
  32. package/dist/render/rendererHarness.js +6 -0
  33. package/dist/render/shader.d.ts +3 -1
  34. package/dist/render/shader.js +6 -1
  35. package/dist/render/vertexPacking.d.ts +16 -4
  36. package/dist/render/vertexPacking.js +10 -3
  37. package/package.json +6 -6
  38. package/src/index.ts +2 -0
  39. package/src/render/backend/api.ts +70 -7
  40. package/src/render/backend/webgl2/pointSkinFeedback.ts +173 -0
  41. package/src/render/backend/webgl2/renderer.ts +63 -2
  42. package/src/render/backend/webgpu/buffers.ts +90 -16
  43. package/src/render/backend/webgpu/clothTextures.ts +14 -11
  44. package/src/render/backend/webgpu/drawCommand.ts +3 -3
  45. package/src/render/backend/webgpu/gpuClothSet.ts +1 -0
  46. package/src/render/backend/webgpu/groupShelf.ts +74 -0
  47. package/src/render/backend/webgpu/pointSkinPass.ts +115 -0
  48. package/src/render/backend/webgpu/renderer.ts +150 -82
  49. package/src/render/backend/webgpu/shaders/clothSolve.wgsl.ts +7 -2
  50. package/src/render/backend/webgpu/shaders/pointSkin.wgsl.ts +58 -0
  51. package/src/render/backend/webgpu/vertexFormats.ts +1 -1
  52. package/src/render/lightBudget.ts +7 -0
  53. package/src/render/mesh.ts +49 -7
  54. package/src/render/pointSkin.ts +178 -0
  55. package/src/render/rendererHarness.ts +6 -0
  56. package/src/render/shader.ts +5 -0
  57. 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(this.gl, drawn, options.dynamic === true);
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(this.gl, drawn, options.dynamic === true);
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(device: GPUDevice, data: MeshData, dynamic = false): GpuMesh {
332
- const { mesh, upload } = createGpuMeshIncremental(device, data, dynamic);
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 own, and any attribute every vertex
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(UPLOAD_BYTES_PER_STEP / (stride + (dynamic ? 24 : 0))),
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, and a dynamic one its rows.
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
- private paired: GpuClothBinding | null = null;
109
- private pairedViews: ClothViews[] = [];
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
- if (this.paired !== binding) {
185
- this.paired = binding;
186
- this.pairedViews = [0, 1].map((k) => ({
187
- binding: binding.bindingView,
188
- particles: this.views[k] as GPUTextureView,
189
- rest: binding.restView,
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 this.pairedViews[this.latest] as ClothViews;
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 five — a
16
- * dynamic mesh's positions, normals, rows and constants, and an instanced batch's placements — 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
+ }