@driftengine/core 4.8.3 → 4.8.4

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 (178) hide show
  1. package/CHANGELOG.json +71 -0
  2. package/README.md +1 -1
  3. package/dist/geometry/meshConcat.js +2 -0
  4. package/dist/index.d.ts +10 -4
  5. package/dist/index.js +16 -1
  6. package/dist/math/scalar.js +1 -1
  7. package/dist/render/anisotropicLobe.d.ts +7 -0
  8. package/dist/render/anisotropicLobe.js +33 -0
  9. package/dist/render/backend/api.d.ts +41 -4
  10. package/dist/render/backend/select.js +8 -0
  11. package/dist/render/backend/webgl2/clothTextures.d.ts +27 -0
  12. package/dist/render/backend/webgl2/clothTextures.js +93 -0
  13. package/dist/render/backend/webgl2/renderer.d.ts +136 -3
  14. package/dist/render/backend/webgl2/renderer.js +604 -63
  15. package/dist/render/backend/webgl2/skinScatter.d.ts +50 -0
  16. package/dist/render/backend/webgl2/skinScatter.js +256 -0
  17. package/dist/render/backend/webgpu/buffers.d.ts +25 -11
  18. package/dist/render/backend/webgpu/buffers.js +98 -49
  19. package/dist/render/backend/webgpu/clothSolvePack.d.ts +27 -0
  20. package/dist/render/backend/webgpu/clothSolvePack.js +129 -0
  21. package/dist/render/backend/webgpu/clothTextures.d.ts +68 -0
  22. package/dist/render/backend/webgpu/clothTextures.js +145 -0
  23. package/dist/render/backend/webgpu/compressedUpload.d.ts +18 -0
  24. package/dist/render/backend/webgpu/compressedUpload.js +38 -0
  25. package/dist/render/backend/webgpu/computeHost.d.ts +21 -0
  26. package/dist/render/backend/webgpu/computeHost.js +5 -0
  27. package/dist/render/backend/webgpu/depthPass.d.ts +16 -6
  28. package/dist/render/backend/webgpu/depthPass.js +39 -9
  29. package/dist/render/backend/webgpu/drawCommand.js +6 -2
  30. package/dist/render/backend/webgpu/flatPass.d.ts +38 -4
  31. package/dist/render/backend/webgpu/flatPass.js +109 -12
  32. package/dist/render/backend/webgpu/gpuSkinnedCloth.d.ts +74 -0
  33. package/dist/render/backend/webgpu/gpuSkinnedCloth.js +452 -0
  34. package/dist/render/backend/webgpu/motionPass.d.ts +20 -0
  35. package/dist/render/backend/webgpu/motionPass.js +106 -9
  36. package/dist/render/backend/webgpu/postPass.d.ts +3 -1
  37. package/dist/render/backend/webgpu/postPass.js +6 -3
  38. package/dist/render/backend/webgpu/renderer.d.ts +113 -5
  39. package/dist/render/backend/webgpu/renderer.js +498 -48
  40. package/dist/render/backend/webgpu/shaders/clothSolve.wgsl.d.ts +33 -0
  41. package/dist/render/backend/webgpu/shaders/clothSolve.wgsl.js +469 -0
  42. package/dist/render/backend/webgpu/skinScatterPass.d.ts +51 -0
  43. package/dist/render/backend/webgpu/skinScatterPass.js +253 -0
  44. package/dist/render/backend/webgpu/surfaceTexturePass.d.ts +4 -1
  45. package/dist/render/backend/webgpu/surfaceTexturePass.js +37 -12
  46. package/dist/render/clothBindingData.d.ts +41 -0
  47. package/dist/render/clothBindingData.js +72 -0
  48. package/dist/render/clothPlacement.d.ts +25 -0
  49. package/dist/render/clothPlacement.js +85 -0
  50. package/dist/render/compressedSource.d.ts +66 -0
  51. package/dist/render/compressedSource.js +138 -0
  52. package/dist/render/cutoutCaster.d.ts +2 -0
  53. package/dist/render/cutoutCaster.js +2 -0
  54. package/dist/render/cutoutDither.d.ts +51 -0
  55. package/dist/render/cutoutDither.js +90 -0
  56. package/dist/render/eyeRefraction.d.ts +35 -0
  57. package/dist/render/eyeRefraction.js +40 -0
  58. package/dist/render/glCompressed.d.ts +27 -0
  59. package/dist/render/glCompressed.js +53 -0
  60. package/dist/render/hairLobes.d.ts +70 -0
  61. package/dist/render/hairLobes.js +120 -0
  62. package/dist/render/lightBudget.d.ts +22 -0
  63. package/dist/render/lightBudget.js +22 -0
  64. package/dist/render/mesh.d.ts +7 -0
  65. package/dist/render/mesh.js +24 -0
  66. package/dist/render/recon/motionVectors.d.ts +4 -2
  67. package/dist/render/recon/motionVectors.js +6 -3
  68. package/dist/render/renderQuality.d.ts +20 -0
  69. package/dist/render/renderQuality.js +5 -0
  70. package/dist/render/rendererHarness.js +4 -0
  71. package/dist/render/sceneTarget.d.ts +12 -0
  72. package/dist/render/sceneTarget.js +14 -0
  73. package/dist/render/shaders/clothBinding.d.ts +22 -0
  74. package/dist/render/shaders/clothBinding.js +80 -0
  75. package/dist/render/shaders/depth.js +42 -6
  76. package/dist/render/shaders/flat/anisotropic.d.ts +15 -0
  77. package/dist/render/shaders/flat/anisotropic.js +68 -0
  78. package/dist/render/shaders/flat/eye.d.ts +1 -0
  79. package/dist/render/shaders/flat/eye.js +81 -0
  80. package/dist/render/shaders/flat/hair.d.ts +1 -0
  81. package/dist/render/shaders/flat/hair.js +101 -0
  82. package/dist/render/shaders/flat/index.js +36 -2
  83. package/dist/render/shaders/flat/main.d.ts +1 -1
  84. package/dist/render/shaders/flat/main.js +113 -17
  85. package/dist/render/shaders/flat/models.d.ts +14 -0
  86. package/dist/render/shaders/flat/models.js +273 -0
  87. package/dist/render/shaders/flat/preamble.js +26 -24
  88. package/dist/render/shaders/flat/skin.d.ts +1 -0
  89. package/dist/render/shaders/flat/skin.js +114 -0
  90. package/dist/render/shaders/generated/depth.wgsl.d.ts +38 -16
  91. package/dist/render/shaders/generated/depth.wgsl.js +38 -16
  92. package/dist/render/shaders/generated/flat.wgsl.d.ts +1790 -1704
  93. package/dist/render/shaders/generated/flat.wgsl.js +1828 -1742
  94. package/dist/render/shaders/generated/skinBlur.wgsl.d.ts +48 -0
  95. package/dist/render/shaders/generated/skinBlur.wgsl.js +54 -0
  96. package/dist/render/shaders/recon/motion.wgsl.d.ts +1 -1
  97. package/dist/render/shaders/recon/motion.wgsl.js +102 -0
  98. package/dist/render/shaders/skinBlur.d.ts +1 -0
  99. package/dist/render/shaders/skinBlur.js +103 -0
  100. package/dist/render/shaders/skinning.d.ts +3 -1
  101. package/dist/render/shaders/skinning.js +31 -4
  102. package/dist/render/shadowCasters.d.ts +13 -1
  103. package/dist/render/skinBlur.d.ts +41 -0
  104. package/dist/render/skinBlur.js +49 -0
  105. package/dist/render/skinScatter.d.ts +67 -0
  106. package/dist/render/skinScatter.js +80 -0
  107. package/dist/render/skinnedClothRun.d.ts +26 -0
  108. package/dist/render/skinnedClothRun.js +84 -0
  109. package/dist/render/surfaceModel.d.ts +129 -0
  110. package/dist/render/surfaceModel.js +152 -0
  111. package/dist/render/surfaceTexture.d.ts +44 -4
  112. package/dist/render/surfaceTexture.js +60 -46
  113. package/dist/render/vertexDefaults.js +7 -0
  114. package/package.json +6 -6
  115. package/src/geometry/meshConcat.ts +2 -0
  116. package/src/index.ts +40 -1
  117. package/src/math/scalar.ts +1 -1
  118. package/src/render/anisotropicLobe.ts +41 -0
  119. package/src/render/backend/api.ts +52 -3
  120. package/src/render/backend/select.ts +8 -0
  121. package/src/render/backend/webgl2/clothTextures.ts +120 -0
  122. package/src/render/backend/webgl2/renderer.ts +693 -64
  123. package/src/render/backend/webgl2/skinScatter.ts +309 -0
  124. package/src/render/backend/webgpu/buffers.ts +137 -63
  125. package/src/render/backend/webgpu/clothSolvePack.ts +161 -0
  126. package/src/render/backend/webgpu/clothTextures.ts +194 -0
  127. package/src/render/backend/webgpu/compressedUpload.ts +65 -0
  128. package/src/render/backend/webgpu/computeHost.ts +25 -0
  129. package/src/render/backend/webgpu/depthPass.ts +39 -7
  130. package/src/render/backend/webgpu/drawCommand.ts +6 -2
  131. package/src/render/backend/webgpu/flatPass.ts +145 -7
  132. package/src/render/backend/webgpu/gpuSkinnedCloth.ts +502 -0
  133. package/src/render/backend/webgpu/motionPass.ts +128 -9
  134. package/src/render/backend/webgpu/postPass.ts +5 -1
  135. package/src/render/backend/webgpu/renderer.ts +663 -43
  136. package/src/render/backend/webgpu/shaders/clothSolve.wgsl.ts +470 -0
  137. package/src/render/backend/webgpu/skinScatterPass.ts +309 -0
  138. package/src/render/backend/webgpu/surfaceTexturePass.ts +42 -12
  139. package/src/render/clothBindingData.ts +89 -0
  140. package/src/render/clothPlacement.ts +114 -0
  141. package/src/render/compressedSource.ts +198 -0
  142. package/src/render/cutoutCaster.ts +5 -0
  143. package/src/render/cutoutDither.ts +99 -0
  144. package/src/render/eyeRefraction.ts +72 -0
  145. package/src/render/glCompressed.ts +86 -0
  146. package/src/render/hairLobes.ts +188 -0
  147. package/src/render/lightBudget.ts +25 -0
  148. package/src/render/mesh.ts +23 -0
  149. package/src/render/recon/motionVectors.ts +6 -3
  150. package/src/render/renderQuality.ts +28 -0
  151. package/src/render/rendererHarness.ts +4 -0
  152. package/src/render/sceneTarget.ts +21 -0
  153. package/src/render/shaders/clothBinding.ts +81 -0
  154. package/src/render/shaders/depth.ts +42 -6
  155. package/src/render/shaders/flat/anisotropic.ts +68 -0
  156. package/src/render/shaders/flat/eye.ts +82 -0
  157. package/src/render/shaders/flat/hair.ts +102 -0
  158. package/src/render/shaders/flat/index.ts +36 -2
  159. package/src/render/shaders/flat/main.ts +113 -17
  160. package/src/render/shaders/flat/models.ts +286 -0
  161. package/src/render/shaders/flat/preamble.ts +26 -24
  162. package/src/render/shaders/flat/skin.ts +116 -0
  163. package/src/render/shaders/generated/depth.wgsl.ts +38 -16
  164. package/src/render/shaders/generated/flat.wgsl.ts +1828 -1742
  165. package/src/render/shaders/generated/skinBlur.wgsl.ts +56 -0
  166. package/src/render/shaders/recon/motion.wgsl.ts +102 -0
  167. package/src/render/shaders/skinBlur.ts +105 -0
  168. package/src/render/shaders/skinning.ts +31 -4
  169. package/src/render/shadowCasters.ts +13 -0
  170. package/src/render/skinBlur.ts +53 -0
  171. package/src/render/skinScatter.ts +92 -0
  172. package/src/render/skinnedClothRun.ts +128 -0
  173. package/src/render/surfaceModel.ts +274 -0
  174. package/src/render/surfaceTexture.ts +123 -63
  175. package/src/render/vertexDefaults.ts +7 -0
  176. package/dist/math/exact.d.ts +0 -76
  177. package/dist/math/exact.js +0 -327
  178. package/src/math/exact.ts +0 -340
