@driftengine/core 4.10.0 → 4.11.1

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 (165) hide show
  1. package/CHANGELOG.json +107 -0
  2. package/README.md +1 -1
  3. package/dist/index.d.ts +6 -5
  4. package/dist/index.js +2 -2
  5. package/dist/render/ambientOcclusionPass.d.ts +3 -2
  6. package/dist/render/ambientOcclusionPass.js +4 -2
  7. package/dist/render/backend/api.d.ts +21 -1
  8. package/dist/render/backend/webgl2/renderer.d.ts +141 -3
  9. package/dist/render/backend/webgl2/renderer.js +454 -42
  10. package/dist/render/backend/webgpu/buffers.d.ts +5 -0
  11. package/dist/render/backend/webgpu/buffers.js +98 -48
  12. package/dist/render/backend/webgpu/clothKernels.d.ts +8 -0
  13. package/dist/render/backend/webgpu/clothKernels.js +76 -0
  14. package/dist/render/backend/webgpu/clothOps.d.ts +39 -0
  15. package/dist/render/backend/webgpu/clothOps.js +78 -0
  16. package/dist/render/backend/webgpu/clothSetLayout.d.ts +25 -0
  17. package/dist/render/backend/webgpu/clothSetLayout.js +84 -0
  18. package/dist/render/backend/webgpu/clothSetPack.d.ts +33 -0
  19. package/dist/render/backend/webgpu/clothSetPack.js +144 -0
  20. package/dist/render/backend/webgpu/drawCommand.d.ts +2 -0
  21. package/dist/render/backend/webgpu/drawCommand.js +1 -0
  22. package/dist/render/backend/webgpu/flatPass.d.ts +1 -1
  23. package/dist/render/backend/webgpu/flatPass.js +2 -1
  24. package/dist/render/backend/webgpu/gpuClothSet.d.ts +80 -0
  25. package/dist/render/backend/webgpu/gpuClothSet.js +462 -0
  26. package/dist/render/backend/webgpu/latePass.js +1 -0
  27. package/dist/render/backend/webgpu/motionPass.js +15 -6
  28. package/dist/render/backend/webgpu/pipelineCache.d.ts +22 -1
  29. package/dist/render/backend/webgpu/pipelineCache.js +39 -1
  30. package/dist/render/backend/webgpu/postPass.d.ts +1 -1
  31. package/dist/render/backend/webgpu/renderer.d.ts +100 -1
  32. package/dist/render/backend/webgpu/renderer.js +423 -95
  33. package/dist/render/backend/webgpu/shaders/clothSolve.wgsl.d.ts +9 -2
  34. package/dist/render/backend/webgpu/shaders/clothSolve.wgsl.js +121 -47
  35. package/dist/render/backend/webgpu/staticBundles.js +2 -1
  36. package/dist/render/backend/webgpu/vertexFormats.d.ts +22 -0
  37. package/dist/render/backend/webgpu/vertexFormats.js +97 -0
  38. package/dist/render/globalMediumPass.d.ts +2 -0
  39. package/dist/render/globalMediumPass.js +1 -0
  40. package/dist/render/indexWidth.d.ts +17 -0
  41. package/dist/render/indexWidth.js +23 -0
  42. package/dist/render/instances.d.ts +13 -1
  43. package/dist/render/instances.js +10 -0
  44. package/dist/render/lightMatrix.d.ts +11 -0
  45. package/dist/render/lightMatrix.js +14 -0
  46. package/dist/render/mesh.d.ts +9 -0
  47. package/dist/render/mesh.js +38 -14
  48. package/dist/render/occlusionFade.d.ts +21 -0
  49. package/dist/render/occlusionFade.js +27 -0
  50. package/dist/render/renderQuality.d.ts +30 -0
  51. package/dist/render/renderQuality.js +5 -0
  52. package/dist/render/rendererHarness.d.ts +7 -0
  53. package/dist/render/rendererHarness.js +12 -0
  54. package/dist/render/sceneTarget.d.ts +2 -0
  55. package/dist/render/sceneTarget.js +1 -1
  56. package/dist/render/shader.d.ts +15 -0
  57. package/dist/render/shader.js +41 -0
  58. package/dist/render/shaders/ambientOcclusion.js +12 -1
  59. package/dist/render/shaders/flat/directionalShadow.js +84 -10
  60. package/dist/render/shaders/flat/glassTint.d.ts +1 -1
  61. package/dist/render/shaders/flat/glassTint.js +3 -0
  62. package/dist/render/shaders/flat/index.d.ts +15 -0
  63. package/dist/render/shaders/flat/index.js +24 -4
  64. package/dist/render/shaders/flat/layered.d.ts +27 -0
  65. package/dist/render/shaders/flat/layered.js +198 -0
  66. package/dist/render/shaders/flat/main.d.ts +1 -1
  67. package/dist/render/shaders/flat/main.js +50 -16
  68. package/dist/render/shaders/flat/mapReads.d.ts +10 -0
  69. package/dist/render/shaders/flat/mapReads.js +38 -0
  70. package/dist/render/shaders/flat/movingSun.d.ts +21 -0
  71. package/dist/render/shaders/flat/movingSun.js +46 -0
  72. package/dist/render/shaders/flat/normalMap.d.ts +6 -2
  73. package/dist/render/shaders/flat/normalMap.js +23 -3
  74. package/dist/render/shaders/flat/overlay.d.ts +3 -2
  75. package/dist/render/shaders/flat/overlay.js +41 -19
  76. package/dist/render/shaders/flat/preamble.js +23 -5
  77. package/dist/render/shaders/flat/worldUv.d.ts +22 -0
  78. package/dist/render/shaders/flat/worldUv.js +113 -0
  79. package/dist/render/shaders/generated/ambientOcclusion.wgsl.d.ts +6 -1
  80. package/dist/render/shaders/generated/ambientOcclusion.wgsl.js +6 -1
  81. package/dist/render/shaders/generated/flat.wgsl.d.ts +768 -368
  82. package/dist/render/shaders/generated/flat.wgsl.js +791 -391
  83. package/dist/render/shaders/generated/globalMedium.wgsl.d.ts +8 -3
  84. package/dist/render/shaders/generated/globalMedium.wgsl.js +8 -3
  85. package/dist/render/shaders/generated/lightVolume.wgsl.d.ts +15 -10
  86. package/dist/render/shaders/generated/lightVolume.wgsl.js +16 -11
  87. package/dist/render/shaders/globalMedium.js +9 -1
  88. package/dist/render/shaders/lightVolume.js +9 -1
  89. package/dist/render/shaders/tangentFrame.d.ts +1 -1
  90. package/dist/render/shaders/tangentFrame.js +16 -3
  91. package/dist/render/skinnedClothRun.d.ts +28 -0
  92. package/dist/render/skinnedClothRun.js +103 -35
  93. package/dist/render/surfaceLayers.d.ts +107 -0
  94. package/dist/render/surfaceLayers.js +82 -0
  95. package/dist/render/surfaceOverlay.d.ts +16 -3
  96. package/dist/render/surfaceOverlay.js +11 -6
  97. package/dist/render/surfaceProjection.d.ts +34 -0
  98. package/dist/render/surfaceProjection.js +30 -0
  99. package/dist/render/surfaceTexture.d.ts +39 -0
  100. package/dist/render/transmission.d.ts +21 -0
  101. package/dist/render/transmission.js +21 -0
  102. package/dist/render/vertexPacking.d.ts +48 -0
  103. package/dist/render/vertexPacking.js +68 -0
  104. package/dist/ui/splash.d.ts +42 -2
  105. package/dist/ui/splash.js +78 -13
  106. package/package.json +6 -6
  107. package/src/index.ts +6 -2
  108. package/src/render/ambientOcclusionPass.ts +4 -1
  109. package/src/render/backend/api.ts +31 -0
  110. package/src/render/backend/webgl2/renderer.ts +519 -56
  111. package/src/render/backend/webgpu/buffers.ts +153 -54
  112. package/src/render/backend/webgpu/clothKernels.ts +86 -0
  113. package/src/render/backend/webgpu/clothOps.ts +86 -0
  114. package/src/render/backend/webgpu/clothSetLayout.ts +102 -0
  115. package/src/render/backend/webgpu/clothSetPack.ts +197 -0
  116. package/src/render/backend/webgpu/drawCommand.ts +3 -0
  117. package/src/render/backend/webgpu/flatPass.ts +26 -28
  118. package/src/render/backend/webgpu/gpuClothSet.ts +564 -0
  119. package/src/render/backend/webgpu/latePass.ts +1 -0
  120. package/src/render/backend/webgpu/motionPass.ts +18 -7
  121. package/src/render/backend/webgpu/pipelineCache.ts +46 -7
  122. package/src/render/backend/webgpu/renderer.ts +622 -164
  123. package/src/render/backend/webgpu/shaders/clothSolve.wgsl.ts +121 -47
  124. package/src/render/backend/webgpu/staticBundles.ts +2 -1
  125. package/src/render/backend/webgpu/vertexFormats.ts +113 -0
  126. package/src/render/globalMediumPass.ts +7 -0
  127. package/src/render/indexWidth.ts +27 -0
  128. package/src/render/instances.ts +18 -1
  129. package/src/render/lightMatrix.ts +15 -0
  130. package/src/render/mesh.ts +66 -14
  131. package/src/render/occlusionFade.ts +29 -0
  132. package/src/render/renderQuality.ts +38 -0
  133. package/src/render/rendererHarness.ts +15 -0
  134. package/src/render/sceneTarget.ts +3 -0
  135. package/src/render/shader.ts +59 -0
  136. package/src/render/shaders/ambientOcclusion.ts +12 -1
  137. package/src/render/shaders/flat/directionalShadow.ts +84 -10
  138. package/src/render/shaders/flat/glassTint.ts +7 -1
  139. package/src/render/shaders/flat/index.ts +39 -4
  140. package/src/render/shaders/flat/layered.ts +198 -0
  141. package/src/render/shaders/flat/main.ts +50 -16
  142. package/src/render/shaders/flat/mapReads.ts +38 -0
  143. package/src/render/shaders/flat/movingSun.ts +46 -0
  144. package/src/render/shaders/flat/normalMap.ts +23 -3
  145. package/src/render/shaders/flat/overlay.ts +41 -19
  146. package/src/render/shaders/flat/preamble.ts +23 -5
  147. package/src/render/shaders/flat/worldUv.ts +113 -0
  148. package/src/render/shaders/generated/ambientOcclusion.wgsl.ts +6 -1
  149. package/src/render/shaders/generated/flat.wgsl.ts +791 -391
  150. package/src/render/shaders/generated/globalMedium.wgsl.ts +8 -3
  151. package/src/render/shaders/generated/lightVolume.wgsl.ts +16 -11
  152. package/src/render/shaders/globalMedium.ts +9 -1
  153. package/src/render/shaders/lightVolume.ts +9 -1
  154. package/src/render/shaders/tangentFrame.ts +16 -3
  155. package/src/render/skinnedClothRun.ts +138 -38
  156. package/src/render/surfaceLayers.ts +176 -0
  157. package/src/render/surfaceOverlay.ts +24 -7
  158. package/src/render/surfaceProjection.ts +57 -0
  159. package/src/render/surfaceTexture.ts +39 -0
  160. package/src/render/transmission.ts +27 -0
  161. package/src/render/vertexPacking.ts +109 -0
  162. package/src/ui/splash.ts +83 -14
  163. package/dist/render/backend/webgpu/gpuSkinnedCloth.d.ts +0 -74
  164. package/dist/render/backend/webgpu/gpuSkinnedCloth.js +0 -452
  165. package/src/render/backend/webgpu/gpuSkinnedCloth.ts +0 -502
