@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.
Files changed (78) hide show
  1. package/CHANGELOG.json +42 -0
  2. package/README.md +1 -1
  3. package/dist/index.d.ts +4 -1
  4. package/dist/index.js +1 -0
  5. package/dist/render/backend/api.d.ts +63 -8
  6. package/dist/render/backend/webgl2/pointSkinFeedback.d.ts +23 -0
  7. package/dist/render/backend/webgl2/pointSkinFeedback.js +159 -0
  8. package/dist/render/backend/webgl2/renderer.d.ts +33 -0
  9. package/dist/render/backend/webgl2/renderer.js +88 -4
  10. package/dist/render/backend/webgpu/buffers.d.ts +10 -2
  11. package/dist/render/backend/webgpu/buffers.js +66 -16
  12. package/dist/render/backend/webgpu/clothTextures.d.ts +6 -3
  13. package/dist/render/backend/webgpu/clothTextures.js +14 -11
  14. package/dist/render/backend/webgpu/drawCommand.js +3 -3
  15. package/dist/render/backend/webgpu/gpuClothSet.js +1 -0
  16. package/dist/render/backend/webgpu/groupShelf.d.ts +31 -0
  17. package/dist/render/backend/webgpu/groupShelf.js +54 -0
  18. package/dist/render/backend/webgpu/pointSkinPass.d.ts +27 -0
  19. package/dist/render/backend/webgpu/pointSkinPass.js +104 -0
  20. package/dist/render/backend/webgpu/renderer.d.ts +40 -7
  21. package/dist/render/backend/webgpu/renderer.js +151 -48
  22. package/dist/render/backend/webgpu/shaders/clothSolve.wgsl.d.ts +1 -1
  23. package/dist/render/backend/webgpu/shaders/clothSolve.wgsl.js +7 -2
  24. package/dist/render/backend/webgpu/shaders/pointSkin.wgsl.d.ts +13 -0
  25. package/dist/render/backend/webgpu/shaders/pointSkin.wgsl.js +57 -0
  26. package/dist/render/backend/webgpu/vertexFormats.js +1 -1
  27. package/dist/render/lightBudget.d.ts +6 -0
  28. package/dist/render/lightBudget.js +6 -0
  29. package/dist/render/mesh.d.ts +19 -5
  30. package/dist/render/mesh.js +39 -8
  31. package/dist/render/pointSkin.d.ts +55 -0
  32. package/dist/render/pointSkin.js +121 -0
  33. package/dist/render/rendererHarness.js +6 -0
  34. package/dist/render/shader.d.ts +3 -1
  35. package/dist/render/shader.js +6 -1
  36. package/dist/render/shaders/flat/detail.d.ts +21 -0
  37. package/dist/render/shaders/flat/detail.js +74 -0
  38. package/dist/render/shaders/flat/glassTint.d.ts +1 -1
  39. package/dist/render/shaders/flat/glassTint.js +1 -0
  40. package/dist/render/shaders/flat/index.d.ts +5 -0
  41. package/dist/render/shaders/flat/index.js +3 -0
  42. package/dist/render/shaders/flat/main.d.ts +1 -1
  43. package/dist/render/shaders/flat/main.js +2 -0
  44. package/dist/render/shaders/generated/flat.wgsl.d.ts +144 -16
  45. package/dist/render/shaders/generated/flat.wgsl.js +183 -55
  46. package/dist/render/surfaceDetail.d.ts +79 -0
  47. package/dist/render/surfaceDetail.js +86 -0
  48. package/dist/render/surfaceTexture.d.ts +9 -0
  49. package/dist/render/vertexPacking.d.ts +16 -4
  50. package/dist/render/vertexPacking.js +10 -3
  51. package/package.json +6 -6
  52. package/src/index.ts +4 -0
  53. package/src/render/backend/api.ts +70 -7
  54. package/src/render/backend/webgl2/pointSkinFeedback.ts +173 -0
  55. package/src/render/backend/webgl2/renderer.ts +121 -4
  56. package/src/render/backend/webgpu/buffers.ts +90 -16
  57. package/src/render/backend/webgpu/clothTextures.ts +14 -11
  58. package/src/render/backend/webgpu/drawCommand.ts +3 -3
  59. package/src/render/backend/webgpu/gpuClothSet.ts +1 -0
  60. package/src/render/backend/webgpu/groupShelf.ts +74 -0
  61. package/src/render/backend/webgpu/pointSkinPass.ts +115 -0
  62. package/src/render/backend/webgpu/renderer.ts +192 -83
  63. package/src/render/backend/webgpu/shaders/clothSolve.wgsl.ts +7 -2
  64. package/src/render/backend/webgpu/shaders/pointSkin.wgsl.ts +58 -0
  65. package/src/render/backend/webgpu/vertexFormats.ts +1 -1
  66. package/src/render/lightBudget.ts +7 -0
  67. package/src/render/mesh.ts +49 -7
  68. package/src/render/pointSkin.ts +178 -0
  69. package/src/render/rendererHarness.ts +6 -0
  70. package/src/render/shader.ts +5 -0
  71. package/src/render/shaders/flat/detail.ts +74 -0
  72. package/src/render/shaders/flat/glassTint.ts +3 -1
  73. package/src/render/shaders/flat/index.ts +8 -0
  74. package/src/render/shaders/flat/main.ts +2 -0
  75. package/src/render/shaders/generated/flat.wgsl.ts +183 -55
  76. package/src/render/surfaceDetail.ts +136 -0
  77. package/src/render/surfaceTexture.ts +9 -0
  78. package/src/render/vertexPacking.ts +25 -5
