@driftengine/core 4.10.0 → 4.11.0

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 (140) hide show
  1. package/CHANGELOG.json +71 -0
  2. package/README.md +1 -1
  3. package/dist/index.d.ts +4 -3
  4. package/dist/index.js +1 -1
  5. package/dist/render/backend/webgl2/renderer.d.ts +99 -2
  6. package/dist/render/backend/webgl2/renderer.js +361 -42
  7. package/dist/render/backend/webgpu/buffers.d.ts +5 -0
  8. package/dist/render/backend/webgpu/buffers.js +98 -48
  9. package/dist/render/backend/webgpu/clothKernels.d.ts +8 -0
  10. package/dist/render/backend/webgpu/clothKernels.js +76 -0
  11. package/dist/render/backend/webgpu/clothOps.d.ts +39 -0
  12. package/dist/render/backend/webgpu/clothOps.js +78 -0
  13. package/dist/render/backend/webgpu/clothSetLayout.d.ts +25 -0
  14. package/dist/render/backend/webgpu/clothSetLayout.js +84 -0
  15. package/dist/render/backend/webgpu/clothSetPack.d.ts +33 -0
  16. package/dist/render/backend/webgpu/clothSetPack.js +144 -0
  17. package/dist/render/backend/webgpu/drawCommand.d.ts +2 -0
  18. package/dist/render/backend/webgpu/drawCommand.js +1 -0
  19. package/dist/render/backend/webgpu/flatPass.d.ts +1 -1
  20. package/dist/render/backend/webgpu/flatPass.js +2 -1
  21. package/dist/render/backend/webgpu/gpuClothSet.d.ts +80 -0
  22. package/dist/render/backend/webgpu/gpuClothSet.js +462 -0
  23. package/dist/render/backend/webgpu/latePass.js +1 -0
  24. package/dist/render/backend/webgpu/motionPass.js +15 -6
  25. package/dist/render/backend/webgpu/pipelineCache.d.ts +22 -1
  26. package/dist/render/backend/webgpu/pipelineCache.js +39 -1
  27. package/dist/render/backend/webgpu/postPass.d.ts +1 -1
  28. package/dist/render/backend/webgpu/renderer.d.ts +48 -0
  29. package/dist/render/backend/webgpu/renderer.js +207 -27
  30. package/dist/render/backend/webgpu/shaders/clothSolve.wgsl.d.ts +9 -2
  31. package/dist/render/backend/webgpu/shaders/clothSolve.wgsl.js +121 -47
  32. package/dist/render/backend/webgpu/staticBundles.js +2 -1
  33. package/dist/render/backend/webgpu/vertexFormats.d.ts +22 -0
  34. package/dist/render/backend/webgpu/vertexFormats.js +97 -0
  35. package/dist/render/globalMediumPass.d.ts +2 -0
  36. package/dist/render/globalMediumPass.js +1 -0
  37. package/dist/render/indexWidth.d.ts +17 -0
  38. package/dist/render/indexWidth.js +23 -0
  39. package/dist/render/lightMatrix.d.ts +11 -0
  40. package/dist/render/lightMatrix.js +14 -0
  41. package/dist/render/mesh.d.ts +9 -0
  42. package/dist/render/mesh.js +38 -14
  43. package/dist/render/renderQuality.d.ts +30 -0
  44. package/dist/render/renderQuality.js +5 -0
  45. package/dist/render/rendererHarness.d.ts +7 -0
  46. package/dist/render/rendererHarness.js +12 -0
  47. package/dist/render/shader.d.ts +15 -0
  48. package/dist/render/shader.js +41 -0
  49. package/dist/render/shaders/flat/directionalShadow.js +84 -10
  50. package/dist/render/shaders/flat/glassTint.d.ts +1 -1
  51. package/dist/render/shaders/flat/glassTint.js +3 -0
  52. package/dist/render/shaders/flat/index.d.ts +15 -0
  53. package/dist/render/shaders/flat/index.js +15 -2
  54. package/dist/render/shaders/flat/layered.d.ts +19 -0
  55. package/dist/render/shaders/flat/layered.js +115 -0
  56. package/dist/render/shaders/flat/main.d.ts +1 -1
  57. package/dist/render/shaders/flat/main.js +43 -16
  58. package/dist/render/shaders/flat/mapReads.d.ts +10 -0
  59. package/dist/render/shaders/flat/mapReads.js +38 -0
  60. package/dist/render/shaders/flat/movingSun.d.ts +21 -0
  61. package/dist/render/shaders/flat/movingSun.js +46 -0
  62. package/dist/render/shaders/flat/normalMap.d.ts +6 -2
  63. package/dist/render/shaders/flat/normalMap.js +14 -3
  64. package/dist/render/shaders/flat/overlay.d.ts +3 -2
  65. package/dist/render/shaders/flat/overlay.js +41 -19
  66. package/dist/render/shaders/flat/preamble.js +23 -5
  67. package/dist/render/shaders/flat/worldUv.d.ts +22 -0
  68. package/dist/render/shaders/flat/worldUv.js +113 -0
  69. package/dist/render/shaders/generated/flat.wgsl.d.ts +768 -368
  70. package/dist/render/shaders/generated/flat.wgsl.js +791 -391
  71. package/dist/render/shaders/generated/globalMedium.wgsl.d.ts +8 -3
  72. package/dist/render/shaders/generated/globalMedium.wgsl.js +8 -3
  73. package/dist/render/shaders/generated/lightVolume.wgsl.d.ts +15 -10
  74. package/dist/render/shaders/generated/lightVolume.wgsl.js +16 -11
  75. package/dist/render/shaders/globalMedium.js +9 -1
  76. package/dist/render/shaders/lightVolume.js +9 -1
  77. package/dist/render/shaders/tangentFrame.d.ts +1 -1
  78. package/dist/render/shaders/tangentFrame.js +16 -3
  79. package/dist/render/skinnedClothRun.d.ts +28 -0
  80. package/dist/render/skinnedClothRun.js +103 -35
  81. package/dist/render/surfaceLayers.d.ts +39 -0
  82. package/dist/render/surfaceLayers.js +26 -0
  83. package/dist/render/surfaceOverlay.d.ts +16 -3
  84. package/dist/render/surfaceOverlay.js +11 -6
  85. package/dist/render/surfaceProjection.d.ts +34 -0
  86. package/dist/render/surfaceProjection.js +30 -0
  87. package/dist/render/surfaceTexture.d.ts +30 -0
  88. package/dist/render/vertexPacking.d.ts +48 -0
  89. package/dist/render/vertexPacking.js +68 -0
  90. package/package.json +6 -6
  91. package/src/index.ts +4 -2
  92. package/src/render/backend/webgl2/renderer.ts +408 -55
  93. package/src/render/backend/webgpu/buffers.ts +153 -54
  94. package/src/render/backend/webgpu/clothKernels.ts +86 -0
  95. package/src/render/backend/webgpu/clothOps.ts +86 -0
  96. package/src/render/backend/webgpu/clothSetLayout.ts +102 -0
  97. package/src/render/backend/webgpu/clothSetPack.ts +197 -0
  98. package/src/render/backend/webgpu/drawCommand.ts +3 -0
  99. package/src/render/backend/webgpu/flatPass.ts +26 -28
  100. package/src/render/backend/webgpu/gpuClothSet.ts +564 -0
  101. package/src/render/backend/webgpu/latePass.ts +1 -0
  102. package/src/render/backend/webgpu/motionPass.ts +18 -7
  103. package/src/render/backend/webgpu/pipelineCache.ts +46 -7
  104. package/src/render/backend/webgpu/renderer.ts +274 -48
  105. package/src/render/backend/webgpu/shaders/clothSolve.wgsl.ts +121 -47
  106. package/src/render/backend/webgpu/staticBundles.ts +2 -1
  107. package/src/render/backend/webgpu/vertexFormats.ts +113 -0
  108. package/src/render/globalMediumPass.ts +7 -0
  109. package/src/render/indexWidth.ts +27 -0
  110. package/src/render/lightMatrix.ts +15 -0
  111. package/src/render/mesh.ts +66 -14
  112. package/src/render/renderQuality.ts +38 -0
  113. package/src/render/rendererHarness.ts +15 -0
  114. package/src/render/shader.ts +59 -0
  115. package/src/render/shaders/flat/directionalShadow.ts +84 -10
  116. package/src/render/shaders/flat/glassTint.ts +7 -1
  117. package/src/render/shaders/flat/index.ts +30 -2
  118. package/src/render/shaders/flat/layered.ts +115 -0
  119. package/src/render/shaders/flat/main.ts +43 -16
  120. package/src/render/shaders/flat/mapReads.ts +38 -0
  121. package/src/render/shaders/flat/movingSun.ts +46 -0
  122. package/src/render/shaders/flat/normalMap.ts +14 -3
  123. package/src/render/shaders/flat/overlay.ts +41 -19
  124. package/src/render/shaders/flat/preamble.ts +23 -5
  125. package/src/render/shaders/flat/worldUv.ts +113 -0
  126. package/src/render/shaders/generated/flat.wgsl.ts +791 -391
  127. package/src/render/shaders/generated/globalMedium.wgsl.ts +8 -3
  128. package/src/render/shaders/generated/lightVolume.wgsl.ts +16 -11
  129. package/src/render/shaders/globalMedium.ts +9 -1
  130. package/src/render/shaders/lightVolume.ts +9 -1
  131. package/src/render/shaders/tangentFrame.ts +16 -3
  132. package/src/render/skinnedClothRun.ts +138 -38
  133. package/src/render/surfaceLayers.ts +57 -0
  134. package/src/render/surfaceOverlay.ts +24 -7
  135. package/src/render/surfaceProjection.ts +57 -0
  136. package/src/render/surfaceTexture.ts +30 -0
  137. package/src/render/vertexPacking.ts +109 -0
  138. package/dist/render/backend/webgpu/gpuSkinnedCloth.d.ts +0 -74
  139. package/dist/render/backend/webgpu/gpuSkinnedCloth.js +0 -452
  140. package/src/render/backend/webgpu/gpuSkinnedCloth.ts +0 -502
