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