@@ -117,6 +117,15 @@ import {
117
117
  packSurfaceLayers,
118
118
  } from '../../surfaceLayers.ts';
119
119
  import type { SurfaceLayers } from '../../surfaceLayers.ts';
120
+ import {
121
+ DETAIL_FLOATS,
122
+ detailOf,
123
+ detailRefusal,
124
+ detailRefusalWarning,
125
+ packSurfaceDetail,
126
+ type DetailRefusal,
127
+ type SurfaceDetail,
128
+ } from '../../surfaceDetail.ts';
120
129
  import { LOOK_FLOATS, asksLayerLooks, packLayerLooks } from '../../surfaceLayerLooks.ts';
121
130
  import type { SurfaceProjection } from '../../surfaceProjection.ts';
122
131
  import { FILM_FRAG, FILM_VERT } from '../../shaders/film.ts';
@@ -377,6 +386,13 @@ import type { SurfaceTextureHandle } from '../api.ts';
377
386
  import { materialHasMaps, noteMapsWithoutUvs } from '../../mapsWithoutUvs.ts';
378
387
  import { VIEW_MODEL_DEPTH_SHARE, viewModelDepthRange } from '../../viewModel.ts';
379
388
  import { noLitSwitches, type LitSwitch } from '../../shaders/flat/glassTint.ts';
389
+ import {
390
+ poseBounds,
391
+ validatePointSkin,
392
+ validatePose,
393
+ type PointSkinData,
394
+ } from '../../pointSkin.ts';
395
+ import { GlPointSkin, createPointSkinPose, type PointSkinPose } from './pointSkinFeedback.ts';
380
396
 
381
397
  /**
382
398
  * How many frames `firstFrameSettled` will ask before it stops asking.
@@ -1794,6 +1810,13 @@ export class WebGL2Renderer implements RendererApi {
1794
1810
  /** Whether it has room for the layers' looks beside them, and whether the refusal was said. */
1795
1811
  private looksRoom: boolean | null = null;
1796
1812
  private warnedLooksRoom = false;
1813
+ /** `uDetail`, the material's tiled detail packed by `packSurfaceDetail`. */
1814
+ private readonly detailFloats = new Float32Array(DETAIL_FLOATS);
1815
+ /** Whether the lit stage has room for detail on this part, asked once. See `detailOfMaterial`. */
1816
+ private detailRoom: boolean | null = null;
1817
+ private warnedDetailRoom = false;
1818
+ /** The refusals already said, each once. */
1819
+ private readonly warnedDetail = new Set<DetailRefusal>();
1797
1820
  private warnedLayersModelMap = false;
1798
1821
  private warnedLayersTriplanar = false;
1799
1822
  private warnedOverlayRoom = false;
@@ -3624,7 +3647,13 @@ export class WebGL2Renderer implements RendererApi {
3624
3647
  */
3625
3648
  const drawn = withLightmapUvs(data);
3626
3649
  this.warmVariants(drawn);
3627
- return new Mesh(this.gl, drawn, options.dynamic === true);
3650
+ return new Mesh(
3651
+ this.gl,
3652
+ drawn,
3653
+ options.dynamic === true,
3654
+ false,
3655
+ options.dynamicColors === true,
3656
+ );
3628
3657
  }
3629
3658
 