@@ -6,6 +6,16 @@ import type { MeshData } from '../../mesh.ts';
6
6
  import { ABSENT_ATTRIBUTE } from '../../vertexDefaults.ts';
7
7
  import { UPLOAD_BYTES_PER_STEP } from '../../uploadStep.ts';
8
8
  import { INSTANCE_STRIDE } from '../../instances.ts';
9
+ import { compactIndices } from '../../indexWidth.ts';
10
+ import {
11
+ SNORM16_ONE,
12
+ UNORM16_ONE,
13
+ packingFor,
14
+ sharedValue,
15
+ vertexPackingOf,
16
+ type AttributePacking,
17
+ } from '../../vertexPacking.ts';
18
+ import { formatBytes, interleaveFixed, packedFormat, packingIn } from './vertexFormats.ts';
9
19
 
10
20
  /**
11
21
  * Mesh geometry as WebGPU buffers.
@@ -172,8 +182,13 @@ export interface GpuMesh {
172
182
  /** The second four influences' offsets, or -1 for a mesh with four. */
173
183
  readonly joints2: number;
174
184
  readonly weights2: number;
185
+ /** How the weights are read: floats, or sixteen-bit fixed point (`vertexPacking.ts`). */
186
+ readonly weightsFormat: GPUVertexFormat;
187
+ readonly weights2Format: GPUVertexFormat;
175
188
  } | null;