@@ -4,6 +4,9 @@ import { validateMeshData } from '../../mesh.js';
4
4
  import { ABSENT_ATTRIBUTE } from '../../vertexDefaults.js';
5
5
  import { UPLOAD_BYTES_PER_STEP } from '../../uploadStep.js';
6
6
  import { INSTANCE_STRIDE } from '../../instances.js';
7
+ import { compactIndices } from '../../indexWidth.js';
8
+ import { SNORM16_ONE, UNORM16_ONE, packingFor, sharedValue, vertexPackingOf, } from '../../vertexPacking.js';
9
+ import { formatBytes, interleaveFixed, packedFormat, packingIn } from './vertexFormats.js';
7
10
  /**
8
11
  * Mesh geometry as WebGPU buffers.
9
12
  *
@@ -160,13 +163,33 @@ export function createGpuMeshIncremental(device, data, dynamic = false) {
160
163
  const bounds = boundsOfPositions(data.positions, createBounds());
161
164
  const vertexCount = data.positions.length / 3;
162
165
  const layered = data.layers !== undefined;
163
- const supplied = VERTEX_LAYOUT.filter((attribute) => (!attribute.optional || data[attribute.name] !== undefined) &&
164
- !(dynamic && isStreamed(attribute))).map((attribute) => laidOut(attribute, layered));
166
+ /* How each attribute travels, which the pipeline key says too: `vertexPacking.ts`. */
167
+ const packing = vertexPackingOf(data, dynamic);
165
168
  /* Interleaved: one stride holding every attribute the mesh actually supplied — less a dynamic
166
- mesh's positions and normals, which are buffers of their own. */
167
- const stride = supplied.reduce((sum, attribute) => sum + attribute.components * 4, 0);
168
- const step = stride / 4;
169
- const interleaved = new Float32Array(step * vertexCount);
169
+ mesh's positions and normals, which are buffers of their own, and any attribute every vertex
170
+ shares, which is a constant. */
171
+ const fields = [];
172
+ let stride = 0;
173
+ for (const declared of VERTEX_LAYOUT) {
174
+ if (declared.optional && data[declared.name] === undefined)
175
+ continue;
176
+ if (dynamic && isStreamed(declared))
177
+ continue;
178
+ const attribute = laidOut(declared, layered);
179
+ const how = packingFor(packing, attribute.name);
180
+ if (how === 'constant')
181
+ continue;
182
+ const format = packedFormat(attribute.format, how);
183
+ fields.push({ attribute, packing: how, format, offset: stride });
184
+ stride += formatBytes(format);
185
+ }
186
+ const buffer = new ArrayBuffer(stride * vertexCount);
187
+ const rows = {
188
+ floats: new Float32Array(buffer),
189
+ signed: new Int16Array(buffer),
190
+ unsigned: new Uint16Array(buffer),
191
+ bytes: new Uint8Array(buffer),
192
+ };
170
193
  /*
171
194
  * **Refused here, in one sentence, rather than by the driver a hundred times.**
172
195
  *
@@ -181,7 +204,7 @@ export function createGpuMeshIncremental(device, data, dynamic = false) {
181
204
  * whose adapter caps at the default, and it deserves a sentence naming the model's size and
182
205
  * the device's rather than a wall of validation.
183
206
  */