@@ -0,0 +1,452 @@
1
+ /**
2
+ * A skinned cloth solved on the device: `ClothControl`'s schedule, over buffers rather than arrays.
3
+ *
4
+ * **The CPU solver's steps, as dispatches.** A step is `clothBegin`, then per substep
5
+ * `clothPredict`, every batch of every kind of constraint per iteration, `clothLimit` and
6
+ * `clothFinish`; then `clothBlend` while a reset blends back in. The particles never leave the
7
+ * device: a frame ends with `clothPublish` writing them, at the frame's `alpha`, into the particle
8
+ * texture the bound mesh's vertex stage reads — so a garment costs no upload a frame at all.
9
+ *
10
+ * **One submit a frame, everything it needs written before it.** `queue.writeBuffer` lands ahead of
11
+ * the whole submit rather than between two dispatches, which is the trap `AGENTS.md` (2026-08-27)
12
+ * records for draws and is no different for dispatches. So every step takes a slot of its own, at a
13
+ * dynamic offset, and the batches' ranges are slots written once; a frame that runs out of slots
14
+ * submits what it has first and goes on, and so does a pose, whose matrices are one buffer.
15
+ *
16
+ * **What it gives up**: single precision, and a dispatch per batch — a garment of nine colours and
17
+ * four iterations is about forty dispatches a step, which is cheap on a desktop part and is what the
18
+ * budget in `demo/dev/skinnedCloth.html` measures. Self-collision is accepted and not simulated, as on
19
+ * the CPU.
20
+ */
21
+ import { ClothControl, exactExp, validateClothSetup } from '@driftengine/physics';
22
+ import { packCloth } from './clothSolvePack.js';
23
+ import { GpuClothParticles } from './clothTextures.js';
24
+ import { CLOTH_SOLVE_WGSL, CLOTH_WORKGROUP } from './shaders/clothSolve.wgsl.js';
25
+ import { shaderModule } from './shaderModules.js';
26
+ import { DYNAMIC_ALIGNMENT } from './uniformRing.js';
27
+ const STORAGE = 0x80;
28
+ const UNIFORM = 0x40;
29
+ const COPY_DST = 0x8;
30
+ const COPY_SRC = 0x4;
31
+ const MAP_READ = 0x1;
32
+ const COMPUTE = 0x4;
33
+ /** Step slots a submit holds before it is sent and the ring starts again. */
34
+ const STEP_SLOTS = 64;
35
+ /** Floats in a step's slot that are written: see `Step` in the shader. */
36
+ const STEP_FLOATS = 32;
37
+ const CONSTANT_BYTES = 80;
38
+ const ENTRIES = [
39
+ 'clothPose',
40
+ 'clothRest',
41
+ 'clothBegin',
42
+ 'clothPredict',
43
+ 'clothDistance',
44
+ 'clothBending',
45
+ 'clothTether',
46
+ 'clothLimit',
47
+ 'clothFinish',
48
+ 'clothBlend',
49
+ 'clothPublish',
50
+ ];
51
+ const kernelsByDevice = new WeakMap();
52
+ function kernelsFor(device) {
53
+ const held = kernelsByDevice.get(device);
54
+ if (held !== undefined)
55
+ return held;
56
+ const uniform = (binding, dynamic) => ({
57
+ binding,
58
+ visibility: COMPUTE,
59
+ buffer: { type: 'uniform', hasDynamicOffset: dynamic },
60
+ });
61
+ const storage = (binding, type) => ({
62
+ binding,
63
+ visibility: COMPUTE,
64
+ buffer: { type },
65
+ });
66
+ const layout = device.createBindGroupLayout({
67
+ label: 'cloth.solve',
68
+ entries: [
69
+ uniform(0, false),
70
+ uniform(1, true),
71
+ uniform(2, true),
72
+ storage(3, 'storage'),
73
+ storage(4, 'storage'),
74
+ storage(5, 'read-only-storage'),
75
+ storage(6, 'read-only-storage'),
76
+ storage(7, 'storage'),
77
+ storage(8, 'read-only-storage'),
78
+ ],
79
+ });
80
+ const outputLayout = device.createBindGroupLayout({
81
+ label: 'cloth.publish',
82
+ entries: [
83
+ {
84
+ binding: 0,
85
+ visibility: COMPUTE,
86
+ storageTexture: { access: 'write-only', format: 'rgba32float' },
87
+ },
88
+ ],
89
+ });
90
+ const module = shaderModule(device, { label: 'cloth.solve', code: CLOTH_SOLVE_WGSL });
91
+ const solve = device.createPipelineLayout({ bindGroupLayouts: [layout] });
92
+ const publish = device.createPipelineLayout({ bindGroupLayouts: [layout, outputLayout] });
93
+ const pipelines = {};
94
+ for (const entry of ENTRIES) {
95
+ pipelines[entry] = device.createComputePipeline({
96
+ label: `cloth.${entry}`,
97
+ layout: entry === 'clothPublish' ? publish : solve,
98
+ compute: { module, entryPoint: entry },
99
+ });
100
+ }
101
+ const kernels = { layout, outputLayout, pipelines };
102
+ kernelsByDevice.set(device, kernels);
103
+ return kernels;
104
+ }
105
+ /** One garment's solver on the device, and the particle textures it publishes into. */
106
+ export class GpuSkinnedCloth {
107
+ device;
108
+ /** The particles a bound mesh draws by: see `RendererApi.setCloth`. */
109
+ particles;
110
+ count;
111
+ control;
112
+ packed;
113
+ kernels;
114
+ buffers = [];
115
+ state;
116
+ frameData;
117
+ stepSlots;
118
+ group;
119
+ outputs;
120
+ frameStaging;
121
+ stepStaging = new ArrayBuffer(STEP_FLOATS * 4);
122
+ stepFloats = new Float32Array(this.stepStaging);
123
+ stepWords = new Uint32Array(this.stepStaging);
124
+ /** Each batch's slot, by kind and by whether it opens a substep's iterations. */
125
+ batchSlot;
126
+ /** The air the steps drag toward: the set-up's until `setWind`, written into every step. */
127
+ wind = new Float32Array(3);
128
+ groups;
129
+ substeps;
130
+ iterations;
131
+ encoder = null;
132
+ pass = null;
133
+ slot = 0;
134
+ /** Where the pose a step starts from and the one it moves toward sit in `pose`, in floats. */
135
+ fromAt = 0;
136
+ toAt = 0;
137
+ constructor(device, setup) {
138
+ this.device = device;
139
+ validateClothSetup(setup);
140
+ const packed = packCloth(setup);
141
+ this.packed = packed;
142
+ const n = packed.count;
143
+ this.count = n;
144
+ this.kernels = kernelsFor(device);
145
+ this.particles = new GpuClothParticles(device, n, true);
146
+ this.groups = Math.ceil(n / CLOTH_WORKGROUP);
147
+ const solver = {
148
+ targetsChanged: (change) => {
149
+ this.targetsChanged(change);
150
+ },
151
+ rest: () => {
152
+ this.rest();
153
+ },
154
+ step: (fraction, blend) => {
155
+ this.step(fraction, blend);
156
+ },
157
+ };
158
+ this.control = new ClothControl(setup, solver, false);
159
+ const parameters = this.control.parameters;
160
+ this.wind.set(parameters.wind);
161
+ this.substeps = parameters.substeps;
162
+ this.iterations = parameters.iterations;
163
+ const buffer = (label, bytes, usage) => {
164
+ const made = device.createBuffer({
165
+ label: `cloth.${label}`,
166
+ size: Math.max(16, bytes),
167
+ usage,
168
+ });
169
+ this.buffers.push(made);
170
+ return made;
171
+ };
172
+ const constants = buffer('constants', CONSTANT_BYTES, UNIFORM | COPY_DST);
173
+ this.stepSlots = buffer('steps', STEP_SLOTS * DYNAMIC_ALIGNMENT, UNIFORM | COPY_DST);
174
+ const batchFloats = this.writeBatches();
175
+ const batches = buffer('batches', batchFloats.byteLength, UNIFORM | COPY_DST);
176
+ this.state = buffer('state', n * 12 * 4, STORAGE | COPY_DST | COPY_SRC);
177
+ const pose = buffer('pose', n * 9 * 4, STORAGE);
178
+ const statics = buffer('statics', packed.statics.byteLength, STORAGE | COPY_DST);
179
+ const constraints = buffer('constraints', packed.constraints.byteLength, STORAGE | COPY_DST);
180
+ const lambda = buffer('lambda', (packed.distances + packed.bendings) * 4, STORAGE);
181
+ this.frameStaging = new Float32Array(packed.joints * 16 + packed.colliders * 8);
182
+ this.frameData = buffer('frame', this.frameStaging.byteLength, STORAGE | COPY_DST);
183
+ const queue = device.queue;
184
+ queue.writeBuffer(constants, 0, this.constants());
185
+ queue.writeBuffer(batches, 0, batchFloats);
186
+ queue.writeBuffer(statics, 0, packed.statics);
187
+ if (packed.constraints.byteLength > 0) {
188
+ queue.writeBuffer(constraints, 0, packed.constraints);
189
+ }
190
+ /* The rest pose until the first `setPose`, which resets the cloth to its own. */
191
+ const state = new Float32Array(n * 12);
192
+ state.set(setup.positions, 0);
193
+ state.set(setup.positions, n * 9);
194
+ queue.writeBuffer(this.state, 0, state);
195
+ const slot = (bufferOf) => ({
196
+ buffer: bufferOf,
197
+ size: DYNAMIC_ALIGNMENT,
198
+ });
199
+ this.group = device.createBindGroup({
200
+ label: 'cloth.solve',
201
+ layout: this.kernels.layout,
202
+ entries: [
203
+ { binding: 0, resource: { buffer: constants } },
204
+ { binding: 1, resource: slot(this.stepSlots) },
205
+ { binding: 2, resource: slot(batches) },
206
+ { binding: 3, resource: { buffer: this.state } },
207
+ { binding: 4, resource: { buffer: pose } },
208
+ { binding: 5, resource: { buffer: statics } },
209
+ { binding: 6, resource: { buffer: constraints } },
210
+ { binding: 7, resource: { buffer: lambda } },
211
+ { binding: 8, resource: { buffer: this.frameData } },
212
+ ],
213
+ });
214
+ const output = (k) => device.createBindGroup({
215
+ label: 'cloth.publish',
216
+ layout: this.kernels.outputLayout,
217
+ entries: [{ binding: 0, resource: this.particles.viewOf(k) }],
218
+ });
219
+ this.outputs = [output(0), output(1)];
220
+ this.batchSlot = {
221
+ distance: 0,
222
+ bending: (packed.distanceBatches.length - 1) * 2,
223
+ tether: (packed.distanceBatches.length - 1 + packed.bendingBatches.length - 1) * 2,
224
+ };
225
+ }
226
+ /**
227
+ * One frame: the rig's pose, `dt` of the caller's time in whole fixed steps, and the particles at
228
+ * the frame's `alpha` published for the draws that follow, as the particles of `frame`.
229
+ */
230
+ advance(globals, model, dt, frame) {
231
+ this.control.setPose(globals, model);
232
+ this.control.advance(dt);
233
+ const view = this.particles.publish(frame);
234
+ this.dispatch('clothPublish', this.takeSlot(0, 0, this.control.alpha, null), 0, view);
235
+ this.flush();
236
+ }
237
+ /** The frame's sample of the scene's wind, for the steps to come. See `SkinnedCloth.setWind`. */
238
+ setWind(x, y, z) {
239
+ this.wind[0] = x;
240
+ this.wind[1] = y;
241
+ this.wind[2] = z;
242
+ }
243
+ /** Back to the skinned pose, settled; the next `advance` publishes it. */
244
+ reset() {
245
+ this.control.reset();
246
+ }
247
+ /**
248
+ * The particles where the last step left them, read back: for the parity check, which compares
249
+ * them with the CPU solver's. Not for a frame — it waits on the device.
250
+ */
251
+ async read() {
252
+ this.flush();
253
+ const bytes = this.count * 12;
254
+ const readback = this.device.createBuffer({
255
+ label: 'cloth.readback',
256
+ size: bytes,
257
+ usage: MAP_READ | COPY_DST,
258
+ });
259
+ const encoder = this.device.createCommandEncoder({ label: 'cloth.read' });
260
+ encoder.copyBufferToBuffer(this.state, 0, readback, 0, bytes);
261
+ this.device.queue.submit([encoder.finish()]);
262
+ await readback.mapAsync(MAP_READ);
263
+ const out = new Float32Array(readback.getMappedRange().slice(0));
264
+ readback.unmap();
265
+ readback.destroy();
266
+ return out;
267
+ }
268
+ /** Its buffers; the particle textures are the renderer's to retire with the frame. */
269
+ dispose() {
270
+ this.flush();
271
+ for (const made of this.buffers)
272
+ made.destroy();
273
+ }
274
+ targetsChanged(change) {
275
+ if (change !== 'posed') {
276
+ this.fromAt = this.toAt;
277
+ return;
278
+ }
279
+ /*
280
+ * The pose's matrices and colliders are written ahead of the next submit, so anything already
281
+ * recorded — a reset's settle steps, which read the colliders — is sent first, or it would run
282
+ * against this pose rather than its own.
283
+ */
284
+ this.flush();
285
+ this.writeFrameData();
286
+ this.fromAt = this.toAt;
287
+ this.toAt = this.toAt === 0 ? this.count * 3 : 0;
288
+ this.dispatch('clothPose', this.takeSlot(0, 0, 0, null), 0, -1);
289
+ }
290
+ rest() {
291
+ this.dispatch('clothRest', this.takeSlot(1, 0, 0, null), 0, -1);
292
+ }
293
+ step(fraction, blend) {
294
+ const at = this.takeSlot(fraction, blend, 0, this.control.targets);
295
+ this.dispatch('clothBegin', at, 0, -1);
296
+ const { distanceBatches, bendingBatches, tetherBatches } = this.packed;
297
+ for (let s = 0; s < this.substeps; s++) {
298
+ this.dispatch('clothPredict', at, 0, -1);
299
+ for (let pass = 0; pass < this.iterations; pass++) {
300
+ const first = pass === 0 ? 1 : 0;
301
+ this.batches('clothDistance', distanceBatches, this.batchSlot.distance, first, at);
302
+ this.batches('clothBending', bendingBatches, this.batchSlot.bending, first, at);
303
+ this.batches('clothTether', tetherBatches, this.batchSlot.tether, first, at);
304
+ }
305
+ this.dispatch('clothLimit', at, 0, -1);
306
+ this.dispatch('clothFinish', at, 0, -1);
307
+ }
308
+ if (blend > 0)
309
+ this.dispatch('clothBlend', at, 0, -1);
310
+ }
311
+ /** Every batch of one kind, in colour order, each a dispatch over its range. */
312
+ batches(entry, batches, firstSlot, first, stepAt) {
313
+ for (let b = 0; b + 1 < batches.length; b++) {
314
+ const size = batches[b + 1] - batches[b];
315
+ if (size === 0)
316
+ continue;
317
+ const slot = firstSlot + b * 2 + first;
318
+ this.dispatch(entry, stepAt, slot * DYNAMIC_ALIGNMENT, -1, Math.ceil(size / CLOTH_WORKGROUP));
319
+ }
320
+ }
321
+ dispatch(entry, stepAt, batchAt, output, groups = this.groups) {
322
+ const pass = this.passFor();
323
+ pass.setPipeline(this.kernels.pipelines[entry]);
324
+ pass.setBindGroup(0, this.group, [stepAt, batchAt]);
325
+ if (output >= 0)
326
+ pass.setBindGroup(1, this.outputs[output]);
327
+ pass.dispatchWorkgroups(groups);
328
+ }
329
+ passFor() {
330
+ if (this.pass === null) {
331
+ this.encoder = this.device.createCommandEncoder({ label: 'cloth' });
332
+ this.pass = this.encoder.beginComputePass({ label: 'cloth' });
333
+ }
334
+ return this.pass;
335
+ }
336
+ /** Send what has been recorded: every slot it reads was written ahead of it on the queue. */
337
+ flush() {
338
+ const { pass, encoder } = this;
339
+ if (pass === null || encoder === null)
340
+ return;
341
+ pass.end();
342
+ this.device.queue.submit([encoder.finish()]);
343
+ this.pass = null;
344
+ this.encoder = null;
345
+ this.slot = 0;
346
+ }
347
+ /**
348
+ * A step's slot: its fraction, blend and alpha, the poses it reads, and — where `targets` is
349
+ * given — the share of the character's motion the cloth is carried by, as `carryParticles` takes
350
+ * it. Returns the slot's byte offset.
351
+ */
352
+ takeSlot(fraction, blend, alpha, targets) {
353
+ if (this.slot === STEP_SLOTS)
354
+ this.flush();
355
+ const f = this.stepFloats;
356
+ const u = this.stepWords;
357
+ f.fill(0);
358
+ f[0] = fraction;
359
+ f[1] = blend;
360
+ f[2] = alpha;
361
+ u[4] = this.fromAt;
362
+ u[5] = this.toAt;
363
+ f.set(this.wind, 28);
364
+ if (targets !== null)
365
+ this.writeCarry(targets.modelBefore, targets.modelNow);
366
+ const at = this.slot * DYNAMIC_ALIGNMENT;
367
+ this.device.queue.writeBuffer(this.stepSlots, at, this.stepStaging);
368
+ this.slot++;
369
+ return at;
370
+ }
371
+ /** `carryParticles`' rotation, origin and move, into the step's slot; carry 0 where none. */
372
+ writeCarry(previous, next) {
373
+ const { linearInertia, angularInertia } = this.control.parameters;
374
+ const turn = 1 - angularInertia;
375
+ const move = 1 - linearInertia;
376
+ if (turn === 0 && move === 0)
377
+ return;
378
+ const f = this.stepFloats;
379
+ this.stepWords[3] = 1;
380
+ /* Column c of R = R_next × R_previousᵀ, at turn0 … turn2. */
381
+ for (let col = 0; col < 3; col++) {
382
+ for (let row = 0; row < 3; row++) {
383
+ let sum = 0;
384
+ for (let k = 0; k < 3; k++) {
385
+ sum += next[k * 4 + row] * previous[k * 4 + col];
386
+ }
387
+ f[8 + col * 4 + row] = sum;
388
+ }
389
+ }
390
+ for (let k = 0; k < 3; k++) {
391
+ f[20 + k] = previous[12 + k];
392
+ f[24 + k] = (next[12 + k] - previous[12 + k]) * move;
393
+ }
394
+ f[23] = turn;
395
+ }
396
+ /** The pose's skin matrices — or the model, for a cloth with no rig — and its colliders. */
397
+ writeFrameData() {
398
+ const targets = this.control.targets;
399
+ const out = this.frameStaging;
400
+ const joints = this.packed.joints;
401
+ out.set(this.packed.rigged && targets.skin !== null ? targets.skin : targets.model, 0);
402
+ const colliders = this.packed.colliders;
403
+ out.set(targets.colliderEnds, joints * 16);
404
+ out.set(targets.colliderRadii, joints * 16 + colliders * 6);
405
+ if (out.byteLength > 0)
406
+ this.device.queue.writeBuffer(this.frameData, 0, out);
407
+ }
408
+ constants() {
409
+ const p = this.control.parameters;
410
+ const packed = this.packed;
411
+ const bytes = new ArrayBuffer(CONSTANT_BYTES);
412
+ const u = new Uint32Array(bytes);
413
+ const f = new Float32Array(bytes);
414
+ const h = p.step / p.substeps;
415
+ u[0] = packed.count;
416
+ u[1] = packed.distances;
417
+ u[2] = packed.bendings;
418
+ u[3] = packed.joints;
419
+ u[4] = packed.colliders;
420
+ u[5] = packed.limits;
421
+ u[6] = this.particles.width;
422
+ f[8] = h;
423
+ f[9] = 1 / h;
424
+ f[10] = 1 / (h * h);
425
+ f[11] = exactExp(p.damping * h);
426
+ f[12] = exactExp(p.drag * h);
427
+ f[13] = p.maxDistanceScale;
428
+ f[14] = p.margin;
429
+ f.set(p.gravity, 16);
430
+ return bytes;
431
+ }
432
+ /** Each batch's range, twice — opening a substep's iterations and not — at a slot each. */
433
+ writeBatches() {
434
+ const { distanceBatches, bendingBatches, tetherBatches } = this.packed;
435
+ const kinds = [distanceBatches, bendingBatches, tetherBatches];
436
+ const slots = kinds.reduce((sum, batches) => sum + (batches.length - 1) * 2, 0);
437
+ const words = new Uint32Array((Math.max(1, slots) * DYNAMIC_ALIGNMENT) / 4);
438
+ let slot = 0;
439
+ for (const batches of kinds) {
440
+ for (let b = 0; b + 1 < batches.length; b++) {
441
+ for (let first = 0; first < 2; first++) {
442
+ const at = (slot * DYNAMIC_ALIGNMENT) / 4;
443
+ words[at] = batches[b];
444
+ words[at + 1] = batches[b + 1];
445
+ words[at + 2] = first;
446
+ slot++;
447
+ }
448
+ }
449
+ }
450
+ return words;
451
+ }
452
+ }
@@ -11,6 +11,19 @@ export declare class MotionPass {
11
11
  /** A skinned record's two palettes, this frame's and last frame's, by record index. */
12
12
  private readonly paletteNow;
13
13
  private readonly paletteWas;
14
+ /** A cloth record's binding and its particles now and last frame, by record index. */
15
+ private readonly clothBinding;
16
+ private readonly clothNow;
17
+ private readonly clothWas;
18
+ private clothLayout;
19
+ private clothPipelineLayout;
20
+ /** Keyed as `skinnedPipelines` is. */
21
+ private readonly clothPipelines;
22
+ /**
23
+ * A bind group per particles now, particles then and binding. Weak at the top, by this frame's
24
+ * particles, so a character's groups go when its particles do; a steady garment settles on three.
25
+ */
26
+ private clothGroups;
14
27
  /** A rewritten record's last-frame rows, by record index. */
15
28
  private readonly previousRows;
16
29
  /** One pipeline per stride for rewritten meshes: current and previous rows at that stride. */
@@ -48,6 +61,11 @@ export declare class MotionPass {
48
61
  recordRigid(mesh: GpuMesh, model: ArrayLike<number>, previousModel: ArrayLike<number>): void;
49
62
  /** A skinned mover: its positions under last frame's pose as well as last frame's model. */
50
63
  recordSkinned(mesh: GpuMesh, model: ArrayLike<number>, previousModel: ArrayLike<number>, palette: GPUTextureView, previousPalette: GPUTextureView): void;
64
+ /**
65
+ * A cloth-bound skinned draw: its skinned positions under both poses, each carried toward the
66
+ * cloth by its painted weight — this frame's `particles` and last frame's `previousParticles`.
67
+ */
68
+ recordCloth(mesh: GpuMesh, model: ArrayLike<number>, previousModel: ArrayLike<number>, palette: GPUTextureView, previousPalette: GPUTextureView, binding: GPUTextureView, particles: GPUTextureView, previousParticles: GPUTextureView): void;
51
69
  /** A rewritten mesh: its vertices as they were, read from `previous`, under last frame's model. */
52
70
  recordDynamic(mesh: GpuMesh, model: ArrayLike<number>, previousModel: ArrayLike<number>, previous: GPUBuffer): void;
53
71
  /** An instanced batch: its first `count` slots, each under last frame's placement of that slot. */
@@ -77,6 +95,8 @@ export declare class MotionPass {
77
95
  private instancedPipelineFor;
78
96
  /** The bind group holding one pair of palettes, built the first time the pair is seen. */
79
97
  private paletteGroup;
98
+ /** The bind group holding a cloth's binding and its two particle textures, built on first sight. */
99
+ private clothGroup;
80
100
  /** The pipeline for one vertex stride, built on first sight of it. */
81
101
  private pipelineFor;
82
102
  /**
@@ -13,6 +13,7 @@ const RIGID = 0;
13
13
  const SKINNED = 1;
14
14
  const DYNAMIC = 2;
15
15
  const INSTANCED = 3;
16
+ const CLOTH = 4;
16
17
  /** An instanced record's per-draw matrices are not read; this fills their slot. */
17
18
  const IDENTITY = new Float32Array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]);
18
19
  export class MotionPass {
@@ -27,6 +28,19 @@ export class MotionPass {
27
28
  /** A skinned record's two palettes, this frame's and last frame's, by record index. */
28
29
  paletteNow = [];
29
30
  paletteWas = [];
31
+ /** A cloth record's binding and its particles now and last frame, by record index. */
32
+ clothBinding = [];
33
+ clothNow = [];
34
+ clothWas = [];
35
+ clothLayout = null;
36
+ clothPipelineLayout = null;
37
+ /** Keyed as `skinnedPipelines` is. */
38
+ clothPipelines = new Map();
39
+ /**
40
+ * A bind group per particles now, particles then and binding. Weak at the top, by this frame's
41
+ * particles, so a character's groups go when its particles do; a steady garment settles on three.
42
+ */
43
+ clothGroups = new WeakMap();
30
44
  /** A rewritten record's last-frame rows, by record index. */
31
45
  previousRows = [];
32
46
  /** One pipeline per stride for rewritten meshes: current and previous rows at that stride. */
@@ -74,6 +88,18 @@ export class MotionPass {
74
88
  this.paletteNow[at] = palette;
75
89
  this.paletteWas[at] = previousPalette;
76
90
  }
91
+ /**
92
+ * A cloth-bound skinned draw: its skinned positions under both poses, each carried toward the
93
+ * cloth by its painted weight — this frame's `particles` and last frame's `previousParticles`.
94
+ */
95
+ recordCloth(mesh, model, previousModel, palette, previousPalette, binding, particles, previousParticles) {
96
+ const at = this.take(mesh, model, previousModel, CLOTH);
97
+ this.paletteNow[at] = palette;
98
+ this.paletteWas[at] = previousPalette;
99
+ this.clothBinding[at] = binding;
100
+ this.clothNow[at] = particles;
101
+ this.clothWas[at] = previousParticles;
102
+ }
77
103
  /** A rewritten mesh: its vertices as they were, read from `previous`, under last frame's model. */
78
104
  recordDynamic(mesh, model, previousModel, previous) {
79
105
  const at = this.take(mesh, model, previousModel, DYNAMIC);
@@ -147,6 +173,18 @@ export class MotionPass {
147
173
  this.skinnedLayout = this.device.createPipelineLayout({
148
174
  bindGroupLayouts: [this.layout, this.paletteLayout],
149
175
  });
176
+ /* The binding and the two particle textures, read with `textureLoad` as the palettes are. */
177
+ this.clothLayout = this.device.createBindGroupLayout({
178
+ label: 'recon.motion.cloth',
179
+ entries: [0, 1, 2].map((binding) => ({
180
+ binding,
181
+ visibility: 0x1,
182
+ texture: { sampleType: 'unfilterable-float' },
183
+ })),
184
+ });
185
+ this.clothPipelineLayout = this.device.createPipelineLayout({
186
+ bindGroupLayouts: [this.layout, this.paletteLayout, this.clothLayout],
187
+ });
150
188
  }
151
189
  /**
152
190
  * The skinned pipeline for one stride and one placement of the joints and weights in it.
@@ -154,21 +192,32 @@ export class MotionPass {
154
192
  * Same fragment stage, winding and depth test as the rigid one — see `pipelineFor` — with the two
155
193
  * skinning attributes read from the interleaved rows at the offsets the mesh records.
156
194
  */
157
- skinnedPipelineFor(stride, offsets) {
158
- const key = (stride * 4096 + offsets.joints) * 4096 + offsets.weights;
159
- const held = this.skinnedPipelines.get(key);
195
+ skinnedPipelineFor(stride, offsets, cloth = false) {
196
+ /* A rig of eight takes the stage that sums the second four as well. See motion.wgsl.ts. */
197
+ const eight = offsets.joints2 >= 0 && offsets.weights2 >= 0;
198
+ /*
199
+ * Five byte counts, each under 256 because a vertex is at most 172 bytes, as five base-256
200
+ * digits: exact below 2^40, so two layouts can never share a key.
201
+ */
202
+ const key = (((stride * 256 + offsets.joints) * 256 + offsets.weights) * 256 + offsets.joints2 + 1) *
203
+ 256 +
204
+ offsets.weights2 +
205
+ 1;
206
+ const pipelines = cloth ? this.clothPipelines : this.skinnedPipelines;
207
+ const held = pipelines.get(key);
160
208
  if (held !== undefined)
161
209
  return held;
162
210
  const module = this.module;
163
- const layout = this.skinnedLayout;
211
+ const layout = cloth ? this.clothPipelineLayout : this.skinnedLayout;
164
212
  if (module === null || layout === null)
165
213
  return null;
214
+ const stage = cloth ? 'Cloth' : 'Skinned';
166
215
  const pipeline = this.device.createRenderPipeline({
167
- label: `recon.motion.skinned.${String(stride)}`,
216
+ label: `recon.motion.${cloth ? 'cloth' : 'skinned'}.${String(stride)}`,
168
217
  layout,
169
218
  vertex: {
170
219
  module,
171
- entryPoint: 'motionSkinnedVert',
220
+ entryPoint: eight ? `motion${stage}EightVert` : `motion${stage}Vert`,
172
221
  buffers: [
173
222
  {
174
223
  arrayStride: stride,
@@ -176,6 +225,12 @@ export class MotionPass {
176
225
  { shaderLocation: 0, offset: 0, format: 'float32x3' },
177
226
  { shaderLocation: 11, offset: offsets.joints, format: 'float32x4' },
178
227
  { shaderLocation: 12, offset: offsets.weights, format: 'float32x4' },
228
+ ...(eight
229
+ ? [
230
+ { shaderLocation: 14, offset: offsets.joints2, format: 'float32x4' },
231
+ { shaderLocation: 15, offset: offsets.weights2, format: 'float32x4' },
232
+ ]
233
+ : []),
179
234
  ],
180
235
  },
181
236
  ],
@@ -188,7 +243,7 @@ export class MotionPass {
188
243
  depthCompare: DEPTH_COMPARE_EQUAL,
189
244
  },
190
245
  });
191
- this.skinnedPipelines.set(key, pipeline);
246
+ pipelines.set(key, pipeline);
192
247
  return pipeline;
193
248
  }
194
249
  /** The rewritten pipeline for one stride: position from each of two buffers of that stride. */
@@ -300,6 +355,36 @@ export class MotionPass {
300
355
  }
301
356
  return group;
302
357
  }
358
+ /** The bind group holding a cloth's binding and its two particle textures, built on first sight. */
359
+ clothGroup(binding, now, was) {
360
+ const layout = this.clothLayout;
361
+ if (layout === null)
362
+ return null;
363
+ let byWas = this.clothGroups.get(now);
364
+ if (byWas === undefined) {
365
+ byWas = new Map();
366
+ this.clothGroups.set(now, byWas);
367
+ }
368
+ let byBinding = byWas.get(was);
369
+ if (byBinding === undefined) {
370
+ byBinding = new Map();
371
+ byWas.set(was, byBinding);
372
+ }
373
+ let group = byBinding.get(binding);
374
+ if (group === undefined) {
375
+ group = this.device.createBindGroup({
376
+ label: 'recon.motion.cloth',
377
+ layout,
378
+ entries: [
379
+ { binding: 0, resource: binding },
380
+ { binding: 1, resource: now },
381
+ { binding: 2, resource: was },
382
+ ],
383
+ });
384
+ byBinding.set(binding, group);
385
+ }
386
+ return group;
387
+ }
303
388
  /** The pipeline for one vertex stride, built on first sight of it. */
304
389
  pipelineFor(stride) {
305
390
  const held = this.pipelines.get(stride);
@@ -450,16 +535,27 @@ export class MotionPass {
450
535
  pipeline = this.dynamicPipelineFor(mesh.vertexStride);
451
536
  pass.setVertexBuffer(1, previous);
452
537
  }
453
- else if (kind === SKINNED) {
538
+ else if (kind === SKINNED || kind === CLOTH) {
454
539
  const offsets = mesh.skinOffsets;
455
540
  const now = this.paletteNow[i] ?? null;
456
541
  const was = this.paletteWas[i] ?? null;
457
542
  if (offsets === null || now === null || was === null)
458
543
  continue;
459
- pipeline = this.skinnedPipelineFor(mesh.vertexStride, offsets);
544
+ pipeline = this.skinnedPipelineFor(mesh.vertexStride, offsets, kind === CLOTH);
460
545
  const palettes = this.paletteGroup(now, was);
461
546
  if (pipeline === null || palettes === null)
462
547
  continue;
548
+ if (kind === CLOTH) {
549
+ const binding = this.clothBinding[i] ?? null;
550
+ const particles = this.clothNow[i] ?? null;
551
+ const previous = this.clothWas[i] ?? null;
552
+ if (binding === null || particles === null || previous === null)
553
+ continue;
554
+ const cloth = this.clothGroup(binding, particles, previous);
555
+ if (cloth === null)
556
+ continue;
557
+ pass.setBindGroup(2, cloth);
558
+ }
463
559
  pass.setBindGroup(1, palettes);
464
560
  }
465
561
  else {
@@ -482,6 +578,7 @@ export class MotionPass {
482
578
  }
483
579
  dispose() {
484
580
  this.paletteGroups.clear();
581
+ this.clothGroups = new WeakMap();
485
582
  this.frameBuffer?.destroy();
486
583
  this.drawBuffer?.destroy();
487
584
  this.frameBuffer = null;