176
189
  readonly indexBuffer: GPUBuffer;
190
+ /** Sixteen or thirty-two bits an index: every pass that binds `indexBuffer` says which. */
191
+ readonly indexFormat: GPUIndexFormat;
177
192
  /**
178
193
  * How many indices the mesh draws. **Zero is a mesh with nothing to draw, and no pass draws it**:
179
194
  * a level can hold none of some kind of geometry, and a draw of zero indices does nothing but
@@ -357,17 +372,31 @@ export function createGpuMeshIncremental(
357
372
  const bounds = boundsOfPositions(data.positions, createBounds());
358
373
  const vertexCount = data.positions.length / 3;
359
374
  const layered = data.layers !== undefined;
360
- const supplied = VERTEX_LAYOUT.filter(
361
- (attribute) =>
362
- (!attribute.optional || data[attribute.name as keyof MeshData] !== undefined) &&
363
- !(dynamic && isStreamed(attribute)),
364
- ).map((attribute) => laidOut(attribute, layered));
375
+ /* How each attribute travels, which the pipeline key says too: `vertexPacking.ts`. */
376
+ const packing = vertexPackingOf(data, dynamic);
365
377
 
366
378
  /* Interleaved: one stride holding every attribute the mesh actually supplied — less a dynamic
367
- mesh's positions and normals, which are buffers of their own. */
368
- const stride = supplied.reduce((sum, attribute) => sum + attribute.components * 4, 0);
369
- const step = stride / 4;
370
- const interleaved = new Float32Array(step * vertexCount);
379
+ mesh's positions and normals, which are buffers of their own, and any attribute every vertex
380
+ shares, which is a constant. */
381
+ const fields: Field[] = [];
382
+ let stride = 0;
383
+ for (const declared of VERTEX_LAYOUT) {
384
+ if (declared.optional && data[declared.name as keyof MeshData] === undefined) continue;
385
+ if (dynamic && isStreamed(declared)) continue;
386
+ const attribute = laidOut(declared, layered);
387
+ const how = packingFor(packing, attribute.name);
388
+ if (how === 'constant') continue;
389
+ const format = packedFormat(attribute.format, how);
390
+ fields.push({ attribute, packing: how, format, offset: stride });
391
+ stride += formatBytes(format);
392
+ }
393
+ const buffer = new ArrayBuffer(stride * vertexCount);
394
+ const rows: Rows = {
395
+ floats: new Float32Array(buffer),
396
+ signed: new Int16Array(buffer),
397
+ unsigned: new Uint16Array(buffer),
398
+ bytes: new Uint8Array(buffer),
399
+ };
371
400
 