3630
3659
  /**
@@ -3639,7 +3668,12 @@ export class WebGL2Renderer implements RendererApi {
3639
3668
  createMeshIncremental(data: MeshData, options: MeshOptions = {}): IncrementalMesh {
3640
3669
  const drawn = withLightmapUvs(data);
3641
3670
  this.warmVariants(drawn);
3642
- const { mesh, upload } = createMeshIncremental(this.gl, drawn, options.dynamic === true);
3671
+ const { mesh, upload } = createMeshIncremental(
3672
+ this.gl,
3673
+ drawn,
3674
+ options.dynamic === true,
3675
+ options.dynamicColors === true,
3676
+ );
3643
3677
  return {
3644
3678
  mesh,
3645
3679
  upload: {
@@ -3692,6 +3726,49 @@ export class WebGL2Renderer implements RendererApi {
3692
3726
  mesh.update(this.gl, positions, normals);
3693
3727
  }
3694
3728
 
3729
+ /** The program and feedback object every point skin poses with, made at the first one. */
3730
+ private pointSkinPose: PointSkinPose | null = null;
3731
+
3732
+ /**
3733
+ * A dynamic mesh posed on the device from weighted points in its joints' spaces, by transform
3734
+ * feedback. Refused in words on a mesh that is not dynamic, whose buffers are not hinted for a
3735
+ * rewrite every frame.
3736
+ */
3737
+ createPointSkin(mesh: Mesh, data: PointSkinData): GlPointSkin {
3738
+ if (mesh.streams === null) {
3739
+ throw new Error(
3740
+ 'a point skin poses a mesh created with { dynamic: true }, whose positions and normals ' +
3741
+ 'are rewritten every frame. Say so at `createMesh`.',
3742
+ );
3743
+ }
3744
+ validatePointSkin(data, mesh.vertexCount);
3745
+ this.pointSkinPose ??= createPointSkinPose(this.gl);
3746
+ return new GlPointSkin(this.gl, mesh, data);
3747
+ }
3748
+
3749
+ /**
3750
+ * Pose a skin now: `matrices` holds each joint's transform into the mesh's model space, sixteen
3751
+ * floats, column-major. A command in the stream, so the draws after it read this pose.
3752
+ */
3753
+ posePointSkin(skin: GlPointSkin, matrices: Float32Array): void {
3754
+ validatePose(matrices, skin.reach);
3755
+ if (this.contextLost || this.pointSkinPose === null) return;
3756
+ poseBounds(matrices, skin.reach, skin.mesh.bounds);
3757
+ skin.pose(this.gl, this.pointSkinPose, matrices);
3758
+ }
3759
+
3760
+ disposePointSkin(skin: GlPointSkin): void {
3761
+ skin.dispose(this.gl);
3762
+ }
3763
+
3764
+ /**
3765
+ * Rewrite a mesh's colours, on a mesh made `dynamicColors`. The VAO is left alone for the reason
3766
+ * `updateMesh` gives: the buffer is the same buffer.
3767
+ */
3768
+ updateMeshColors(mesh: Mesh, colors: Float32Array): void {
3769
+ mesh.updateColors(this.gl, colors);
3770
+ }
3771
+
3695
3772
  /**
3696
3773
  * Compile the skinned flat program, once, on the first rigged mesh.
3697
3774
  *
@@ -4401,10 +4478,16 @@ export class WebGL2Renderer implements RendererApi {
4401
4478
  const modelMap = material?.modelMap ?? null;
4402
4479
  /* A layered material's mask, where it is a map, goes where a model's would: surfaceLayers.ts. */
4403
4480
  const layers = this.layersOf(material);
4404
- const boundMap = modelMap ?? layerMaskMap(layers);
4481
+ const taken = modelMap ?? layerMaskMap(layers);
4482
+ /* And its detail's array, where nothing else is read there: surfaceDetail.ts. */
4483
+ const detail = this.detailOfMaterial(material, taken);
4484
+ const boundMap = taken ?? detail?.maps ?? null;
4405
4485
  gl.activeTexture(gl.TEXTURE0 + MODEL_MAP_TEXTURE_UNIT);
4406
4486
  gl.bindTexture(gl.TEXTURE_2D_ARRAY, this.emptyTexture2DArray);
4407
4487
  if (boundMap !== null) boundMap.bind(gl, MODEL_MAP_TEXTURE_UNIT);
4488
+ packSurfaceDetail(detail, (material?.orm ?? null) !== null, this.detailFloats);
4489
+ if (detail !== null) this.askLit('SURFACE_DETAIL');
4490
+ gl.uniform4fv(u['uDetail'] ?? null, this.detailFloats);
4408
4491
  packSurfaceLayers(layers, this.layerFloats);
