@driftengine/core 4.9.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 (231) hide show
  1. package/CHANGELOG.json +133 -11
  2. package/README.md +43 -38
  3. package/dist/index.d.ts +7 -4
  4. package/dist/index.js +5 -4
  5. package/dist/render/ambientHarmonics.d.ts +1 -1
  6. package/dist/render/ambientHarmonics.js +1 -1
  7. package/dist/render/areaLights.d.ts +4 -4
  8. package/dist/render/areaLights.js +1 -1
  9. package/dist/render/backend/api.d.ts +10 -3
  10. package/dist/render/backend/select.js +11 -5
  11. package/dist/render/backend/webgl2/boneAnimations.d.ts +29 -0
  12. package/dist/render/backend/webgl2/boneAnimations.js +80 -0
  13. package/dist/render/backend/webgl2/instanced.d.ts +5 -1
  14. package/dist/render/backend/webgl2/instanced.js +13 -1
  15. package/dist/render/backend/webgl2/renderer.d.ts +189 -4
  16. package/dist/render/backend/webgl2/renderer.js +614 -66
  17. package/dist/render/backend/webgl2/skinPaletteTexture.d.ts +5 -3
  18. package/dist/render/backend/webgl2/skinPaletteTexture.js +6 -4
  19. package/dist/render/backend/webgpu/boneAnimations.d.ts +34 -0
  20. package/dist/render/backend/webgpu/boneAnimations.js +76 -0
  21. package/dist/render/backend/webgpu/buffers.d.ts +24 -4
  22. package/dist/render/backend/webgpu/buffers.js +116 -59
  23. package/dist/render/backend/webgpu/clothKernels.d.ts +8 -0
  24. package/dist/render/backend/webgpu/clothKernels.js +76 -0
  25. package/dist/render/backend/webgpu/clothOps.d.ts +39 -0
  26. package/dist/render/backend/webgpu/clothOps.js +78 -0
  27. package/dist/render/backend/webgpu/clothSetLayout.d.ts +25 -0
  28. package/dist/render/backend/webgpu/clothSetLayout.js +84 -0
  29. package/dist/render/backend/webgpu/clothSetPack.d.ts +33 -0
  30. package/dist/render/backend/webgpu/clothSetPack.js +144 -0
  31. package/dist/render/backend/webgpu/compressedUpload.d.ts +0 -1
  32. package/dist/render/backend/webgpu/compressedUpload.js +53 -8
  33. package/dist/render/backend/webgpu/depthPass.d.ts +59 -3
  34. package/dist/render/backend/webgpu/depthPass.js +49 -14
  35. package/dist/render/backend/webgpu/drawCommand.d.ts +20 -4
  36. package/dist/render/backend/webgpu/drawCommand.js +11 -3
  37. package/dist/render/backend/webgpu/flatPass.d.ts +85 -42
  38. package/dist/render/backend/webgpu/flatPass.js +137 -20
  39. package/dist/render/backend/webgpu/gpuClothSet.d.ts +80 -0
  40. package/dist/render/backend/webgpu/gpuClothSet.js +462 -0
  41. package/dist/render/backend/webgpu/instanced.d.ts +14 -1
  42. package/dist/render/backend/webgpu/instanced.js +24 -1
  43. package/dist/render/backend/webgpu/latePass.js +1 -0
  44. package/dist/render/backend/webgpu/materialStore.d.ts +101 -0
  45. package/dist/render/backend/webgpu/materialStore.js +265 -0
  46. package/dist/render/backend/webgpu/motionPass.js +15 -6
  47. package/dist/render/backend/webgpu/pipelineCache.d.ts +28 -1
  48. package/dist/render/backend/webgpu/pipelineCache.js +46 -1
  49. package/dist/render/backend/webgpu/postPass.d.ts +1 -1
  50. package/dist/render/backend/webgpu/renderer.d.ts +200 -35
  51. package/dist/render/backend/webgpu/renderer.js +993 -168
  52. package/dist/render/backend/webgpu/shaders/clothSolve.wgsl.d.ts +9 -2
  53. package/dist/render/backend/webgpu/shaders/clothSolve.wgsl.js +121 -47
  54. package/dist/render/backend/webgpu/skinPaletteRing.d.ts +7 -3
  55. package/dist/render/backend/webgpu/skinPaletteRing.js +13 -7
  56. package/dist/render/backend/webgpu/skinPaletteTexture.d.ts +6 -0
  57. package/dist/render/backend/webgpu/skinPaletteTexture.js +7 -1
  58. package/dist/render/backend/webgpu/staticBundles.d.ts +125 -0
  59. package/dist/render/backend/webgpu/staticBundles.js +242 -0
  60. package/dist/render/backend/webgpu/surfaceTexturePass.d.ts +10 -3
  61. package/dist/render/backend/webgpu/surfaceTexturePass.js +20 -2
  62. package/dist/render/backend/webgpu/uniformRing.d.ts +21 -0
  63. package/dist/render/backend/webgpu/uniformRing.js +9 -1
  64. package/dist/render/backend/webgpu/vertexFormats.d.ts +22 -0
  65. package/dist/render/backend/webgpu/vertexFormats.js +97 -0
  66. package/dist/render/bloomChain.d.ts +3 -3
  67. package/dist/render/boneAnimation.d.ts +92 -0
  68. package/dist/render/boneAnimation.js +133 -0
  69. package/dist/render/compressedSource.d.ts +33 -13
  70. package/dist/render/compressedSource.js +70 -25
  71. package/dist/render/glCompressed.js +83 -0
  72. package/dist/render/globalMediumPass.d.ts +2 -0
  73. package/dist/render/globalMediumPass.js +1 -0
  74. package/dist/render/indexWidth.d.ts +17 -0
  75. package/dist/render/indexWidth.js +23 -0
  76. package/dist/render/instances.d.ts +26 -0
  77. package/dist/render/lightBudget.d.ts +10 -0
  78. package/dist/render/lightBudget.js +10 -0
  79. package/dist/render/lightMatrix.d.ts +11 -0
  80. package/dist/render/lightMatrix.js +14 -0
  81. package/dist/render/lightmap.d.ts +1 -1
  82. package/dist/render/mesh.d.ts +14 -0
  83. package/dist/render/mesh.js +44 -14
  84. package/dist/render/pointLightSelection.d.ts +4 -4
  85. package/dist/render/renderQuality.d.ts +30 -0
  86. package/dist/render/renderQuality.js +5 -0
  87. package/dist/render/rendererHarness.d.ts +7 -0
  88. package/dist/render/rendererHarness.js +12 -0
  89. package/dist/render/screenSpaceReflection.d.ts +2 -1
  90. package/dist/render/shader.d.ts +15 -0
  91. package/dist/render/shader.js +41 -0
  92. package/dist/render/shaders/boneAnimation.d.ts +19 -0
  93. package/dist/render/shaders/boneAnimation.js +63 -0
  94. package/dist/render/shaders/depth.d.ts +7 -0
  95. package/dist/render/shaders/depth.js +35 -10
  96. package/dist/render/shaders/flat/directionalShadow.js +84 -10
  97. package/dist/render/shaders/flat/glassTint.d.ts +1 -1
  98. package/dist/render/shaders/flat/glassTint.js +3 -0
  99. package/dist/render/shaders/flat/index.d.ts +21 -0
  100. package/dist/render/shaders/flat/index.js +42 -7
  101. package/dist/render/shaders/flat/layered.d.ts +19 -0
  102. package/dist/render/shaders/flat/layered.js +115 -0
  103. package/dist/render/shaders/flat/main.d.ts +1 -1
  104. package/dist/render/shaders/flat/main.js +58 -21
  105. package/dist/render/shaders/flat/mapReads.d.ts +10 -0
  106. package/dist/render/shaders/flat/mapReads.js +38 -0
  107. package/dist/render/shaders/flat/models.js +1 -1
  108. package/dist/render/shaders/flat/movingSun.d.ts +21 -0
  109. package/dist/render/shaders/flat/movingSun.js +46 -0
  110. package/dist/render/shaders/flat/normalMap.d.ts +6 -2
  111. package/dist/render/shaders/flat/normalMap.js +14 -3
  112. package/dist/render/shaders/flat/overlay.d.ts +3 -2
  113. package/dist/render/shaders/flat/overlay.js +42 -20
  114. package/dist/render/shaders/flat/pointShadow.js +2 -2
  115. package/dist/render/shaders/flat/preamble.js +59 -27
  116. package/dist/render/shaders/flat/worldUv.d.ts +22 -0
  117. package/dist/render/shaders/flat/worldUv.js +113 -0
  118. package/dist/render/shaders/generated/depth.wgsl.d.ts +93 -0
  119. package/dist/render/shaders/generated/depth.wgsl.js +93 -0
  120. package/dist/render/shaders/generated/flat.wgsl.d.ts +3616 -3042
  121. package/dist/render/shaders/generated/flat.wgsl.js +3688 -3113
  122. package/dist/render/shaders/generated/globalMedium.wgsl.d.ts +8 -3
  123. package/dist/render/shaders/generated/globalMedium.wgsl.js +8 -3
  124. package/dist/render/shaders/generated/lightVolume.wgsl.d.ts +15 -10
  125. package/dist/render/shaders/generated/lightVolume.wgsl.js +16 -11
  126. package/dist/render/shaders/globalMedium.js +9 -1
  127. package/dist/render/shaders/lightVolume.js +9 -1
  128. package/dist/render/shaders/outputTransform.d.ts +7 -0
  129. package/dist/render/shaders/outputTransform.js +28 -14
  130. package/dist/render/shaders/tangentFrame.d.ts +1 -1
  131. package/dist/render/shaders/tangentFrame.js +16 -3
  132. package/dist/render/shaders/vertexChannel.d.ts +7 -0
  133. package/dist/render/shaders/vertexChannel.js +26 -0
  134. package/dist/render/skinnedClothRun.d.ts +28 -0
  135. package/dist/render/skinnedClothRun.js +103 -35
  136. package/dist/render/staticDraws.d.ts +47 -0
  137. package/dist/render/staticDraws.js +43 -0
  138. package/dist/render/surfaceLayers.d.ts +39 -0
  139. package/dist/render/surfaceLayers.js +26 -0
  140. package/dist/render/surfaceOverlay.d.ts +16 -3
  141. package/dist/render/surfaceOverlay.js +11 -6
  142. package/dist/render/surfaceProjection.d.ts +34 -0
  143. package/dist/render/surfaceProjection.js +30 -0
  144. package/dist/render/surfaceTexture.d.ts +68 -7
  145. package/dist/render/surfaceTexture.js +56 -17
  146. package/dist/render/vertexPacking.d.ts +48 -0
  147. package/dist/render/vertexPacking.js +68 -0
  148. package/package.json +6 -6
  149. package/src/index.ts +12 -4
  150. package/src/render/ambientHarmonics.ts +1 -1
  151. package/src/render/areaLights.ts +4 -4
  152. package/src/render/backend/api.ts +13 -3
  153. package/src/render/backend/select.ts +10 -5
  154. package/src/render/backend/webgl2/boneAnimations.ts +105 -0
  155. package/src/render/backend/webgl2/instanced.ts +20 -1
  156. package/src/render/backend/webgl2/renderer.ts +723 -79
  157. package/src/render/backend/webgl2/skinPaletteTexture.ts +6 -4
  158. package/src/render/backend/webgpu/boneAnimations.ts +97 -0
  159. package/src/render/backend/webgpu/buffers.ts +195 -70
  160. package/src/render/backend/webgpu/clothKernels.ts +86 -0
  161. package/src/render/backend/webgpu/clothOps.ts +86 -0
  162. package/src/render/backend/webgpu/clothSetLayout.ts +102 -0
  163. package/src/render/backend/webgpu/clothSetPack.ts +197 -0
  164. package/src/render/backend/webgpu/compressedUpload.ts +59 -9
  165. package/src/render/backend/webgpu/depthPass.ts +51 -10
  166. package/src/render/backend/webgpu/drawCommand.ts +25 -4
  167. package/src/render/backend/webgpu/flatPass.ts +212 -41
  168. package/src/render/backend/webgpu/gpuClothSet.ts +564 -0
  169. package/src/render/backend/webgpu/instanced.ts +32 -1
  170. package/src/render/backend/webgpu/latePass.ts +1 -0
  171. package/src/render/backend/webgpu/materialStore.ts +276 -0
  172. package/src/render/backend/webgpu/motionPass.ts +18 -7
  173. package/src/render/backend/webgpu/pipelineCache.ts +53 -7
  174. package/src/render/backend/webgpu/renderer.ts +1201 -202
  175. package/src/render/backend/webgpu/shaders/clothSolve.wgsl.ts +121 -47
  176. package/src/render/backend/webgpu/skinPaletteRing.ts +13 -7
  177. package/src/render/backend/webgpu/skinPaletteTexture.ts +7 -1
  178. package/src/render/backend/webgpu/staticBundles.ts +364 -0
  179. package/src/render/backend/webgpu/surfaceTexturePass.ts +29 -3
  180. package/src/render/backend/webgpu/uniformRing.ts +10 -1
  181. package/src/render/backend/webgpu/vertexFormats.ts +113 -0
  182. package/src/render/bloomChain.ts +3 -3
  183. package/src/render/boneAnimation.ts +223 -0
  184. package/src/render/compressedSource.ts +121 -29
  185. package/src/render/glCompressed.ts +83 -0
  186. package/src/render/globalMediumPass.ts +7 -0
  187. package/src/render/indexWidth.ts +27 -0
  188. package/src/render/instances.ts +27 -0
  189. package/src/render/lightBudget.ts +11 -0
  190. package/src/render/lightMatrix.ts +15 -0
  191. package/src/render/lightmap.ts +1 -1
  192. package/src/render/mesh.ts +72 -14
  193. package/src/render/pointLightSelection.ts +4 -4
  194. package/src/render/renderQuality.ts +38 -0
  195. package/src/render/rendererHarness.ts +15 -0
  196. package/src/render/screenSpaceReflection.ts +2 -1
  197. package/src/render/shader.ts +59 -0
  198. package/src/render/shaders/boneAnimation.ts +63 -0
  199. package/src/render/shaders/depth.ts +45 -10
  200. package/src/render/shaders/flat/directionalShadow.ts +84 -10
  201. package/src/render/shaders/flat/glassTint.ts +7 -1
  202. package/src/render/shaders/flat/index.ts +63 -7
  203. package/src/render/shaders/flat/layered.ts +115 -0
  204. package/src/render/shaders/flat/main.ts +58 -21
  205. package/src/render/shaders/flat/mapReads.ts +38 -0
  206. package/src/render/shaders/flat/models.ts +1 -1
  207. package/src/render/shaders/flat/movingSun.ts +46 -0
  208. package/src/render/shaders/flat/normalMap.ts +14 -3
  209. package/src/render/shaders/flat/overlay.ts +42 -20
  210. package/src/render/shaders/flat/pointShadow.ts +2 -2
  211. package/src/render/shaders/flat/preamble.ts +59 -27
  212. package/src/render/shaders/flat/worldUv.ts +113 -0
  213. package/src/render/shaders/generated/depth.wgsl.ts +95 -0
  214. package/src/render/shaders/generated/flat.wgsl.ts +3688 -3113
  215. package/src/render/shaders/generated/globalMedium.wgsl.ts +8 -3
  216. package/src/render/shaders/generated/lightVolume.wgsl.ts +16 -11
  217. package/src/render/shaders/globalMedium.ts +9 -1
  218. package/src/render/shaders/lightVolume.ts +9 -1
  219. package/src/render/shaders/outputTransform.ts +30 -14
  220. package/src/render/shaders/tangentFrame.ts +16 -3
  221. package/src/render/shaders/vertexChannel.ts +27 -0
  222. package/src/render/skinnedClothRun.ts +138 -38
  223. package/src/render/staticDraws.ts +95 -0
  224. package/src/render/surfaceLayers.ts +57 -0
  225. package/src/render/surfaceOverlay.ts +24 -7
  226. package/src/render/surfaceProjection.ts +57 -0
  227. package/src/render/surfaceTexture.ts +124 -27
  228. package/src/render/vertexPacking.ts +109 -0
  229. package/dist/render/backend/webgpu/gpuSkinnedCloth.d.ts +0 -74
  230. package/dist/render/backend/webgpu/gpuSkinnedCloth.js +0 -452
  231. package/src/render/backend/webgpu/gpuSkinnedCloth.ts +0 -502