372
401
  /*
373
402
  * **Refused here, in one sentence, rather than by the driver a hundred times.**
@@ -383,7 +412,7 @@ export function createGpuMeshIncremental(
383
412
  * whose adapter caps at the default, and it deserves a sentence naming the model's size and
384
413
  * the device's rather than a wall of validation.
385
414
  */
386
- const byteLength = align4(interleaved.byteLength);
415
+ const byteLength = align4(buffer.byteLength);
387
416
  const ceiling = device.limits.maxBufferSize;
388
417
  if (typeof ceiling === 'number' && byteLength > ceiling) {
389
418
  throw new Error(
@@ -420,11 +449,20 @@ export function createGpuMeshIncremental(
420
449
  if (value === undefined) continue;
421
450
  absent.set(value, slot * (CONSTANT_SLOT / 4));
422
451
  }
452
+ /* And what every vertex shares, where a mesh's attribute is one value: `vertexPacking.ts`. */
453
+ for (let slot = 0; slot < VERTEX_LAYOUT.length; slot++) {
454
+ const attribute = VERTEX_LAYOUT[slot] as VertexAttribute;
455
+ if (packingFor(packing, attribute.name) !== 'constant') continue;
456
+ const values = data[attribute.name as keyof MeshData] as Float32Array;
457
+ absent.set(sharedValue(values, attribute.components), slot * (CONSTANT_SLOT / 4));
458
+ }
423
459
  device.queue.writeBuffer(constants, 0, absent);
424
460
 
461
+ /* Sixteen bits where every vertex fits, which every draw of this mesh says: indexWidth.ts. */
462
+ const indices = compactIndices(data.indices, vertexCount);
425
463
  const indexBuffer = device.createBuffer({
426
464
  label: 'mesh.indices',
427
- size: align4(data.indices.byteLength),
465
+ size: align4(indices.byteLength),
428
466
  usage: USAGE.INDEX | USAGE.COPY_DST,
429
467
  });
430
468
 
@@ -453,11 +491,12 @@ export function createGpuMeshIncremental(
453
491
  1,
454
492
  Math.floor(UPLOAD_BYTES_PER_STEP / (stride + (dynamic ? 24 : 0))),
455
493
  );
456
- const indexWidth = data.indices.BYTES_PER_ELEMENT;
457
- const indicesPerStep = Math.max(1, Math.floor(UPLOAD_BYTES_PER_STEP / indexWidth));
494
+ /* Whole four-byte words a step, which is all a queue writes: two sixteen-bit indices to one. */
495
+ const indexWidth = indices.BYTES_PER_ELEMENT;
496
+ const indicesPerStep = Math.max(1, Math.floor(UPLOAD_BYTES_PER_STEP / 4)) * (4 / indexWidth);
458
497
  const totalSteps =
459
498
  Math.max(1, Math.ceil(vertexCount / verticesPerStep)) +
460
- Math.max(1, Math.ceil(data.indices.length / indicesPerStep));
499
+ Math.max(1, Math.ceil(indices.length / indicesPerStep));
461
500
 
462
501
  /*
463
502
  * **The mesh is assembled out here, by functions of its own, and that is the fix for a copy
@@ -474,6 +513,7 @@ export function createGpuMeshIncremental(
474
513
  constants,
475
514
  streams,
476
515
  indexBuffer,
516
+ indexFormat: indices instanceof Uint16Array ? 'uint16' : 'uint32',
477
517
  stride: streams === null ? stride : 12,
478
518
  indexCount: data.indices.length,
479
519
  bounds,
@@ -483,7 +523,7 @@ export function createGpuMeshIncremental(
483
523
  isSkinnedEight: data.joints !== undefined && data.joints2 !== undefined,
484
524
  vertexCount,
485
525
  hasUvs: data.uvs !== undefined,
486
- skinOffsets: streams === null ? skinOffsetsOf(supplied) : null,
526
+ skinOffsets: streams === null ? skinOffsetsOf(fields) : null,
487
527
  hasChannel: data.channel !== undefined,
488
528
  morph:
489
529
  data.morphTargets === undefined || data.morphTargetCount === undefined
@@ -502,14 +542,14 @@ export function createGpuMeshIncremental(
502
542
 
503
543
  const job: UploadJob = {
504
544
  data,
505
- interleaved,
545
+ indices,
546
+ rows,
506
547
  device,
507
- supplied,
548
+ fields,
508
549
  streams,
509
550
  vertices,
510
551
  indexBuffer,
511
552
  vertexCount,
512
- step,
513
553
  stride,
514
554
  verticesPerStep,
515
555
  indicesPerStep,
@@ -519,20 +559,38 @@ export function createGpuMeshIncremental(
519
559
  return { mesh, upload: uploadSteps(job) };
520
560
  }
521
561
 
562
+ /** One attribute as a mesh interleaves it: how it travels, the format it is read with, where. */
563
+ interface Field {
564
+ readonly attribute: VertexAttribute;
565
+ readonly packing: AttributePacking;
566
+ readonly format: GPUVertexFormat;
567
+ /** Bytes into the vertex. */
568
+ readonly offset: number;
569
+ }
570
+
571
+ /** The interleaved rows, viewed as each field's type writes them. */
572
+ interface Rows {
573
+ readonly floats: Float32Array;
574
+ readonly signed: Int16Array;
575
+ readonly unsigned: Uint16Array;
576
+ readonly bytes: Uint8Array;
577
+ }
578
+
522
579
  /**
523
580
  * What an upload reads, held by the upload alone and let go when it finishes — so a caller that
524
581
  * keeps its iterator, as a streamer keeps the handle it was given, keeps nothing through it.
525
582
  */
526
583
  interface UploadJob {
527
584
  data: MeshData | null;
528
- interleaved: Float32Array | null;
585
+ /** The mesh's indices at the width its buffer holds them: `compactIndices`. */
586
+ indices: Uint16Array | Uint32Array | null;
587
+ rows: Rows | null;
529
588
  readonly device: GPUDevice;
530
- readonly supplied: readonly VertexAttribute[];
589
+ readonly fields: readonly Field[];
531
590
  readonly streams: { readonly positions: GPUBuffer; readonly normals: GPUBuffer } | null;
532
591
  readonly vertices: GPUBuffer;
533
592
  readonly indexBuffer: GPUBuffer;
534
593
  readonly vertexCount: number;
535
- readonly step: number;
536
594
  readonly stride: number;
537
595
  readonly verticesPerStep: number;
538
596
  readonly indicesPerStep: number;
@@ -541,33 +599,64 @@ interface UploadJob {
541
599
  }
542
600
 
543
601
  function* uploadSteps(job: UploadJob): Generator<void, void, void> {
544
- const { device, supplied, vertices, indexBuffer, vertexCount, step, stride } = job;
602
+ const { device, fields, vertices, indexBuffer, vertexCount, stride } = job;
603
+ /* Floats and halves a vertex: every field is whole words, so both divide. */
604
+ const step = stride / 4;
605
+ const halves = stride / 2;
545
606
  let taken = 0;
546
607
  for (let from = 0; from < vertexCount; from += job.verticesPerStep) {
547
608
  const data = job.data as MeshData;
548
- const interleaved = job.interleaved as Float32Array;
609
+ const rows = job.rows as Rows;
549
610
  const to = Math.min(from + job.verticesPerStep, vertexCount);
550
- let fieldOffset = 0;
551
- for (const attribute of supplied) {
611
+ for (const field of fields) {
612
+ const attribute = field.attribute;
552
613
  const source = data[attribute.name as keyof MeshData] as Float32Array;
553
- if (attribute === LAYERED_UVS) {
614
+ if (field.packing === 'snorm16') {
615
+ interleaveFixed(
616
+ rows.signed,
617
+ source,
618
+ attribute.components,
619
+ halves,
620
+ field.offset / 2,
621
+ from,
622
+ to,
623
+ SNORM16_ONE,
624
+ );
625
+ } else if (field.packing === 'unorm16') {
626
+ interleaveFixed(
627
+ rows.unsigned,
628
+ source,
629
+ attribute.components,
630
+ halves,
631
+ field.offset / 2,
632
+ from,
633
+ to,
634
+ UNORM16_ONE,
635
+ );
636
+ } else if (attribute === LAYERED_UVS) {
554
637
  /* (u, v) from the coordinates and the layer after them, one field of three. */
555
- interleaveField(interleaved, source, 2, step, fieldOffset, from, to);
638
+ const at = field.offset / 4;
639
+ interleaveField(rows.floats, source, 2, step, at, from, to);
640
+ interleaveField(rows.floats, data.layers as Float32Array, 1, step, at + 2, from, to);
641
+ } else {
556
642
  interleaveField(
557
- interleaved,
558
- data.layers as Float32Array,
559
- 1,
643
+ rows.floats,
644
+ source,
645
+ attribute.components,
560
646
  step,
561
- fieldOffset + 2,
647
+ field.offset / 4,
562
648
  from,
563
649
  to,
564
650
  );
565
- } else {
566
- interleaveField(interleaved, source, attribute.components, step, fieldOffset, from, to);
567
651
  }
568
- fieldOffset += attribute.components;
569
652
  }
570
- device.queue.writeBuffer(vertices, from * stride, interleaved, from * step, (to - from) * step);
653
+ device.queue.writeBuffer(
654
+ vertices,
655
+ from * stride,
656
+ rows.bytes,
657
+ from * stride,
658
+ (to - from) * stride,
659
+ );
571
660
  if (job.streams !== null) {
572
661
  const count = (to - from) * 3;
573
662
  device.queue.writeBuffer(job.streams.positions, from * 12, data.positions, from * 3, count);
@@ -577,7 +666,7 @@ function* uploadSteps(job: UploadJob): Generator<void, void, void> {
577
666
  if (taken < job.totalSteps) yield;
578
667
  }
579
668
 
580
- const indices = (job.data as MeshData).indices;
669
+ const indices = job.indices as Uint16Array | Uint32Array;
581
670
  const indexWidth = indices.BYTES_PER_ELEMENT;
582
671
  for (let from = 0; from < indices.length; from += job.indicesPerStep) {
583
672
  const to = Math.min(from + job.indicesPerStep, indices.length);
@@ -587,7 +676,8 @@ function* uploadSteps(job: UploadJob): Generator<void, void, void> {
587
676
  }
588
677
 
589
678
  job.data = null;
590
- job.interleaved = null;
679
+ job.indices = null;
680
+ job.rows = null;
591
681
  job.progress.uploaded = true;
592
682
  }
593
683
 
@@ -638,23 +728,30 @@ function interleaveField(
638
728
  * Where the joints and weights sit in a vertex of `supplied`, the attributes a mesh interleaves in
639
729
  * layout order; null unless it supplied both.
640
730
  */
641
- function skinOffsetsOf(supplied: readonly VertexAttribute[]): GpuMesh['skinOffsets'] {
642
- let floats = 0;
731
+ function skinOffsetsOf(fields: readonly Field[]): GpuMesh['skinOffsets'] {
643
732
  let joints = -1;
644
733
  let weights = -1;
645
734
  let joints2 = -1;
646
735
  let weights2 = -1;
647
- for (const attribute of supplied) {
648
- if (attribute.name === 'joints') joints = floats * 4;
649
- if (attribute.name === 'weights') weights = floats * 4;
650
- if (attribute.name === 'joints2') joints2 = floats * 4;
651
- if (attribute.name === 'weights2') weights2 = floats * 4;
652
- floats += attribute.components;
736
+ let weightsFormat: GPUVertexFormat = 'float32x4';
737
+ let weights2Format: GPUVertexFormat = 'float32x4';
738
+ for (const field of fields) {
739
+ const name = field.attribute.name;
740
+ if (name === 'joints') joints = field.offset;
741
+ if (name === 'joints2') joints2 = field.offset;
742
+ if (name === 'weights') {
743
+ weights = field.offset;
744
+ weightsFormat = field.format;
745
+ }
746
+ if (name === 'weights2') {
747
+ weights2 = field.offset;
748
+ weights2Format = field.format;
749
+ }
653
750
  }
654
751
  if (joints < 0 || weights < 0) return null;
655
752
  return joints2 >= 0 && weights2 >= 0
656
- ? { joints, weights, joints2, weights2 }
657
- : { joints, weights, joints2: -1, weights2: -1 };
753
+ ? { joints, weights, joints2, weights2, weightsFormat, weights2Format }
754
+ : { joints, weights, joints2: -1, weights2: -1, weightsFormat, weights2Format };
658
755
  }
659
756
 
660
757
  /** The handle, made where it can see only what it hands out. */
@@ -663,6 +760,7 @@ function gpuMeshOf(parts: {
663
760
  readonly constants: GPUBuffer;
664
761
  readonly streams: { readonly positions: GPUBuffer; readonly normals: GPUBuffer } | null;
665
762
  readonly indexBuffer: GPUBuffer;
763
+ readonly indexFormat: GPUIndexFormat;
666
764
  readonly stride: number;
667
765
  readonly indexCount: number;
668
766
  readonly bounds: Bounds;
@@ -690,6 +788,7 @@ function gpuMeshOf(parts: {
690
788
  vertexStride: parts.stride,
691
789
  skinOffsets: parts.skinOffsets,
692
790
  indexBuffer,
791
+ indexFormat: parts.indexFormat,
693
792
  indexCount: parts.indexCount,
694
793
  bounds: parts.bounds,
695
794
  get complete(): boolean {
@@ -844,7 +943,10 @@ export function vertexBufferLayouts(
844
943
  * in the interleaved one. A *skinned* mesh interleaves them, so removing them would shift
845
944
  * the stride under every other attribute — which is why `createInstanced` refuses one.
846
945
  */
847
- const supplied = !attribute.optional || present[String(attribute.name)] === true;
946
+ /* One value every vertex shares sits with the absent ones: `vertexPacking.ts`. */
947
+ const packing = packingIn(present, String(attribute.name));
948
+ const supplied =
949
+ (!attribute.optional || present[String(attribute.name)] === true) && packing !== 'constant';
848
950
  if (instanced && INSTANCE_LOCATIONS.has(attribute.shaderLocation)) {
849
951
  /*
850
952
  * **Free to drop only because the mesh does not supply it.** An absent optional attribute
@@ -866,12 +968,9 @@ export function vertexBufferLayouts(
866
968
  return;
867
969
  }
868
970
  if (supplied) {
869
- interleaved.push({
870
- shaderLocation: attribute.shaderLocation,
871
- offset,
872
- format: attribute.format,
873
- });
874
- offset += attribute.components * 4;
971
+ const format = packedFormat(attribute.format, packing);
972
+ interleaved.push({ shaderLocation: attribute.shaderLocation, offset, format });
973
+ offset += formatBytes(format);
875
974
  return;
876
975
  }
877
976
  constants.push({
@@ -0,0 +1,86 @@
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.ts';
6
+ import { shaderModule } from './shaderModules.ts';
7
+
8
+ const COMPUTE = 0x4;
9
+
10
+ export const CLOTH_ENTRIES = [
11
+ 'clothPose',
12
+ 'clothRest',
13
+ 'clothBegin',
14
+ 'clothPredict',
15
+ 'clothDistance',
16
+ 'clothBending',
17
+ 'clothTether',
18
+ 'clothLimit',
19
+ 'clothFinish',
20
+ 'clothBlend',
21
+ 'clothPublish',
22
+ ] as const;
23
+ export type ClothEntry = (typeof CLOTH_ENTRIES)[number];
24
+
25
+ export interface ClothKernels {
26
+ readonly layout: GPUBindGroupLayout;
27
+ readonly outputLayout: GPUBindGroupLayout;
28
+ readonly pipelines: Readonly<Record<ClothEntry, GPUComputePipeline>>;
29
+ }
30
+
31
+ const kernelsByDevice = new WeakMap<GPUDevice, ClothKernels>();
32
+
33
+ export function clothKernelsFor(device: GPUDevice): ClothKernels {
34
+ const held = kernelsByDevice.get(device);
35
+ if (held !== undefined) return held;
36
+ const uniform = (binding: number, dynamic: boolean): GPUBindGroupLayoutEntry => ({
37
+ binding,
38
+ visibility: COMPUTE,
39
+ buffer: { type: 'uniform', hasDynamicOffset: dynamic },
40
+ });
41
+ const storage = (binding: number, type: GPUBufferBindingType): GPUBindGroupLayoutEntry => ({
42
+ binding,
43
+ visibility: COMPUTE,
44
+ buffer: { type },
45
+ });
46
+ /* Seven storage buffers of the eight a stage is granted: the shader's header says why so few. */
47
+ const layout = device.createBindGroupLayout({
48
+ label: 'cloth.solve',
49
+ entries: [
50
+ uniform(0, false),
51
+ uniform(1, true),
52
+ uniform(2, true),
53
+ storage(3, 'storage'),
54
+ storage(4, 'storage'),
55
+ storage(5, 'read-only-storage'),
56
+ storage(6, 'read-only-storage'),
57
+ storage(7, 'storage'),
58
+ storage(8, 'read-only-storage'),
59
+ storage(9, 'read-only-storage'),
60
+ ],
61
+ });
62
+ const outputLayout = device.createBindGroupLayout({
63
+ label: 'cloth.publish',
64
+ entries: [
65
+ {
66
+ binding: 0,
67
+ visibility: COMPUTE,
68
+ storageTexture: { access: 'write-only', format: 'rgba32float' },
69
+ },
70
+ ],
71
+ });
72
+ const module = shaderModule(device, { label: 'cloth.solve', code: CLOTH_SOLVE_WGSL });
73
+ const solve = device.createPipelineLayout({ bindGroupLayouts: [layout] });
74
+ const publish = device.createPipelineLayout({ bindGroupLayouts: [layout, outputLayout] });
75
+ const pipelines = {} as Record<ClothEntry, GPUComputePipeline>;
76
+ for (const entry of CLOTH_ENTRIES) {
77
+ pipelines[entry] = device.createComputePipeline({
78
+ label: `cloth.${entry}`,
79
+ layout: entry === 'clothPublish' ? publish : solve,
80
+ compute: { module, entryPoint: entry },
81
+ });
82
+ }
83
+ const kernels = { layout, outputLayout, pipelines };
84
+ kernelsByDevice.set(device, kernels);
85
+ return kernels;
86
+ }
@@ -0,0 +1,86 @@
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
+
13
+ /** Words of a garment's record in a round: the shader's `Pace`. */
14
+ export const PACE_WORDS = 44;
15
+ /** The head of it, which an op carries: everything before the garment's own constants. */
16
+ export const PACE_HEAD_WORDS = 32;
17
+
18
+ export const CLOTH_OP_POSE = 0;
19
+ export const CLOTH_OP_REST = 1;
20
+ export const CLOTH_OP_STEP = 2;
21
+
22
+ export class ClothOps {
23
+ private kinds = new Uint8Array(8);
24
+ private heads = new ArrayBuffer(8 * PACE_HEAD_WORDS * 4);
25
+ private floats = new Float32Array(this.heads);
26
+ private words = new Uint32Array(this.heads);
27
+ private first = 0;
28
+ private end = 0;
29
+
30
+ get pending(): number {
31
+ return this.end - this.first;
32
+ }
33
+
34
+ /** The kind of the op to run next, or -1 where there is none. */
35
+ get next(): number {
36
+ return this.first < this.end ? (this.kinds[this.first] as number) : -1;
37
+ }
38
+
39
+ /** A new op of `kind`, its head zeroed, for the caller to fill: the float and word offset of it. */
40
+ push(kind: number): number {
41
+ if (this.end === this.kinds.length) this.grow();
42
+ const at = this.end * PACE_HEAD_WORDS;
43
+ this.floats.fill(0, at, at + PACE_HEAD_WORDS);
44
+ this.kinds[this.end] = kind;
45
+ this.end++;
46
+ return at;
47
+ }
48
+
49
+ get headFloats(): Float32Array {
50
+ return this.floats;
51
+ }
52
+
53
+ get headWords(): Uint32Array {
54
+ return this.words;
55
+ }
56
+
57
+ /**
58
+ * The next op's head into `out` at `at`, and the op gone. Copied as words, since a head holds
59
+ * integers too, and a word at a time, since a view a call would be an allocation a round.
60
+ */
61
+ take(out: Uint32Array, at: number): void {
62
+ const from = this.first * PACE_HEAD_WORDS;
63
+ for (let w = 0; w < PACE_HEAD_WORDS; w++) out[at + w] = this.words[from + w] as number;
64
+ this.first++;
65
+ if (this.first === this.end) {
66
+ this.first = 0;
67
+ this.end = 0;
68
+ }
69
+ }
70
+
71
+ private grow(): void {
72
+ const live = this.end - this.first;
73
+ const capacity = this.kinds.length * 2;
74
+ const kinds = new Uint8Array(capacity);
75
+ kinds.set(this.kinds.subarray(this.first, this.end));
76
+ const heads = new ArrayBuffer(capacity * PACE_HEAD_WORDS * 4);
77
+ const floats = new Float32Array(heads);
78
+ floats.set(this.floats.subarray(this.first * PACE_HEAD_WORDS, this.end * PACE_HEAD_WORDS));
79
+ this.kinds = kinds;
80
+ this.heads = heads;
81
+ this.floats = floats;
82
+ this.words = new Uint32Array(heads);
83
+ this.first = 0;
84
+ this.end = live;
85
+ }
86
+ }
@@ -0,0 +1,102 @@
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
+
9
+ import type { PackedClothSet } from './clothSetPack.ts';
10
+ import { DYNAMIC_ALIGNMENT } from './uniformRing.ts';
11
+
12
+ /** Bytes of the shader's `Constants`. */
13
+ export const CLOTH_CONSTANT_BYTES = 48;
14
+
15
+ /** The set's `Constants`: its totals, and the substep length every garment of it shares. */
16
+ export function clothConstants(packed: PackedClothSet, h: number): ArrayBuffer {
17
+ const bytes = new ArrayBuffer(CLOTH_CONSTANT_BYTES);
18
+ const u = new Uint32Array(bytes);
19
+ const f = new Float32Array(bytes);
20
+ u[0] = packed.count;
21
+ u[1] = packed.distances;
22
+ u[2] = packed.bendings;
23
+ u[3] = packed.joints;
24
+ u[4] = packed.colliders;
25
+ f[8] = h;
26
+ f[9] = 1 / h;
27
+ f[10] = 1 / (h * h);
28
+ return bytes;
29
+ }
30
+
31
+ /** Each of `rounds` round slots' first record: the round's index times the garments. */
32
+ export function clothRoundOffsets(rounds: number, garments: number): Uint32Array<ArrayBuffer> {
33
+ const words = new Uint32Array((rounds * DYNAMIC_ALIGNMENT) / 4);
34
+ for (let r = 0; r < rounds; r++) words[(r * DYNAMIC_ALIGNMENT) / 4] = r * garments;
35
+ return words;
36
+ }
37
+
38
+ /**
39
+ * Each colour's range, twice — opening a substep's iterations and not — at a slot each, kind by
40
+ * kind; then one slot a garment for publishing, its particles' range and its index.
41
+ */
42
+ export function clothBatchSlots(packed: PackedClothSet): Uint32Array<ArrayBuffer> {
43
+ const { distanceBatches, bendingBatches, tetherBatches, garments } = packed;
44
+ const kinds = [distanceBatches, bendingBatches, tetherBatches];
45
+ let slots = garments.length;
46
+ for (const batches of kinds) slots += (batches.length - 1) * 2;
47
+ const words = new Uint32Array((slots * DYNAMIC_ALIGNMENT) / 4);
48
+ let slot = 0;
49
+ for (const batches of kinds) {
50
+ for (let b = 0; b + 1 < batches.length; b++) {
51
+ for (let first = 0; first < 2; first++) {
52
+ const at = (slot * DYNAMIC_ALIGNMENT) / 4;
53
+ words[at] = batches[b] as number;
54
+ words[at + 1] = batches[b + 1] as number;
55
+ words[at + 2] = first;
56
+ slot++;
57
+ }
58
+ }
59
+ }
60
+ garments.forEach((range, g) => {
61
+ const at = (slot * DYNAMIC_ALIGNMENT) / 4;
62
+ words[at] = range.particleBase;
63
+ words[at + 1] = range.particleBase + range.count;
64
+ words[at + 2] = g;
65
+ slot++;
66
+ });
67
+ return words;
68
+ }
69
+
70
+ /**
71
+ * `carryParticles`' rotation, origin and move, into a record's head at `at`, between the model
72
+ * matrices before and after the step; nothing where the garment keeps all of its character's
73
+ * motion, which leaves the head's carry at 0.
74
+ */
75
+ export function writeClothCarry(
76
+ parameters: ResolvedClothParameters,
77
+ previous: Float32Array,
78
+ next: Float32Array,
79
+ words: Uint32Array,
80
+ f: Float32Array,
81
+ at: number,
82
+ ): void {
83
+ const turn = 1 - parameters.angularInertia;
84
+ const move = 1 - parameters.linearInertia;
85
+ if (turn === 0 && move === 0) return;
86
+ words[at + 3] = 1;
87
+ /* Column c of R = R_next × R_previousᵀ, at turn0 … turn2. */
88
+ for (let col = 0; col < 3; col++) {
89
+ for (let row = 0; row < 3; row++) {
90
+ let sum = 0;
91
+ for (let k = 0; k < 3; k++) {
92
+ sum += (next[k * 4 + row] as number) * (previous[k * 4 + col] as number);
93
+ }
94
+ f[at + 8 + col * 4 + row] = sum;
95
+ }
96
+ }
97
+ for (let k = 0; k < 3; k++) {
98
+ f[at + 20 + k] = previous[12 + k] as number;
99
+ f[at + 24 + k] = ((next[12 + k] as number) - (previous[12 + k] as number)) * move;
100
+ }
101
+ f[at + 23] = turn;
102
+ }