4409
4492
  if (layers !== null) this.askLit('LAYERED');
4410
4493
  gl.uniform4fv(u['uLayers'] ?? null, this.layerFloats);
@@ -4710,6 +4793,37 @@ export class WebGL2Renderer implements RendererApi {
4710
4793
  * as they are, said once — which on a material sharing its arrays reads the wrong textures, so
4711
4794
  * the sentence names the way out.
4712
4795
  */
4796
+ /**
4797
+ * A material's detail, or null where it is refused: its slot holds another array, it is placed by
4798
+ * the world, or the lit stage has no room for its thirty-three vectors. Each said once.
4799
+ */
4800
+ private detailOfMaterial(
4801
+ material: SurfaceMaterial | null,
4802
+ taken: SurfaceTexture | null,
4803
+ ): SurfaceDetail<SurfaceTexture> | null {
4804
+ const detail = detailOf(material) as SurfaceDetail<SurfaceTexture> | null;
4805
+ if (detail === null) return null;
4806
+ const refusal = detailRefusal(detail, taken, (material?.projection ?? null) !== null);
4807
+ if (refusal !== null) {
4808
+ if (!this.warnedDetail.has(refusal)) {
4809
+ this.warnedDetail.add(refusal);
4810
+ console.warn(detailRefusalWarning(refusal));
4811
+ }
4812
+ return null;
4813
+ }
4814
+ this.detailRoom ??= this.switchFits('SURFACE_DETAIL');
4815
+ if (this.detailRoom) return detail;
4816
+ if (!this.warnedDetailRoom) {
4817
+ this.warnedDetailRoom = true;
4818
+ console.warn(
4819
+ "Renderer: a material's detail is refused on this device. It takes thirty-three fragment " +
4820
+ 'uniform vectors the lit shader does not have, so the material is drawn without it. A ' +
4821
+ 'lower `maxLights` makes the room, or WebGPU has it.',
4822
+ );
4823
+ }
4824
+ return null;
4825
+ }
4826
+
4713
4827
  private looksFit(layers: SurfaceLayers<SurfaceTexture> | null): boolean {
4714
4828
  if (!asksLayerLooks(layers)) return false;
4715
4829
  this.looksRoom ??= this.switchFits('LAYER_LOOKS', 'LAYERED');
@@ -7060,6 +7174,7 @@ export class WebGL2Renderer implements RendererApi {
7060
7174
  movingSun: asIf.MOVING_SUN ?? this.litOn.MOVING_SUN,
7061
7175
  layered: asIf.LAYERED ?? this.litOn.LAYERED,
7062
7176
  layerLooks: asIf.LAYER_LOOKS ?? this.litOn.LAYER_LOOKS,
7177
+ surfaceDetail: asIf.SURFACE_DETAIL ?? this.litOn.SURFACE_DETAIL,
7063
7178
  maxLights: budget.maxLights,
7064
7179
  maxAreaLights: budget.maxAreaLights,
7065
7180
  });
@@ -9226,9 +9341,10 @@ export class WebGL2Renderer implements RendererApi {
9226
9341
  this.projection.fill(0);
9227
9342
  /* Nor light through from behind. See `SurfaceMaterial.diffuseTransmission`. */
9228
9343
  resolveTransmission(null, this.transmission);
9229
- /* Nor layers: one layer until a material asks for more. */
9344
+ /* Nor layers: one layer until a material asks for more. Nor detail. */
9230
9345
  this.layerFloats.fill(0);
9231
9346
  this.lookFloats.fill(0);
9347
+ this.detailFloats.fill(0);
9232
9348
  this.overlayMaps = null;
9233
9349
  this.overlayTexture = null;
9234
9350
  /* Nor an ambient of its own: the frame's, until a draw asks. See `setAmbientSH`. */
@@ -9404,6 +9520,7 @@ export class WebGL2Renderer implements RendererApi {
9404
9520
  gl.uniform4fv(u['uWorldUv'] ?? null, this.projection);
9405
9521
  gl.uniform4fv(u['uLayers'] ?? null, this.layerFloats);
9406
9522
  gl.uniform4fv(u['uLayerLooks'] ?? null, this.lookFloats);
9523
+ gl.uniform4fv(u['uDetail'] ?? null, this.detailFloats);
9407
9524
  /* Neither refracting nor glass, which is every draw that does not say otherwise. See
9408
9525
  `bindSeeThrough` for the four numbers in the one vector. */
9409
9526
  gl.uniform4f(u['uSeeThrough'] ?? null, 0, 0, 0, 0);
@@ -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
+ }