184
- const byteLength = align4(interleaved.byteLength);
207
+ const byteLength = align4(buffer.byteLength);
185
208
  const ceiling = device.limits.maxBufferSize;
186
209
  if (typeof ceiling === 'number' && byteLength > ceiling) {
187
210
  throw new Error(`this mesh needs a ${Math.round(byteLength / 1024 / 1024)} MB vertex buffer and this GPU ` +
@@ -216,10 +239,20 @@ export function createGpuMeshIncremental(device, data, dynamic = false) {
216
239
  continue;
217
240
  absent.set(value, slot * (CONSTANT_SLOT / 4));
218
241
  }
242
+ /* And what every vertex shares, where a mesh's attribute is one value: `vertexPacking.ts`. */
243
+ for (let slot = 0; slot < VERTEX_LAYOUT.length; slot++) {
244
+ const attribute = VERTEX_LAYOUT[slot];
245
+ if (packingFor(packing, attribute.name) !== 'constant')
246
+ continue;
247
+ const values = data[attribute.name];
248
+ absent.set(sharedValue(values, attribute.components), slot * (CONSTANT_SLOT / 4));
249
+ }
219
250
  device.queue.writeBuffer(constants, 0, absent);
251
+ /* Sixteen bits where every vertex fits, which every draw of this mesh says: indexWidth.ts. */
252
+ const indices = compactIndices(data.indices, vertexCount);
220
253
  const indexBuffer = device.createBuffer({
221
254
  label: 'mesh.indices',
222
- size: align4(data.indices.byteLength),
255
+ size: align4(indices.byteLength),
223
256
  usage: USAGE.INDEX | USAGE.COPY_DST,
224
257
  });
225
258
  /* A dynamic mesh's two moving streams: copied from as well as written, for last frame's positions. */
@@ -243,10 +276,11 @@ export function createGpuMeshIncremental(device, data, dynamic = false) {
243
276
  * buffer never holds a torn one even for the frame between two steps.
244
277
  */
245
278
  const verticesPerStep = Math.max(1, Math.floor(UPLOAD_BYTES_PER_STEP / (stride + (dynamic ? 24 : 0))));
246
- const indexWidth = data.indices.BYTES_PER_ELEMENT;
247
- const indicesPerStep = Math.max(1, Math.floor(UPLOAD_BYTES_PER_STEP / indexWidth));
279
+ /* Whole four-byte words a step, which is all a queue writes: two sixteen-bit indices to one. */
280
+ const indexWidth = indices.BYTES_PER_ELEMENT;
281
+ const indicesPerStep = Math.max(1, Math.floor(UPLOAD_BYTES_PER_STEP / 4)) * (4 / indexWidth);
248
282
  const totalSteps = Math.max(1, Math.ceil(vertexCount / verticesPerStep)) +
249
- Math.max(1, Math.ceil(data.indices.length / indicesPerStep));
283
+ Math.max(1, Math.ceil(indices.length / indicesPerStep));
250
284
  /*
251
285
  * **The mesh is assembled out here, by functions of its own, and that is the fix for a copy
252
286
  * nobody named.** It was an object literal in this scope, and its `complete` getter shared this
@@ -262,6 +296,7 @@ export function createGpuMeshIncremental(device, data, dynamic = false) {
262
296
  constants,
263
297
  streams,
264
298
  indexBuffer,
299
+ indexFormat: indices instanceof Uint16Array ? 'uint16' : 'uint32',
265
300
  stride: streams === null ? stride : 12,
266
301
  indexCount: data.indices.length,
267
302
  bounds,
@@ -271,7 +306,7 @@ export function createGpuMeshIncremental(device, data, dynamic = false) {
271
306
  isSkinnedEight: data.joints !== undefined && data.joints2 !== undefined,
272
307
  vertexCount,
273
308
  hasUvs: data.uvs !== undefined,
274
- skinOffsets: streams === null ? skinOffsetsOf(supplied) : null,
309
+ skinOffsets: streams === null ? skinOffsetsOf(fields) : null,
275
310
  hasChannel: data.channel !== undefined,
276
311
  morph: data.morphTargets === undefined || data.morphTargetCount === undefined
277
312
  ? null
@@ -282,14 +317,14 @@ export function createGpuMeshIncremental(device, data, dynamic = false) {
282
317
  });
283
318
  const job = {
284
319
  data,
285
- interleaved,
320
+ indices,
321
+ rows,
286
322
  device,
287
- supplied,
323
+ fields,
288
324
  streams,
289
325
  vertices,
290
326
  indexBuffer,
291
327
  vertexCount,
292
- step,
293
328
  stride,
294
329
  verticesPerStep,
295
330
  indicesPerStep,
@@ -299,26 +334,35 @@ export function createGpuMeshIncremental(device, data, dynamic = false) {
299
334
  return { mesh, upload: uploadSteps(job) };
300
335
  }
301
336
  function* uploadSteps(job) {
302
- const { device, supplied, vertices, indexBuffer, vertexCount, step, stride } = job;
337
+ const { device, fields, vertices, indexBuffer, vertexCount, stride } = job;
338
+ /* Floats and halves a vertex: every field is whole words, so both divide. */
339
+ const step = stride / 4;
340
+ const halves = stride / 2;
303
341
  let taken = 0;
304
342
  for (let from = 0; from < vertexCount; from += job.verticesPerStep) {
305
343
  const data = job.data;
306
- const interleaved = job.interleaved;
344
+ const rows = job.rows;
307
345
  const to = Math.min(from + job.verticesPerStep, vertexCount);
308
- let fieldOffset = 0;
309
- for (const attribute of supplied) {
346
+ for (const field of fields) {
347
+ const attribute = field.attribute;
310
348
  const source = data[attribute.name];
311
- if (attribute === LAYERED_UVS) {
349
+ if (field.packing === 'snorm16') {
350
+ interleaveFixed(rows.signed, source, attribute.components, halves, field.offset / 2, from, to, SNORM16_ONE);
351
+ }
352
+ else if (field.packing === 'unorm16') {
353
+ interleaveFixed(rows.unsigned, source, attribute.components, halves, field.offset / 2, from, to, UNORM16_ONE);
354
+ }
355
+ else if (attribute === LAYERED_UVS) {
312
356
  /* (u, v) from the coordinates and the layer after them, one field of three. */
313
- interleaveField(interleaved, source, 2, step, fieldOffset, from, to);
314
- interleaveField(interleaved, data.layers, 1, step, fieldOffset + 2, from, to);
357
+ const at = field.offset / 4;
358
+ interleaveField(rows.floats, source, 2, step, at, from, to);
359
+ interleaveField(rows.floats, data.layers, 1, step, at + 2, from, to);
315
360
  }
316
361
  else {
317
- interleaveField(interleaved, source, attribute.components, step, fieldOffset, from, to);
362
+ interleaveField(rows.floats, source, attribute.components, step, field.offset / 4, from, to);
318
363
  }
319
- fieldOffset += attribute.components;
320
364
  }
321
- device.queue.writeBuffer(vertices, from * stride, interleaved, from * step, (to - from) * step);
365
+ device.queue.writeBuffer(vertices, from * stride, rows.bytes, from * stride, (to - from) * stride);
322
366
  if (job.streams !== null) {
323
367
  const count = (to - from) * 3;
324
368
  device.queue.writeBuffer(job.streams.positions, from * 12, data.positions, from * 3, count);
@@ -328,7 +372,7 @@ function* uploadSteps(job) {
328
372
  if (taken < job.totalSteps)
329
373
  yield;
330
374
  }
331
- const indices = job.data.indices;
375
+ const indices = job.indices;
332
376
  const indexWidth = indices.BYTES_PER_ELEMENT;
333
377
  for (let from = 0; from < indices.length; from += job.indicesPerStep) {
334
378
  const to = Math.min(from + job.indicesPerStep, indices.length);
@@ -338,7 +382,8 @@ function* uploadSteps(job) {
338
382
  yield;
339
383
  }
340
384
  job.data = null;
341
- job.interleaved = null;
385
+ job.indices = null;
386
+ job.rows = null;
342
387
  job.progress.uploaded = true;
343
388
  }
344
389
  /**
@@ -383,28 +428,33 @@ function interleaveField(rows, source, width, step, fieldOffset, from, to) {
383
428
  * Where the joints and weights sit in a vertex of `supplied`, the attributes a mesh interleaves in
384
429
  * layout order; null unless it supplied both.
385
430
  */
386
- function skinOffsetsOf(supplied) {
387
- let floats = 0;
431
+ function skinOffsetsOf(fields) {
388
432
  let joints = -1;
389
433
  let weights = -1;
390
434
  let joints2 = -1;
391
435
  let weights2 = -1;
392
- for (const attribute of supplied) {
393
- if (attribute.name === 'joints')
394
- joints = floats * 4;
395
- if (attribute.name === 'weights')
396
- weights = floats * 4;
397
- if (attribute.name === 'joints2')
398
- joints2 = floats * 4;
399
- if (attribute.name === 'weights2')
400
- weights2 = floats * 4;
401
- floats += attribute.components;
436
+ let weightsFormat = 'float32x4';
437
+ let weights2Format = 'float32x4';
438
+ for (const field of fields) {
439
+ const name = field.attribute.name;
440
+ if (name === 'joints')
441
+ joints = field.offset;
442
+ if (name === 'joints2')
443
+ joints2 = field.offset;
444
+ if (name === 'weights') {
445
+ weights = field.offset;
446
+ weightsFormat = field.format;
447
+ }
448
+ if (name === 'weights2') {
449
+ weights2 = field.offset;
450
+ weights2Format = field.format;
451
+ }
402
452
  }
403
453
  if (joints < 0 || weights < 0)
404
454
  return null;
405
455
  return joints2 >= 0 && weights2 >= 0
406
- ? { joints, weights, joints2, weights2 }
407
- : { joints, weights, joints2: -1, weights2: -1 };
456
+ ? { joints, weights, joints2, weights2, weightsFormat, weights2Format }
457
+ : { joints, weights, joints2: -1, weights2: -1, weightsFormat, weights2Format };
408
458
  }
409
459
  /** The handle, made where it can see only what it hands out. */
410
460
  function gpuMeshOf(parts) {
@@ -419,6 +469,7 @@ function gpuMeshOf(parts) {
419
469
  vertexStride: parts.stride,
420
470
  skinOffsets: parts.skinOffsets,
421
471
  indexBuffer,
472
+ indexFormat: parts.indexFormat,
422
473
  indexCount: parts.indexCount,
423
474
  bounds: parts.bounds,
424
475
  get complete() {
@@ -553,7 +604,9 @@ instanced = false) {
553
604
  * in the interleaved one. A *skinned* mesh interleaves them, so removing them would shift
554
605
  * the stride under every other attribute — which is why `createInstanced` refuses one.
555
606
  */
556
- const supplied = !attribute.optional || present[String(attribute.name)] === true;
607
+ /* One value every vertex shares sits with the absent ones: `vertexPacking.ts`. */
608
+ const packing = packingIn(present, String(attribute.name));
609
+ const supplied = (!attribute.optional || present[String(attribute.name)] === true) && packing !== 'constant';
557
610
  if (instanced && INSTANCE_LOCATIONS.has(attribute.shaderLocation)) {
558
611
  /*
559
612
  * **Free to drop only because the mesh does not supply it.** An absent optional attribute
@@ -573,12 +626,9 @@ instanced = false) {
573
626
  return;
574
627
  }
575
628
  if (supplied) {
576
- interleaved.push({
577
- shaderLocation: attribute.shaderLocation,
578
- offset,
579
- format: attribute.format,
580
- });
581
- offset += attribute.components * 4;
629
+ const format = packedFormat(attribute.format, packing);
630
+ interleaved.push({ shaderLocation: attribute.shaderLocation, offset, format });
631
+ offset += formatBytes(format);
582
632
  return;
583
633
  }
584
634
  constants.push({
@@ -0,0 +1,8 @@
1
+ export declare const CLOTH_ENTRIES: readonly ['clothPose', 'clothRest', 'clothBegin', 'clothPredict', 'clothDistance', 'clothBending', 'clothTether', 'clothLimit', 'clothFinish', 'clothBlend', 'clothPublish'];
2
+ export type ClothEntry = (typeof CLOTH_ENTRIES)[number];
3
+ export interface ClothKernels {
4
+ readonly layout: GPUBindGroupLayout;
5
+ readonly outputLayout: GPUBindGroupLayout;
6
+ readonly pipelines: Readonly<Record<ClothEntry, GPUComputePipeline>>;
7
+ }
8
+ export declare function clothKernelsFor(device: GPUDevice): ClothKernels;
@@ -0,0 +1,76 @@
1
+ /**
2
+ * The cloth solver's layouts and one compute pipeline an entry point of `shaders/clothSolve.wgsl.ts`,
3
+ * built once a device and shared by every set of garments on it.
4
+ */
5
+ import { CLOTH_SOLVE_WGSL } from './shaders/clothSolve.wgsl.js';
6
+ import { shaderModule } from './shaderModules.js';
7
+ const COMPUTE = 0x4;
8
+ export const CLOTH_ENTRIES = [
9
+ 'clothPose',
10
+ 'clothRest',
11
+ 'clothBegin',
12
+ 'clothPredict',
13
+ 'clothDistance',
14
+ 'clothBending',
15
+ 'clothTether',
16
+ 'clothLimit',
17
+ 'clothFinish',
18
+ 'clothBlend',
19
+ 'clothPublish',
20
+ ];
21
+ const kernelsByDevice = new WeakMap();
22
+ export function clothKernelsFor(device) {
23
+ const held = kernelsByDevice.get(device);
24
+ if (held !== undefined)
25
+ return held;
26
+ const uniform = (binding, dynamic) => ({
27
+ binding,
28
+ visibility: COMPUTE,
29
+ buffer: { type: 'uniform', hasDynamicOffset: dynamic },
30
+ });
31
+ const storage = (binding, type) => ({
32
+ binding,
33
+ visibility: COMPUTE,
34
+ buffer: { type },
35
+ });
36
+ /* Seven storage buffers of the eight a stage is granted: the shader's header says why so few. */
37
+ const layout = device.createBindGroupLayout({
38
+ label: 'cloth.solve',
39
+ entries: [
40
+ uniform(0, false),
41
+ uniform(1, true),
42
+ uniform(2, true),
43
+ storage(3, 'storage'),
44
+ storage(4, 'storage'),
45
+ storage(5, 'read-only-storage'),
46
+ storage(6, 'read-only-storage'),
47
+ storage(7, 'storage'),
48
+ storage(8, 'read-only-storage'),
49
+ storage(9, 'read-only-storage'),
50
+ ],
51
+ });
52
+ const outputLayout = device.createBindGroupLayout({
53
+ label: 'cloth.publish',
54
+ entries: [
55
+ {
56
+ binding: 0,
57
+ visibility: COMPUTE,
58
+ storageTexture: { access: 'write-only', format: 'rgba32float' },
59
+ },
60
+ ],
61
+ });
62
+ const module = shaderModule(device, { label: 'cloth.solve', code: CLOTH_SOLVE_WGSL });
63
+ const solve = device.createPipelineLayout({ bindGroupLayouts: [layout] });
64
+ const publish = device.createPipelineLayout({ bindGroupLayouts: [layout, outputLayout] });
65
+ const pipelines = {};
66
+ for (const entry of CLOTH_ENTRIES) {
67
+ pipelines[entry] = device.createComputePipeline({
68
+ label: `cloth.${entry}`,
69
+ layout: entry === 'clothPublish' ? publish : solve,
70
+ compute: { module, entryPoint: entry },
71
+ });
72
+ }
73
+ const kernels = { layout, outputLayout, pipelines };
74
+ kernelsByDevice.set(device, kernels);
75
+ return kernels;
76
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * What one garment of a set has asked the device for and the device has not yet recorded: a pose, a
3
+ * reset to rest, steps — each with the numbers its round reads, taken when it was asked for.
4
+ *
5
+ * **Taken when asked for, because they change after.** `ClothControl` moves its targets to a step's
6
+ * fraction and calls the device; by the time the set records the step, the next step has moved them
7
+ * again. So each op keeps the head of its record — fraction, blend, the poses it reads, the carry and
8
+ * the wind — as it stood, and the set copies it into the round that runs it.
9
+ *
10
+ * Grows by doubling and never shrinks, so a settled set allocates nothing a frame.
11
+ */
12
+ /** Words of a garment's record in a round: the shader's `Pace`. */
13
+ export declare const PACE_WORDS = 44;
14
+ /** The head of it, which an op carries: everything before the garment's own constants. */
15
+ export declare const PACE_HEAD_WORDS = 32;
16
+ export declare const CLOTH_OP_POSE = 0;
17
+ export declare const CLOTH_OP_REST = 1;
18
+ export declare const CLOTH_OP_STEP = 2;
19
+ export declare class ClothOps {
20
+ private kinds;
21
+ private heads;
22
+ private floats;
23
+ private words;
24
+ private first;
25
+ private end;
26
+ get pending(): number;
27
+ /** The kind of the op to run next, or -1 where there is none. */
28
+ get next(): number;
29
+ /** A new op of `kind`, its head zeroed, for the caller to fill: the float and word offset of it. */
30
+ push(kind: number): number;
31
+ get headFloats(): Float32Array;
32
+ get headWords(): Uint32Array;
33
+ /**
34
+ * The next op's head into `out` at `at`, and the op gone. Copied as words, since a head holds
35
+ * integers too, and a word at a time, since a view a call would be an allocation a round.
36
+ */
37
+ take(out: Uint32Array, at: number): void;
38
+ private grow;
39
+ }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * What one garment of a set has asked the device for and the device has not yet recorded: a pose, a
3
+ * reset to rest, steps — each with the numbers its round reads, taken when it was asked for.
4
+ *
5
+ * **Taken when asked for, because they change after.** `ClothControl` moves its targets to a step's
6
+ * fraction and calls the device; by the time the set records the step, the next step has moved them
7
+ * again. So each op keeps the head of its record — fraction, blend, the poses it reads, the carry and
8
+ * the wind — as it stood, and the set copies it into the round that runs it.
9
+ *
10
+ * Grows by doubling and never shrinks, so a settled set allocates nothing a frame.
11
+ */
12
+ /** Words of a garment's record in a round: the shader's `Pace`. */
13
+ export const PACE_WORDS = 44;
14
+ /** The head of it, which an op carries: everything before the garment's own constants. */
15
+ export const PACE_HEAD_WORDS = 32;
16
+ export const CLOTH_OP_POSE = 0;
17
+ export const CLOTH_OP_REST = 1;
18
+ export const CLOTH_OP_STEP = 2;
19
+ export class ClothOps {
20
+ kinds = new Uint8Array(8);
21
+ heads = new ArrayBuffer(8 * PACE_HEAD_WORDS * 4);
22
+ floats = new Float32Array(this.heads);
23
+ words = new Uint32Array(this.heads);
24
+ first = 0;
25
+ end = 0;
26
+ get pending() {
27
+ return this.end - this.first;
28
+ }
29
+ /** The kind of the op to run next, or -1 where there is none. */
30
+ get next() {
31
+ return this.first < this.end ? this.kinds[this.first] : -1;
32
+ }
33
+ /** A new op of `kind`, its head zeroed, for the caller to fill: the float and word offset of it. */
34
+ push(kind) {
35
+ if (this.end === this.kinds.length)
36
+ this.grow();
37
+ const at = this.end * PACE_HEAD_WORDS;
38
+ this.floats.fill(0, at, at + PACE_HEAD_WORDS);
39
+ this.kinds[this.end] = kind;
40
+ this.end++;
41
+ return at;
42
+ }
43
+ get headFloats() {
44
+ return this.floats;
45
+ }
46
+ get headWords() {
47
+ return this.words;
48
+ }
49
+ /**
50
+ * The next op's head into `out` at `at`, and the op gone. Copied as words, since a head holds
51
+ * integers too, and a word at a time, since a view a call would be an allocation a round.
52
+ */
53
+ take(out, at) {
54
+ const from = this.first * PACE_HEAD_WORDS;
55
+ for (let w = 0; w < PACE_HEAD_WORDS; w++)
56
+ out[at + w] = this.words[from + w];
57
+ this.first++;
58
+ if (this.first === this.end) {
59
+ this.first = 0;
60
+ this.end = 0;
61
+ }
62
+ }
63
+ grow() {
64
+ const live = this.end - this.first;
65
+ const capacity = this.kinds.length * 2;
66
+ const kinds = new Uint8Array(capacity);
67
+ kinds.set(this.kinds.subarray(this.first, this.end));
68
+ const heads = new ArrayBuffer(capacity * PACE_HEAD_WORDS * 4);
69
+ const floats = new Float32Array(heads);
70
+ floats.set(this.floats.subarray(this.first * PACE_HEAD_WORDS, this.end * PACE_HEAD_WORDS));
71
+ this.kinds = kinds;
72
+ this.heads = heads;
73
+ this.floats = floats;
74
+ this.words = new Uint32Array(heads);
75
+ this.first = 0;
76
+ this.end = live;
77
+ }
78
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * What a set of garments writes once into the buffers its rounds read, and the one per-step number
3
+ * worked out on the CPU: the constants, each round slot's first record, each colour's and each
4
+ * garment's batch slot, and the carry a step hands a garment that keeps less than all of its
5
+ * character's motion. Plain functions over the packed set, apart from the solver that schedules.
6
+ */
7
+ import type { ResolvedClothParameters } from '@driftengine/physics';
8
+ import type { PackedClothSet } from './clothSetPack.ts';
9
+ /** Bytes of the shader's `Constants`. */
10
+ export declare const CLOTH_CONSTANT_BYTES = 48;
11
+ /** The set's `Constants`: its totals, and the substep length every garment of it shares. */
12
+ export declare function clothConstants(packed: PackedClothSet, h: number): ArrayBuffer;
13
+ /** Each of `rounds` round slots' first record: the round's index times the garments. */
14
+ export declare function clothRoundOffsets(rounds: number, garments: number): Uint32Array<ArrayBuffer>;
15
+ /**
16
+ * Each colour's range, twice — opening a substep's iterations and not — at a slot each, kind by
17
+ * kind; then one slot a garment for publishing, its particles' range and its index.
18
+ */
19
+ export declare function clothBatchSlots(packed: PackedClothSet): Uint32Array<ArrayBuffer>;
20
+ /**
21
+ * `carryParticles`' rotation, origin and move, into a record's head at `at`, between the model
22
+ * matrices before and after the step; nothing where the garment keeps all of its character's
23
+ * motion, which leaves the head's carry at 0.
24
+ */
25
+ export declare function writeClothCarry(parameters: ResolvedClothParameters, previous: Float32Array, next: Float32Array, words: Uint32Array, f: Float32Array, at: number): void;
@@ -0,0 +1,84 @@
1
+ import { DYNAMIC_ALIGNMENT } from './uniformRing.js';
2
+ /** Bytes of the shader's `Constants`. */
3
+ export const CLOTH_CONSTANT_BYTES = 48;
4
+ /** The set's `Constants`: its totals, and the substep length every garment of it shares. */
5
+ export function clothConstants(packed, h) {
6
+ const bytes = new ArrayBuffer(CLOTH_CONSTANT_BYTES);
7
+ const u = new Uint32Array(bytes);
8
+ const f = new Float32Array(bytes);
9
+ u[0] = packed.count;
10
+ u[1] = packed.distances;
11
+ u[2] = packed.bendings;
12
+ u[3] = packed.joints;
13
+ u[4] = packed.colliders;
14
+ f[8] = h;
15
+ f[9] = 1 / h;
16
+ f[10] = 1 / (h * h);
17
+ return bytes;
18
+ }
19
+ /** Each of `rounds` round slots' first record: the round's index times the garments. */
20
+ export function clothRoundOffsets(rounds, garments) {
21
+ const words = new Uint32Array((rounds * DYNAMIC_ALIGNMENT) / 4);
22
+ for (let r = 0; r < rounds; r++)
23
+ words[(r * DYNAMIC_ALIGNMENT) / 4] = r * garments;
24
+ return words;
25
+ }
26
+ /**
27
+ * Each colour's range, twice — opening a substep's iterations and not — at a slot each, kind by
28
+ * kind; then one slot a garment for publishing, its particles' range and its index.
29
+ */
30
+ export function clothBatchSlots(packed) {
31
+ const { distanceBatches, bendingBatches, tetherBatches, garments } = packed;
32
+ const kinds = [distanceBatches, bendingBatches, tetherBatches];
33
+ let slots = garments.length;
34
+ for (const batches of kinds)
35
+ slots += (batches.length - 1) * 2;
36
+ const words = new Uint32Array((slots * DYNAMIC_ALIGNMENT) / 4);
37
+ let slot = 0;
38
+ for (const batches of kinds) {
39
+ for (let b = 0; b + 1 < batches.length; b++) {
40
+ for (let first = 0; first < 2; first++) {
41
+ const at = (slot * DYNAMIC_ALIGNMENT) / 4;
42
+ words[at] = batches[b];
43
+ words[at + 1] = batches[b + 1];
44
+ words[at + 2] = first;
45
+ slot++;
46
+ }
47
+ }
48
+ }
49
+ garments.forEach((range, g) => {
50
+ const at = (slot * DYNAMIC_ALIGNMENT) / 4;
51
+ words[at] = range.particleBase;
52
+ words[at + 1] = range.particleBase + range.count;
53
+ words[at + 2] = g;
54
+ slot++;
55
+ });
56
+ return words;
57
+ }
58
+ /**
59
+ * `carryParticles`' rotation, origin and move, into a record's head at `at`, between the model
60
+ * matrices before and after the step; nothing where the garment keeps all of its character's
61
+ * motion, which leaves the head's carry at 0.
62
+ */
63
+ export function writeClothCarry(parameters, previous, next, words, f, at) {
64
+ const turn = 1 - parameters.angularInertia;
65
+ const move = 1 - parameters.linearInertia;
66
+ if (turn === 0 && move === 0)
67
+ return;
68
+ words[at + 3] = 1;
69
+ /* Column c of R = R_next × R_previousᵀ, at turn0 … turn2. */
70
+ for (let col = 0; col < 3; col++) {
71
+ for (let row = 0; row < 3; row++) {
72
+ let sum = 0;
73
+ for (let k = 0; k < 3; k++) {
74
+ sum += next[k * 4 + row] * previous[k * 4 + col];
75
+ }
76
+ f[at + 8 + col * 4 + row] = sum;
77
+ }
78
+ }
79
+ for (let k = 0; k < 3; k++) {
80
+ f[at + 20 + k] = previous[12 + k];
81
+ f[at + 24 + k] = (next[12 + k] - previous[12 + k]) * move;
82
+ }
83
+ f[at + 23] = turn;
84
+ }
@@ -0,0 +1,33 @@
1
+ import type { PackedCloth } from './clothSolvePack.ts';
2
+ /** Floats of statics a particle in a set: a garment's own, and which garment it is. */
3
+ export declare const CLOTH_SET_STATIC_FLOATS: number;
4
+ /** Where one garment sits in a set. */
5
+ export interface ClothGarmentRange {
6
+ readonly particleBase: number;
7
+ readonly count: number;
8
+ readonly jointBase: number;
9
+ readonly joints: number;
10
+ readonly colliderBase: number;
11
+ readonly colliders: number;
12
+ /** `packCloth`'s limit flags, the garment's own. */
13
+ readonly limits: number;
14
+ readonly rigged: boolean;
15
+ }
16
+ export interface PackedClothSet {
17
+ /** Particles across every garment. */
18
+ readonly count: number;
19
+ readonly statics: Float32Array;
20
+ readonly constraints: Uint32Array;
21
+ readonly distances: number;
22
+ readonly bendings: number;
23
+ readonly tethers: number;
24
+ /** Each kind's colours across the set: colour `b` of every garment is `[b]` up to `[b + 1]`. */
25
+ readonly distanceBatches: Uint32Array;
26
+ readonly bendingBatches: Uint32Array;
27
+ readonly tetherBatches: Uint32Array;
28
+ /** Skin matrices and colliders across every garment. */
29
+ readonly joints: number;
30
+ readonly colliders: number;
31
+ readonly garments: readonly ClothGarmentRange[];
32
+ }
33
+ export declare function packClothSet(packs: readonly PackedCloth[]): PackedClothSet;