@@ -0,0 +1,564 @@
1
+ /**
2
+ * Skinned cloth solved on the device, a set of garments at a time: `ClothControl`'s schedule for
3
+ * each garment, over one set of buffers, in one compute pass and one submit a frame.
4
+ *
5
+ * **The CPU solver's steps, as dispatches.** A step is `clothBegin`, then per substep
6
+ * `clothPredict`, every colour of every kind of constraint per iteration, `clothLimit` and
7
+ * `clothFinish`; then `clothBlend` while a reset blends back in. The particles never leave the
8
+ * device: a frame ends with `clothPublish` writing each garment's, at the frame's `alpha`, into the
9
+ * particle texture its bound meshes' vertex stage reads — so a garment costs no upload a frame.
10
+ *
11
+ * **A round is one kind of work for every garment that has it next.** Each garment's control asks
12
+ * for poses, resets and steps as it always did; they wait in its queue (`clothOps.ts`) until the
13
+ * set records them a round at a time — the poses of every garment with a pose next, in one
14
+ * dispatch, then the rests, then a step for every garment with a step next, each colour of each kind
15
+ * one dispatch across the set (`clothSetPack.ts`). A garment with nothing of that kind sits the round
16
+ * out. So a step costs one garment's dispatches whatever the set holds: fifteen garments of nine
17
+ * colours at four iterations are about a hundred a step, where a pass a garment was some fourteen
18
+ * hundred.
19
+ *
20
+ * **One submit a frame, everything it needs written before it.** `queue.writeBuffer` lands ahead of
21
+ * the whole submit rather than between two dispatches, which is the trap `AGENTS.md` (2026-08-27)
22
+ * records for draws and is no different here. So every round's records take a slot of their own, a
23
+ * frame that runs out of slots submits what it has and goes on, and a garment posed while work
24
+ * against its last pose is still unsent sends that work first, since its matrices are one region.
25
+ *
26
+ * **What it gives up**: single precision; a colour dispatched over the most records any garment has
27
+ * in it; and garments of one set step together, so they share a step, substeps, iterations and most
28
+ * steps a frame, refused by name otherwise. Self-collision is accepted and not simulated, as on the
29
+ * CPU.
30
+ */
31
+ import { ClothControl, exactExp, validateClothSetup } from '@driftengine/physics';
32
+ import type { ClothDevice, SkinnedClothSetup } from '@driftengine/physics';
33
+
34
+ import { clothKernelsFor } from './clothKernels.ts';
35
+ import type { ClothEntry, ClothKernels } from './clothKernels.ts';
36
+ import {
37
+ CLOTH_OP_POSE,
38
+ CLOTH_OP_REST,
39
+ CLOTH_OP_STEP,
40
+ ClothOps,
41
+ PACE_HEAD_WORDS,
42
+ PACE_WORDS,
43
+ } from './clothOps.ts';
44
+ import {
45
+ CLOTH_CONSTANT_BYTES,
46
+ clothBatchSlots,
47
+ clothConstants,
48
+ clothRoundOffsets,
49
+ writeClothCarry,
50
+ } from './clothSetLayout.ts';
51
+ import { packClothSet } from './clothSetPack.ts';
52
+ import type { ClothGarmentRange, PackedClothSet } from './clothSetPack.ts';
53
+ import { packCloth } from './clothSolvePack.ts';
54
+ import { GpuClothParticles } from './clothTextures.ts';
55
+ import { CLOTH_WORKGROUP } from './shaders/clothSolve.wgsl.ts';
56
+ import { DYNAMIC_ALIGNMENT } from './uniformRing.ts';
57
+
58
+ const STORAGE = 0x80;
59
+ const UNIFORM = 0x40;
60
+ const COPY_DST = 0x8;
61
+ const COPY_SRC = 0x4;
62
+ const MAP_READ = 0x1;
63
+
64
+ /** Rounds a submit holds before it is sent and the slots start again. */
65
+ const ROUNDS = 64;
66
+ const KINDS = [CLOTH_OP_POSE, CLOTH_OP_REST, CLOTH_OP_STEP] as const;
67
+
68
+ /** One garment of a set: its schedule, its queue and where it sits. */
69
+ interface Garment {
70
+ readonly range: ClothGarmentRange;
71
+ readonly control: ClothControl;
72
+ readonly particles: GpuClothParticles;
73
+ readonly ops: ClothOps;
74
+ /** The air its steps drag toward: the set-up's until `setWind`. */
75
+ readonly wind: Float32Array;
76
+ readonly outputs: readonly [GPUBindGroup, GPUBindGroup];
77
+ /** Where the pose a step starts from and the one it moves toward sit in `pose`, in floats. */
78
+ fromAt: number;
79
+ toAt: number;
80
+ /** Work recorded against its pose that has not been sent. */
81
+ recorded: boolean;
82
+ }
83
+
84
+ export class GpuClothSet {
85
+ private readonly garments: Garment[] = [];
86
+ private readonly packed: PackedClothSet;
87
+ private readonly kernels: ClothKernels;
88
+ private readonly buffers: GPUBuffer[] = [];
89
+ private readonly state: GPUBuffer;
90
+ private readonly frameData: GPUBuffer;
91
+ private readonly paces: GPUBuffer;
92
+ private readonly group: GPUBindGroup;
93
+ /** One round's records, every garment's: the head an op's, the rest each garment's own. */
94
+ private readonly staging: ArrayBuffer;
95
+ private readonly stagingWords: Uint32Array;
96
+ private readonly stagingFloats: Float32Array;
97
+ /** Each kind's first batch slot, and the first garment's publishing slot. */
98
+ private readonly batchSlot: {
99
+ distance: number;
100
+ bending: number;
101
+ tether: number;
102
+ publish: number;
103
+ };
104
+ private readonly groups: number;
105
+ private readonly substeps: number;
106
+ private readonly iterations: number;
107
+
108
+ private encoder: GPUCommandEncoder | null = null;
109
+ private pass: GPUComputePassEncoder | null = null;
110
+ private round = 0;
111
+
112
+ constructor(
113
+ private readonly device: GPUDevice,
114
+ setups: readonly SkinnedClothSetup[],
115
+ ) {
116
+ if (setups.length === 0) throw new Error('skinned cloth set: no garments');
117
+ for (const setup of setups) validateClothSetup(setup);
118
+ const packed = packClothSet(setups.map(packCloth));
119
+ this.packed = packed;
120
+ const n = packed.count;
121
+ const count = setups.length;
122
+ this.kernels = clothKernelsFor(device);
123
+ this.groups = Math.ceil(n / CLOTH_WORKGROUP);
124
+
125
+ const buffer = (label: string, bytes: number, usage: number): GPUBuffer => {
126
+ const made = device.createBuffer({
127
+ label: `cloth.${label}`,
128
+ size: Math.max(16, bytes),
129
+ usage,
130
+ });
131
+ this.buffers.push(made);
132
+ return made;
133
+ };
134
+ this.staging = new ArrayBuffer(count * PACE_WORDS * 4);
135
+ this.stagingWords = new Uint32Array(this.staging);
136
+ this.stagingFloats = new Float32Array(this.staging);
137
+ setups.forEach((setup, g) => this.garments.push(this.garment(setup, g)));
138
+ const first = (this.garments[0] as Garment).control.parameters;
139
+ this.substeps = first.substeps;
140
+ this.iterations = first.iterations;
141
+ this.refuseStrangers(first);
142
+
143
+ const constants = buffer('constants', CLOTH_CONSTANT_BYTES, UNIFORM | COPY_DST);
144
+ const rounds = buffer('rounds', ROUNDS * DYNAMIC_ALIGNMENT, UNIFORM | COPY_DST);
145
+ this.paces = buffer('paces', ROUNDS * this.staging.byteLength, STORAGE | COPY_DST);
146
+ const batchWords = clothBatchSlots(packed);
147
+ const batches = buffer('batches', batchWords.byteLength, UNIFORM | COPY_DST);
148
+ this.state = buffer('state', n * 12 * 4, STORAGE | COPY_DST | COPY_SRC);
149
+ const pose = buffer('pose', n * 9 * 4, STORAGE);
150
+ const statics = buffer('statics', packed.statics.byteLength, STORAGE | COPY_DST);
151
+ const constraints = buffer('constraints', packed.constraints.byteLength, STORAGE | COPY_DST);
152
+ const lambda = buffer('lambda', (packed.distances + packed.bendings) * 4, STORAGE);
153
+ this.frameData = buffer(
154
+ 'frame',
155
+ (packed.joints * 16 + packed.colliders * 8) * 4,
156
+ STORAGE | COPY_DST,
157
+ );
158
+
159
+ const queue = device.queue;
160
+ queue.writeBuffer(constants, 0, clothConstants(packed, first.step / first.substeps));
161
+ queue.writeBuffer(rounds, 0, clothRoundOffsets(ROUNDS, count));
162
+ queue.writeBuffer(batches, 0, batchWords);
163
+ queue.writeBuffer(statics, 0, packed.statics as Float32Array<ArrayBuffer>);
164
+ if (packed.constraints.byteLength > 0) {
165
+ queue.writeBuffer(constraints, 0, packed.constraints as Uint32Array<ArrayBuffer>);
166
+ }
167
+ /* The rest pose until the first pose, which resets each garment to its own. */
168
+ const state = new Float32Array(n * 12);
169
+ setups.forEach((setup, g) => {
170
+ const base = (this.garments[g] as Garment).range.particleBase * 3;
171
+ state.set(setup.positions, base);
172
+ state.set(setup.positions, n * 9 + base);
173
+ });
174
+ queue.writeBuffer(this.state, 0, state);
175
+
176
+ const slot = (of: GPUBuffer): GPUBufferBinding => ({ buffer: of, size: DYNAMIC_ALIGNMENT });
177
+ this.group = device.createBindGroup({
178
+ label: 'cloth.solve',
179
+ layout: this.kernels.layout,
180
+ entries: [
181
+ { binding: 0, resource: { buffer: constants } },
182
+ { binding: 1, resource: slot(rounds) },
183
+ { binding: 2, resource: slot(batches) },
184
+ { binding: 3, resource: { buffer: this.state } },
185
+ { binding: 4, resource: { buffer: pose } },
186
+ { binding: 5, resource: { buffer: statics } },
187
+ { binding: 6, resource: { buffer: constraints } },
188
+ { binding: 7, resource: { buffer: lambda } },
189
+ { binding: 8, resource: { buffer: this.frameData } },
190
+ { binding: 9, resource: { buffer: this.paces } },
191
+ ],
192
+ });
193
+ const colours = (batches: Uint32Array): number => (batches.length - 1) * 2;
194
+ this.batchSlot = {
195
+ distance: 0,
196
+ bending: colours(packed.distanceBatches),
197
+ tether: colours(packed.distanceBatches) + colours(packed.bendingBatches),
198
+ publish:
199
+ colours(packed.distanceBatches) +
200
+ colours(packed.bendingBatches) +
201
+ colours(packed.tetherBatches),
202
+ };
203
+ }
204
+
205
+ /** How many garments the set holds. */
206
+ get size(): number {
207
+ return this.garments.length;
208
+ }
209
+
210
+ /** Garment `g`'s particles, what `setCloth` draws its bound meshes by. */
211
+ particles(g: number): GpuClothParticles {
212
+ return this.at(g).particles;
213
+ }
214
+
215
+ /** Garment `g`'s rig for the steps to come; see `ClothControl.setPose`. */
216
+ setPose(g: number, globals: Float32Array, model: Float32Array): void {
217
+ this.at(g).control.setPose(globals, model);
218
+ }
219
+
220
+ /** The frame's sample of the scene's wind for garment `g`. See `SkinnedCloth.setWind`. */
221
+ setWind(g: number, x: number, y: number, z: number): void {
222
+ const wind = this.at(g).wind;
223
+ wind[0] = x;
224
+ wind[1] = y;
225
+ wind[2] = z;
226
+ }
227
+
228
+ /** Garment `g` back to its skinned pose, settled; the next `step` publishes it. */
229
+ reset(g: number): void {
230
+ this.at(g).control.reset();
231
+ }
232
+
233
+ /**
234
+ * `dt` of the caller's time in whole fixed steps for every garment, and each garment's particles
235
+ * at its `alpha` published for the draws that follow, as the particles of `frame`.
236
+ */
237
+ step(dt: number, frame: number): void {
238
+ const garments = this.garments;
239
+ for (let g = 0; g < garments.length; g++) (garments[g] as Garment).control.advance(dt);
240
+ this.run();
241
+ const at = this.takeRound();
242
+ for (let g = 0; g < garments.length; g++) {
243
+ const base = g * PACE_WORDS;
244
+ const garment = garments[g] as Garment;
245
+ this.stagingWords.fill(0, base, base + PACE_HEAD_WORDS);
246
+ this.stagingFloats[base + 2] = garment.control.alpha;
247
+ this.stagingFloats.set(garment.wind, base + 28);
248
+ }
249
+ this.device.queue.writeBuffer(this.paces, this.paceOffset(at), this.staging);
250
+ for (let g = 0; g < garments.length; g++) {
251
+ const garment = garments[g] as Garment;
252
+ const view = garment.particles.publish(frame);
253
+ const slot = (this.batchSlot.publish + g) * DYNAMIC_ALIGNMENT;
254
+ const groups = Math.ceil(garment.range.count / CLOTH_WORKGROUP);
255
+ this.dispatch('clothPublish', at, slot, garment.outputs[view] as GPUBindGroup, groups);
256
+ }
257
+ this.flush();
258
+ }
259
+
260
+ /**
261
+ * Garment `g`'s particles where its last step left them, three floats each: for the parity check,
262
+ * which compares them with the CPU solver's positions. Not for a frame — it waits on the device.
263
+ */
264
+ async read(g: number): Promise<Float32Array> {
265
+ this.run();
266
+ this.flush();
267
+ const { particleBase, count } = this.at(g).range;
268
+ const bytes = count * 12;
269
+ const readback = this.device.createBuffer({
270
+ label: 'cloth.readback',
271
+ size: bytes,
272
+ usage: MAP_READ | COPY_DST,
273
+ });
274
+ const encoder = this.device.createCommandEncoder({ label: 'cloth.read' });
275
+ encoder.copyBufferToBuffer(this.state, particleBase * 12, readback, 0, bytes);
276
+ this.device.queue.submit([encoder.finish()]);
277
+ await readback.mapAsync(MAP_READ);
278
+ const out = new Float32Array(readback.getMappedRange().slice(0));
279
+ readback.unmap();
280
+ readback.destroy();
281
+ return out;
282
+ }
283
+
284
+ /** Its buffers; the particle textures are the renderer's to retire with the frame. */
285
+ dispose(): void {
286
+ this.flush();
287
+ for (const made of this.buffers) made.destroy();
288
+ }
289
+
290
+ private at(g: number): Garment {
291
+ const garment = this.garments[g];
292
+ if (garment === undefined) throw new Error(`skinned cloth set: no garment ${g}`);
293
+ return garment;
294
+ }
295
+
296
+ /** Garment `g`: its schedule over a device that queues, its particles and its own numbers. */
297
+ private garment(setup: SkinnedClothSetup, g: number): Garment {
298
+ const range = this.packed.garments[g] as ClothGarmentRange;
299
+ const particles = new GpuClothParticles(this.device, range.count, true);
300
+ const ops = new ClothOps();
301
+ const solver: ClothDevice = {
302
+ targetsChanged: (change) => this.targetsChanged(garment, change),
303
+ rest: () => this.queueOp(garment, CLOTH_OP_REST),
304
+ step: (fraction, blend) => this.queueStep(garment, fraction, blend),
305
+ };
306
+ const control = new ClothControl(setup, solver, false);
307
+ const output = (k: number): GPUBindGroup =>
308
+ this.device.createBindGroup({
309
+ label: 'cloth.publish',
310
+ layout: this.kernels.outputLayout,
311
+ entries: [{ binding: 0, resource: particles.viewOf(k) }],
312
+ });
313
+ const garment: Garment = {
314
+ range,
315
+ control,
316
+ particles,
317
+ ops,
318
+ wind: new Float32Array(control.parameters.wind),
319
+ outputs: [output(0), output(1)],
320
+ fromAt: 0,
321
+ toAt: 0,
322
+ recorded: false,
323
+ };
324
+ this.writeOwn(garment, g);
325
+ return garment;
326
+ }
327
+
328
+ /**
329
+ * A garment's own words of its record, which every round carries: its colliders, its texture's
330
+ * width and its constants. Its limits ride in the head beside `live`, written as it joins a round.
331
+ */
332
+ private writeOwn(garment: Garment, g: number): void {
333
+ const p = garment.control.parameters;
334
+ const h = p.step / p.substeps;
335
+ const base = g * PACE_WORDS;
336
+ const u = this.stagingWords;
337
+ const f = this.stagingFloats;
338
+ u[base + 32] = garment.range.colliderBase;
339
+ u[base + 33] = garment.range.colliders;
340
+ u[base + 34] = garment.particles.width;
341
+ f[base + 36] = exactExp(p.damping * h);
342
+ f[base + 37] = exactExp(p.drag * h);
343
+ f[base + 38] = p.maxDistanceScale;
344
+ f[base + 39] = p.margin;
345
+ f.set(p.gravity, base + 40);
346
+ }
347
+
348
+ /** Garments that cannot step together, refused by the first number they disagree on. */
349
+ private refuseStrangers(first: ClothControl['parameters']): void {
350
+ this.garments.forEach((garment, g) => {
351
+ const p = garment.control.parameters;
352
+ for (const name of ['step', 'substeps', 'iterations', 'maxSteps'] as const) {
353
+ if (p[name] !== first[name]) {
354
+ throw new Error(
355
+ `skinned cloth set: garment ${g} has ${name} ${p[name]} where garment 0 has ` +
356
+ `${first[name]}; a set steps its garments together, so they must agree`,
357
+ );
358
+ }
359
+ }
360
+ });
361
+ }
362
+
363
+ private targetsChanged(garment: Garment, change: 'posed' | 'settled' | 'consumed'): void {
364
+ if (change !== 'posed') {
365
+ garment.fromAt = garment.toAt;
366
+ return;
367
+ }
368
+ /*
369
+ * The pose's matrices and colliders are written ahead of the next submit, so any work of this
370
+ * garment's still waiting or recorded — a reset's settle steps, which read the colliders — is
371
+ * sent first, or it would run against this pose rather than its own.
372
+ */
373
+ if (garment.ops.pending > 0 || garment.recorded) {
374
+ this.run();
375
+ this.flush();
376
+ }
377
+ this.writeFrameData(garment);
378
+ garment.fromAt = garment.toAt;
379
+ garment.toAt = garment.toAt === 0 ? this.packed.count * 3 : 0;
380
+ this.queueOp(garment, CLOTH_OP_POSE);
381
+ }
382
+
383
+ private queueOp(garment: Garment, kind: number): number {
384
+ const at = garment.ops.push(kind);
385
+ const words = garment.ops.headWords;
386
+ words[at + 4] = garment.fromAt;
387
+ words[at + 5] = garment.toAt;
388
+ garment.ops.headFloats.set(garment.wind, at + 28);
389
+ return at;
390
+ }
391
+
392
+ private queueStep(garment: Garment, fraction: number, blend: number): void {
393
+ const at = this.queueOp(garment, CLOTH_OP_STEP);
394
+ const f = garment.ops.headFloats;
395
+ f[at] = fraction;
396
+ f[at + 1] = blend;
397
+ const { parameters, targets } = garment.control;
398
+ writeClothCarry(
399
+ parameters,
400
+ targets.modelBefore,
401
+ targets.modelNow,
402
+ garment.ops.headWords,
403
+ f,
404
+ at,
405
+ );
406
+ }
407
+
408
+ /** Every op every garment is waiting on, a round of one kind at a time, in each garment's order. */
409
+ private run(): void {
410
+ for (;;) {
411
+ let ran = false;
412
+ for (let k = 0; k < KINDS.length; k++) {
413
+ const kind = KINDS[k] as number;
414
+ if (this.waiting(kind)) {
415
+ this.runRound(kind);
416
+ ran = true;
417
+ }
418
+ }
419
+ if (!ran) return;
420
+ }
421
+ }
422
+
423
+ /** Whether any garment has `kind` next. */
424
+ private waiting(kind: number): boolean {
425
+ for (let g = 0; g < this.garments.length; g++) {
426
+ if ((this.garments[g] as Garment).ops.next === kind) return true;
427
+ }
428
+ return false;
429
+ }
430
+
431
+ /** One round: every garment with `kind` next takes part, and the rest sit it out. */
432
+ private runRound(kind: number): void {
433
+ const at = this.takeRound();
434
+ const words = this.stagingWords;
435
+ let blends = false;
436
+ for (let g = 0; g < this.garments.length; g++) {
437
+ const garment = this.garments[g] as Garment;
438
+ const base = g * PACE_WORDS;
439
+ words.fill(0, base, base + PACE_HEAD_WORDS);
440
+ if (garment.ops.next !== kind) continue;
441
+ garment.ops.take(words, base);
442
+ words[base + 6] = 1;
443
+ words[base + 7] = garment.range.limits;
444
+ garment.recorded = true;
445
+ if ((this.stagingFloats[base + 1] as number) > 0) blends = true;
446
+ }
447
+ this.device.queue.writeBuffer(this.paces, this.paceOffset(at), this.staging);
448
+ if (kind === CLOTH_OP_POSE) this.dispatch('clothPose', at, 0, null);
449
+ else if (kind === CLOTH_OP_REST) this.dispatch('clothRest', at, 0, null);
450
+ else this.stepRound(at, blends);
451
+ }
452
+
453
+ private stepRound(at: number, blends: boolean): void {
454
+ this.dispatch('clothBegin', at, 0, null);
455
+ const { distanceBatches, bendingBatches, tetherBatches } = this.packed;
456
+ for (let s = 0; s < this.substeps; s++) {
457
+ this.dispatch('clothPredict', at, 0, null);
458
+ for (let pass = 0; pass < this.iterations; pass++) {
459
+ const first = pass === 0 ? 1 : 0;
460
+ this.batches('clothDistance', distanceBatches, this.batchSlot.distance, first, at);
461
+ this.batches('clothBending', bendingBatches, this.batchSlot.bending, first, at);
462
+ this.batches('clothTether', tetherBatches, this.batchSlot.tether, first, at);
463
+ }
464
+ this.dispatch('clothLimit', at, 0, null);
465
+ this.dispatch('clothFinish', at, 0, null);
466
+ }
467
+ if (blends) this.dispatch('clothBlend', at, 0, null);
468
+ }
469
+
470
+ /** Every colour of one kind across the set, in colour order, each a dispatch over its range. */
471
+ private batches(
472
+ entry: ClothEntry,
473
+ batches: Uint32Array,
474
+ firstSlot: number,
475
+ first: number,
476
+ roundAt: number,
477
+ ): void {
478
+ for (let b = 0; b + 1 < batches.length; b++) {
479
+ const size = (batches[b + 1] as number) - (batches[b] as number);
480
+ if (size === 0) continue;
481
+ const slot = firstSlot + b * 2 + first;
482
+ this.dispatch(
483
+ entry,
484
+ roundAt,
485
+ slot * DYNAMIC_ALIGNMENT,
486
+ null,
487
+ Math.ceil(size / CLOTH_WORKGROUP),
488
+ );
489
+ }
490
+ }
491
+
492
+ private dispatch(
493
+ entry: ClothEntry,
494
+ roundAt: number,
495
+ batchAt: number,
496
+ output: GPUBindGroup | null,
497
+ groups = this.groups,
498
+ ): void {
499
+ const pass = this.passFor();
500
+ pass.setPipeline(this.kernels.pipelines[entry]);
501
+ pass.setBindGroup(0, this.group, [roundAt, batchAt]);
502
+ if (output !== null) pass.setBindGroup(1, output);
503
+ pass.dispatchWorkgroups(groups);
504
+ }
505
+
506
+ private passFor(): GPUComputePassEncoder {
507
+ if (this.pass === null) {
508
+ this.encoder = this.device.createCommandEncoder({ label: 'cloth' });
509
+ this.pass = this.encoder.beginComputePass({ label: 'cloth' });
510
+ }
511
+ return this.pass;
512
+ }
513
+
514
+ /** A round's slot, sending what is recorded first where the slots are spent: its byte offset. */
515
+ private takeRound(): number {
516
+ if (this.round === ROUNDS) this.flush();
517
+ return this.round++ * DYNAMIC_ALIGNMENT;
518
+ }
519
+
520
+ /** Where the records of the round at `roundAt` sit in `paces`. */
521
+ private paceOffset(roundAt: number): number {
522
+ return (roundAt / DYNAMIC_ALIGNMENT) * this.staging.byteLength;
523
+ }
524
+
525
+ /** Send what has been recorded: every slot it reads was written ahead of it on the queue. */
526
+ private flush(): void {
527
+ const { pass, encoder } = this;
528
+ if (pass === null || encoder === null) return;
529
+ pass.end();
530
+ this.device.queue.submit([encoder.finish()]);
531
+ this.pass = null;
532
+ this.encoder = null;
533
+ this.round = 0;
534
+ for (let g = 0; g < this.garments.length; g++) (this.garments[g] as Garment).recorded = false;
535
+ }
536
+
537
+ /** A garment's skin matrices — or its model, with no rig — and its colliders, into the set's. */
538
+ private writeFrameData(garment: Garment): void {
539
+ const targets = garment.control.targets;
540
+ const { jointBase, joints, colliderBase, colliders, rigged } = garment.range;
541
+ const queue = this.device.queue;
542
+ const matrices = (
543
+ rigged && targets.skin !== null ? targets.skin : targets.model
544
+ ) as Float32Array<ArrayBuffer>;
545
+ queue.writeBuffer(this.frameData, jointBase * 64, matrices, 0, joints * 16);
546
+ if (colliders === 0) return;
547
+ const ends = this.packed.joints * 16;
548
+ const radii = ends + this.packed.colliders * 6;
549
+ queue.writeBuffer(
550
+ this.frameData,
551
+ (ends + colliderBase * 6) * 4,
552
+ targets.colliderEnds as Float32Array<ArrayBuffer>,
553
+ 0,
554
+ colliders * 6,
555
+ );
556
+ queue.writeBuffer(
557
+ this.frameData,
558
+ (radii + colliderBase * 2) * 4,
559
+ targets.colliderRadii as Float32Array<ArrayBuffer>,
560
+ 0,
561
+ colliders * 2,
562
+ );
563
+ }
564
+ }
@@ -4,6 +4,8 @@ import { destroySlot } from './instanceCullPass.ts';
4
4
  import type { CullSlot } from './instanceCullPass.ts';
