@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
@@ -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
+ }
@@ -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;
@@ -254,6 +254,8 @@ export class MotionPass {
254
254
  readonly weights: number;
255
255
  readonly joints2: number;
256
256
  readonly weights2: number;
257
+ readonly weightsFormat: GPUVertexFormat;
258
+ readonly weights2Format: GPUVertexFormat;
257
259
  },
258
260
  cloth = false,
259
261
  ): GPURenderPipeline | null {
@@ -261,13 +263,18 @@ export class MotionPass {
261
263
  const eight = offsets.joints2 >= 0 && offsets.weights2 >= 0;
262
264
  /*
263
265
  * Five byte counts, each under 256 because a vertex is at most 172 bytes, as five base-256
264
- * digits: exact below 2^40, so two layouts can never share a key.
266
+ * digits, and two bits for the weights' formats: exact below 2^42, so two layouts can never
267
+ * share a key.
265
268
  */
266
269
  const key =
267
- (((stride * 256 + offsets.joints) * 256 + offsets.weights) * 256 + offsets.joints2 + 1) *
270
+ ((((stride * 256 + offsets.joints) * 256 + offsets.weights) * 256 + offsets.joints2 + 1) *
268
271
  256 +
269
- offsets.weights2 +
270
- 1;
272
+ offsets.weights2 +
273
+ 1) *
274
+ 4 +
275
+ /* And whether each four weights are floats or fixed point, two bits: `vertexPacking.ts`. */
276
+ (offsets.weightsFormat === 'float32x4' ? 0 : 2) +
277
+ (offsets.weights2Format === 'float32x4' ? 0 : 1);
271
278
  const pipelines = cloth ? this.clothPipelines : this.skinnedPipelines;
272
279
  const held = pipelines.get(key);
273
280
  if (held !== undefined) return held;
@@ -287,11 +294,15 @@ export class MotionPass {
287
294
  attributes: [
288
295
  { shaderLocation: 0, offset: 0, format: 'float32x3' },
289
296
  { shaderLocation: 11, offset: offsets.joints, format: 'float32x4' },
290
- { shaderLocation: 12, offset: offsets.weights, format: 'float32x4' },
297
+ { shaderLocation: 12, offset: offsets.weights, format: offsets.weightsFormat },
291
298
  ...(eight
292
299
  ? ([
293
300
  { shaderLocation: 14, offset: offsets.joints2, format: 'float32x4' },
294
- { shaderLocation: 15, offset: offsets.weights2, format: 'float32x4' },
301
+ {
302
+ shaderLocation: 15,
303
+ offset: offsets.weights2,
304
+ format: offsets.weights2Format,
305
+ },
295
306
  ] as const)
296
307
  : []),
297
308
  ],
@@ -635,7 +646,7 @@ export class MotionPass {
635
646
  }
636
647
  pass.setBindGroup(0, this.group, [i * MOTION_DRAW_STRIDE]);
637
648
  pass.setVertexBuffer(0, mesh.vertexBuffers[0] as GPUBuffer);
638
- pass.setIndexBuffer(mesh.indexBuffer, 'uint32');
649
+ pass.setIndexBuffer(mesh.indexBuffer, mesh.indexFormat);
639
650
  pass.drawIndexed(mesh.indexCount, instances);
640
651
  }
641
652
  }