5
5
  import type { MeshInstances } from '../../instances.ts';
6
6
  import type { GpuMesh } from './buffers.ts';
7
+ import { packInstanceClocks } from '../../boneAnimation.ts';
8
+ import { GpuInstanceClocks, type GpuBoneAnimation } from './boneAnimations.ts';
7
9
 
8
10
  /** VERTEX | COPY_DST, spelled the way every other pass here spells it. */
9
11
  const USAGE_VERTEX = 0x0020 | 0x0008;
@@ -36,10 +38,34 @@ export class GpuInstancedBatch {
36
38
  slotFrame = -1;
37
39
  slotsTaken = 0;
38
40
 
39
- constructor(device: GPUDevice, mesh: GpuMesh, capacity: number, label: string, cull = false) {
41
+ /** The bone animation every instance plays, and its clocks; null for a batch that plays none. */
42
+ readonly animation: GpuBoneAnimation | null;
43
+ readonly clocks: GpuInstanceClocks | null;
44
+ /**
45
+ * An animated batch's own groups — its lit twins by the material group they were built beside, its
46
+ * shadow groups by what they cut with — and the renderer's group epoch they were built in, after
47
+ * which every one is dropped. See `WebGPURenderer.animatedGroup`.
48
+ */
49
+ readonly litGroups = new Map<GPUBindGroup, GPUBindGroup>();
50
+ readonly depthGroups = new Map<unknown, GPUBindGroup>();
51
+ readonly depthPeelGroups = new Map<unknown, GPUBindGroup>();
52
+ groupEpoch = -1;
53
+
54
+ constructor(
55
+ device: GPUDevice,
56
+ mesh: GpuMesh,
57
+ capacity: number,
58
+ label: string,
59
+ cull = false,
60
+ animation: GpuBoneAnimation | null = null,
61
+ ) {
40
62
  this.mesh = mesh;
41
63
  this.capacity = capacity;
64
+ /* An animated batch is culled whole and never by instance: see `submitInstanced`. */
42
65
  this.cull = cull;
66
+ this.animation = animation;
67
+ this.clocks =
68
+ animation === null ? null : new GpuInstanceClocks(device, capacity, `${label}.clocks`);
43
69
  this.staging = new Float32Array(capacity * INSTANCE_FLOATS);
44
70
  this.buffer = device.createBuffer({
45
71
  /*
@@ -83,11 +109,16 @@ export class GpuInstancedBatch {
83
109
  if (count === 0) return;
84
110
  packInstances(data, this.staging);
85
111
  queue.writeBuffer(this.buffer, 0, this.staging, 0, count * INSTANCE_FLOATS);
112
+ if (this.clocks !== null) {
113
+ packInstanceClocks(data, this.clocks.staging);
114
+ this.clocks.upload(queue, count);
115
+ }
86
116
  }
87
117
 
88
118
  dispose(): void {
89
119
  this.previousBuffer?.destroy();
90
120
  this.buffer.destroy();
121
+ this.clocks?.dispose();
91
122
  for (const slot of this.slots) destroySlot(slot);
92
123
  }
93
124
  }
@@ -126,6 +126,7 @@ export class LatePass {
126
126
  command.offsetCount = 0;
127
127
  command.vertexCount = 0;
128
128
  command.indexBuffer = null;
129
+ command.indexFormat = 'uint32';
129
130
  command.indexed = false;
130
131
  command.instances = 1;
131
132
  command.indirect = null;