@driftengine/core 4.8.3 → 4.8.5

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 (180) hide show
  1. package/CHANGELOG.json +82 -0
  2. package/README.md +1 -1
  3. package/dist/build/shaderComments.js +0 -0
  4. package/dist/geometry/meshConcat.js +2 -0
  5. package/dist/index.d.ts +10 -4
  6. package/dist/index.js +16 -1
  7. package/dist/math/scalar.js +1 -1
  8. package/dist/render/anisotropicLobe.d.ts +7 -0
  9. package/dist/render/anisotropicLobe.js +33 -0
  10. package/dist/render/backend/api.d.ts +41 -4
  11. package/dist/render/backend/select.js +8 -0
  12. package/dist/render/backend/webgl2/clothTextures.d.ts +27 -0
  13. package/dist/render/backend/webgl2/clothTextures.js +93 -0
  14. package/dist/render/backend/webgl2/renderer.d.ts +136 -3
  15. package/dist/render/backend/webgl2/renderer.js +604 -63
  16. package/dist/render/backend/webgl2/skinScatter.d.ts +50 -0
  17. package/dist/render/backend/webgl2/skinScatter.js +256 -0
  18. package/dist/render/backend/webgpu/buffers.d.ts +25 -11
  19. package/dist/render/backend/webgpu/buffers.js +98 -49
  20. package/dist/render/backend/webgpu/clothSolvePack.d.ts +27 -0
  21. package/dist/render/backend/webgpu/clothSolvePack.js +129 -0
  22. package/dist/render/backend/webgpu/clothTextures.d.ts +68 -0
  23. package/dist/render/backend/webgpu/clothTextures.js +145 -0
  24. package/dist/render/backend/webgpu/compressedUpload.d.ts +18 -0
  25. package/dist/render/backend/webgpu/compressedUpload.js +38 -0
  26. package/dist/render/backend/webgpu/computeHost.d.ts +21 -0
  27. package/dist/render/backend/webgpu/computeHost.js +5 -0
  28. package/dist/render/backend/webgpu/depthPass.d.ts +16 -6
  29. package/dist/render/backend/webgpu/depthPass.js +39 -9
  30. package/dist/render/backend/webgpu/drawCommand.js +6 -2
  31. package/dist/render/backend/webgpu/flatPass.d.ts +38 -4
  32. package/dist/render/backend/webgpu/flatPass.js +109 -12
  33. package/dist/render/backend/webgpu/gpuSkinnedCloth.d.ts +74 -0
  34. package/dist/render/backend/webgpu/gpuSkinnedCloth.js +452 -0
  35. package/dist/render/backend/webgpu/motionPass.d.ts +20 -0
  36. package/dist/render/backend/webgpu/motionPass.js +106 -9
  37. package/dist/render/backend/webgpu/postPass.d.ts +3 -1
  38. package/dist/render/backend/webgpu/postPass.js +6 -3
  39. package/dist/render/backend/webgpu/renderer.d.ts +113 -5
  40. package/dist/render/backend/webgpu/renderer.js +498 -48
  41. package/dist/render/backend/webgpu/shaders/clothSolve.wgsl.d.ts +33 -0
  42. package/dist/render/backend/webgpu/shaders/clothSolve.wgsl.js +469 -0
  43. package/dist/render/backend/webgpu/skinScatterPass.d.ts +51 -0
  44. package/dist/render/backend/webgpu/skinScatterPass.js +253 -0
  45. package/dist/render/backend/webgpu/surfaceTexturePass.d.ts +4 -1
  46. package/dist/render/backend/webgpu/surfaceTexturePass.js +37 -12
  47. package/dist/render/clothBindingData.d.ts +41 -0
  48. package/dist/render/clothBindingData.js +72 -0
  49. package/dist/render/clothPlacement.d.ts +25 -0
  50. package/dist/render/clothPlacement.js +85 -0
  51. package/dist/render/compressedSource.d.ts +66 -0
  52. package/dist/render/compressedSource.js +138 -0
  53. package/dist/render/cutoutCaster.d.ts +2 -0
  54. package/dist/render/cutoutCaster.js +2 -0
  55. package/dist/render/cutoutDither.d.ts +51 -0
  56. package/dist/render/cutoutDither.js +90 -0
  57. package/dist/render/eyeRefraction.d.ts +35 -0
  58. package/dist/render/eyeRefraction.js +40 -0
  59. package/dist/render/glCompressed.d.ts +27 -0
  60. package/dist/render/glCompressed.js +53 -0
  61. package/dist/render/hairLobes.d.ts +70 -0
  62. package/dist/render/hairLobes.js +120 -0
  63. package/dist/render/lightBudget.d.ts +22 -0
  64. package/dist/render/lightBudget.js +22 -0
  65. package/dist/render/mesh.d.ts +7 -0
  66. package/dist/render/mesh.js +24 -0
  67. package/dist/render/recon/motionVectors.d.ts +4 -2
  68. package/dist/render/recon/motionVectors.js +6 -3
  69. package/dist/render/renderQuality.d.ts +20 -0
  70. package/dist/render/renderQuality.js +5 -0
  71. package/dist/render/rendererHarness.js +4 -0
  72. package/dist/render/sceneTarget.d.ts +12 -0
  73. package/dist/render/sceneTarget.js +14 -0
  74. package/dist/render/shaders/clothBinding.d.ts +22 -0
  75. package/dist/render/shaders/clothBinding.js +80 -0
  76. package/dist/render/shaders/depth.js +42 -6
  77. package/dist/render/shaders/flat/anisotropic.d.ts +15 -0
  78. package/dist/render/shaders/flat/anisotropic.js +68 -0
  79. package/dist/render/shaders/flat/eye.d.ts +1 -0
  80. package/dist/render/shaders/flat/eye.js +81 -0
  81. package/dist/render/shaders/flat/hair.d.ts +1 -0
  82. package/dist/render/shaders/flat/hair.js +101 -0
  83. package/dist/render/shaders/flat/index.js +36 -2
  84. package/dist/render/shaders/flat/main.d.ts +1 -1
  85. package/dist/render/shaders/flat/main.js +113 -17
  86. package/dist/render/shaders/flat/models.d.ts +14 -0
  87. package/dist/render/shaders/flat/models.js +273 -0
  88. package/dist/render/shaders/flat/preamble.js +26 -24
  89. package/dist/render/shaders/flat/skin.d.ts +1 -0
  90. package/dist/render/shaders/flat/skin.js +114 -0
  91. package/dist/render/shaders/generated/depth.wgsl.d.ts +38 -16
  92. package/dist/render/shaders/generated/depth.wgsl.js +38 -16
  93. package/dist/render/shaders/generated/flat.wgsl.d.ts +1790 -1704
  94. package/dist/render/shaders/generated/flat.wgsl.js +1828 -1742
  95. package/dist/render/shaders/generated/skinBlur.wgsl.d.ts +48 -0
  96. package/dist/render/shaders/generated/skinBlur.wgsl.js +54 -0
  97. package/dist/render/shaders/recon/motion.wgsl.d.ts +1 -1
  98. package/dist/render/shaders/recon/motion.wgsl.js +102 -0
  99. package/dist/render/shaders/skinBlur.d.ts +1 -0
  100. package/dist/render/shaders/skinBlur.js +103 -0
  101. package/dist/render/shaders/skinning.d.ts +3 -1
  102. package/dist/render/shaders/skinning.js +31 -4
  103. package/dist/render/shadowCasters.d.ts +13 -1
  104. package/dist/render/skinBlur.d.ts +41 -0
  105. package/dist/render/skinBlur.js +49 -0
  106. package/dist/render/skinScatter.d.ts +67 -0
  107. package/dist/render/skinScatter.js +80 -0
  108. package/dist/render/skinnedClothRun.d.ts +26 -0
  109. package/dist/render/skinnedClothRun.js +84 -0
  110. package/dist/render/surfaceModel.d.ts +129 -0
  111. package/dist/render/surfaceModel.js +152 -0
  112. package/dist/render/surfaceTexture.d.ts +44 -4
  113. package/dist/render/surfaceTexture.js +60 -46
  114. package/dist/render/vertexDefaults.js +7 -0
  115. package/package.json +6 -6
  116. package/src/build/shaderComments.ts +0 -0
  117. package/src/geometry/meshConcat.ts +2 -0
  118. package/src/index.ts +40 -1
  119. package/src/math/scalar.ts +1 -1
  120. package/src/render/anisotropicLobe.ts +41 -0
  121. package/src/render/backend/api.ts +52 -3
  122. package/src/render/backend/select.ts +8 -0
  123. package/src/render/backend/webgl2/clothTextures.ts +120 -0
  124. package/src/render/backend/webgl2/renderer.ts +693 -64
  125. package/src/render/backend/webgl2/skinScatter.ts +309 -0
  126. package/src/render/backend/webgpu/buffers.ts +137 -63
  127. package/src/render/backend/webgpu/clothSolvePack.ts +161 -0
  128. package/src/render/backend/webgpu/clothTextures.ts +194 -0
  129. package/src/render/backend/webgpu/compressedUpload.ts +65 -0
  130. package/src/render/backend/webgpu/computeHost.ts +25 -0
  131. package/src/render/backend/webgpu/depthPass.ts +39 -7
  132. package/src/render/backend/webgpu/drawCommand.ts +6 -2
  133. package/src/render/backend/webgpu/flatPass.ts +145 -7
  134. package/src/render/backend/webgpu/gpuSkinnedCloth.ts +502 -0
  135. package/src/render/backend/webgpu/motionPass.ts +128 -9
  136. package/src/render/backend/webgpu/postPass.ts +5 -1
  137. package/src/render/backend/webgpu/renderer.ts +663 -43
  138. package/src/render/backend/webgpu/shaders/clothSolve.wgsl.ts +470 -0
  139. package/src/render/backend/webgpu/skinScatterPass.ts +309 -0
  140. package/src/render/backend/webgpu/surfaceTexturePass.ts +42 -12
  141. package/src/render/clothBindingData.ts +89 -0
  142. package/src/render/clothPlacement.ts +114 -0
  143. package/src/render/compressedSource.ts +198 -0
  144. package/src/render/cutoutCaster.ts +5 -0
  145. package/src/render/cutoutDither.ts +99 -0
  146. package/src/render/eyeRefraction.ts +72 -0
  147. package/src/render/glCompressed.ts +86 -0
  148. package/src/render/hairLobes.ts +188 -0
  149. package/src/render/lightBudget.ts +25 -0
  150. package/src/render/mesh.ts +23 -0
  151. package/src/render/recon/motionVectors.ts +6 -3
  152. package/src/render/renderQuality.ts +28 -0
  153. package/src/render/rendererHarness.ts +4 -0
  154. package/src/render/sceneTarget.ts +21 -0
  155. package/src/render/shaders/clothBinding.ts +81 -0
  156. package/src/render/shaders/depth.ts +42 -6
  157. package/src/render/shaders/flat/anisotropic.ts +68 -0
  158. package/src/render/shaders/flat/eye.ts +82 -0
  159. package/src/render/shaders/flat/hair.ts +102 -0
  160. package/src/render/shaders/flat/index.ts +36 -2
  161. package/src/render/shaders/flat/main.ts +113 -17
  162. package/src/render/shaders/flat/models.ts +286 -0
  163. package/src/render/shaders/flat/preamble.ts +26 -24
  164. package/src/render/shaders/flat/skin.ts +116 -0
  165. package/src/render/shaders/generated/depth.wgsl.ts +38 -16
  166. package/src/render/shaders/generated/flat.wgsl.ts +1828 -1742
  167. package/src/render/shaders/generated/skinBlur.wgsl.ts +56 -0
  168. package/src/render/shaders/recon/motion.wgsl.ts +102 -0
  169. package/src/render/shaders/skinBlur.ts +105 -0
  170. package/src/render/shaders/skinning.ts +31 -4
  171. package/src/render/shadowCasters.ts +13 -0
  172. package/src/render/skinBlur.ts +53 -0
  173. package/src/render/skinScatter.ts +92 -0
  174. package/src/render/skinnedClothRun.ts +128 -0
  175. package/src/render/surfaceModel.ts +274 -0
  176. package/src/render/surfaceTexture.ts +123 -63
  177. package/src/render/vertexDefaults.ts +7 -0
  178. package/dist/math/exact.d.ts +0 -76
  179. package/dist/math/exact.js +0 -327
  180. package/src/math/exact.ts +0 -340
@@ -24,6 +24,7 @@ import { INSTANCE_STRIDE } from '../../instances.ts';
24
24
  * without one.
25
25
  */
26
26
  const USAGE = {
27
+ COPY_SRC: 0x0004,
27
28
  COPY_DST: 0x0008,
28
29
  INDEX: 0x0010,
29
30
  VERTEX: 0x0020,
@@ -100,6 +101,13 @@ export const VERTEX_LAYOUT: readonly VertexAttribute[] = [
100
101
  * three lanes and a spare live here instead of three attributes that do not fit.
101
102
  */
102
103
  { name: 'channel', shaderLocation: 13, format: 'float32x4', components: 4, optional: true },
104
+ /*
105
+ * The fifth to eighth influences, at the last two locations. Only a skinned variant reads them,
106
+ * and only with `SKIN_EIGHT` on; every other mesh is fed zeros by the constants buffer. An
107
+ * instanced pipeline reclaims 14 and 15 with the rest of 11 to 15, as `INSTANCE_LOCATIONS` says.
108
+ */
109
+ { name: 'joints2', shaderLocation: 14, format: 'float32x4', components: 4, optional: true },
110
+ { name: 'weights2', shaderLocation: 15, format: 'float32x4', components: 4, optional: true },
103
111
  ];
104
112
 
105
113
  /** Geometry on the device, with the buffers bound in layout order. */
@@ -131,14 +139,16 @@ export interface GpuMesh {
131
139
  *
132
140
  * **Two, and the first is interleaved** — every attribute this mesh actually supplied, woven
133
141
  * into one stride — while the second holds one value per *absent* optional attribute at a stride
134
- * of zero. It is emphatically not one buffer per attribute, which this comment said until
142
+ * of zero. **A dynamic mesh has four**: its positions and its normals, each a buffer of its own so
143
+ * `update` writes them alone, then the interleaved rest and the constants. It is emphatically not one buffer per attribute, which this comment said until
135
144
  * 2026-09-17 and which cost the motion pass an afternoon: a pipeline built to that description
136
145
  * reads a position out of every twelve bytes of an interleaved vertex, draws geometry that is
137
146
  * not the mesh, and fails no validation at all.
138
147
  */
139
148
  readonly vertexBuffers: readonly GPUBuffer[];
140
149
  /**
141
- * Bytes a vertex takes in the first of those buffers.
150
+ * Bytes a vertex takes in the first of those buffers: the interleaved stride, or twelve for a
151
+ * dynamic mesh, whose first buffer is its positions.
142
152
  *
143
153
  * Kept because it is a function of which *optional* attributes this mesh supplied, so it differs
144
154
  * between two meshes drawn by the same pipeline — and a pass that reads only the position still
@@ -151,8 +161,18 @@ export interface GpuMesh {
151
161
  * Byte offsets of the joint indices and weights in the interleaved vertex, or null for a mesh
152
162
  * that supplied neither. For a pass that skins through a shader of its own — the motion pass —
153
163
  * which has to find them without the `present` map the generated pipelines are keyed by.
164
+ *
165
+ * **Null on a dynamic mesh too**, whose first buffer holds positions alone: the motion pass reads
166
+ * joints from that buffer, so a dynamic skinned mesh's motion between rewrites is its model's
167
+ * rather than its pose's. A rewrite carries its own motion, from last frame's positions.
154
168
  */
155
- readonly skinOffsets: { readonly joints: number; readonly weights: number } | null;
169
+ readonly skinOffsets: {
170
+ readonly joints: number;
171
+ readonly weights: number;
172
+ /** The second four influences' offsets, or -1 for a mesh with four. */
173
+ readonly joints2: number;
174
+ readonly weights2: number;
175
+ } | null;
156
176
  readonly indexBuffer: GPUBuffer;
157
177
  /**
158
178
  * How many indices the mesh draws. **Zero is a mesh with nothing to draw, and no pass draws it**:
@@ -200,6 +220,10 @@ export interface GpuMesh {
200
220
  * the key is built before the present map is consulted.
201
221
  */
202
222
  readonly isSkinned: boolean;
223
+ /** Whether its rig moves a vertex by eight influences, which keys a pipeline with `SKIN_EIGHT`. */
224
+ readonly isSkinnedEight: boolean;
225
+ /** How many vertices it has, which a cloth binding is checked against. */
226
+ readonly vertexCount: number;
203
227
  /** Whether it has texture coordinates, without which a material's maps read one texel. */
204
228
  readonly hasUvs: boolean;
205
229
  /**
@@ -221,12 +245,13 @@ export interface GpuMesh {
221
245
  /**
222
246
  * Rewrite this mesh's positions, and its normals where the caller has them.
223
247
  *
224
- * Absent unless the mesh was created `dynamic`. **This backend interleaves every attribute into
225
- * one buffer**, so a position is not contiguous with the next one — writing them individually
226
- * would be one `writeBuffer` per vertex. So a dynamic mesh keeps the interleaved array it was
227
- * built from, patches the two fields that move, and uploads the lot in one call. The cost is
228
- * one CPU-side copy of the vertex data per dynamic mesh, which is why the flag exists rather
229
- * than every mesh paying it.
248
+ * Absent unless the mesh was created `dynamic`. **A dynamic mesh's positions and normals are
249
+ * buffers of their own**, so this is one `writeBuffer` of the caller's positions and one of its
250
+ * normals, and nothing else moves. Until 4.8.4 every attribute was interleaved into one buffer,
251
+ * and a dynamic mesh kept a CPU copy of it to patch and upload whole — a garment's colours,
252
+ * coordinates, tangents and joints re-sent every frame for the twenty-four bytes a vertex that
253
+ * changed, reported at 2.8 MB a frame for one character. What the split costs is a pipeline per
254
+ * layout, keyed by `:dynamic`, and two more buffers bound per draw of such a mesh.
230
255
  */
231
256
  readonly update:
232
257
  | ((
@@ -237,9 +262,9 @@ export interface GpuMesh {
237
262
  ) => void)
238
263
  | null;
239
264
  /**
240
- * A dynamic mesh's motion state, which the renderer owns: a buffer for last frame's interleaved
241
- * rows, made the first time a reconstructing renderer rewrites the mesh, and the frame of the
242
- * last rewrite. Null on a mesh created without `{ dynamic: true }`, which never moves a vertex.
265
+ * A dynamic mesh's motion state, which the renderer owns: a buffer for last frame's positions,
266
+ * made the first time a reconstructing renderer rewrites the mesh, and the frame of the last
267
+ * rewrite. Null on a mesh created without `{ dynamic: true }`, which never moves a vertex.
243
268
  */
244
269
  readonly motion: { previous: GPUBuffer | null; changed: number } | null;
245
270
  dispose(): void;
@@ -314,10 +339,13 @@ export function createGpuMeshIncremental(
314
339
  const vertexCount = data.positions.length / 3;
315
340
  const layered = data.layers !== undefined;
316
341
  const supplied = VERTEX_LAYOUT.filter(
317
- (attribute) => !attribute.optional || data[attribute.name as keyof MeshData] !== undefined,
342
+ (attribute) =>
343
+ (!attribute.optional || data[attribute.name as keyof MeshData] !== undefined) &&
344
+ !(dynamic && isStreamed(attribute)),
318
345
  ).map((attribute) => laidOut(attribute, layered));
319
346
 
320
- /* Interleaved: one stride holding every attribute the mesh actually supplied. */
347
+ /* Interleaved: one stride holding every attribute the mesh actually supplied — less a dynamic
348
+ mesh's positions and normals, which are buffers of their own. */
321
349
  const stride = supplied.reduce((sum, attribute) => sum + attribute.components * 4, 0);
322
350
  const step = stride / 4;
323
351
  const interleaved = new Float32Array(step * vertexCount);
@@ -381,26 +409,31 @@ export function createGpuMeshIncremental(
381
409
  usage: USAGE.INDEX | USAGE.COPY_DST,
382
410
  });
383
411
 
384
- /*
385
- * Where the position and normal fields sit inside one vertex, in floats. Found from the same
386
- * `supplied` list the interleave walks, so the two cannot disagree — reading them off
387
- * `VERTEX_LAYOUT` instead would be right until a mesh omits an optional attribute before them,
388
- * which is the bug `present` already records once in this file.
389
- */
390
- let positionField = -1;
391
- let normalField = -1;
392
- let walked = 0;
393
- for (const attribute of supplied) {
394
- if (attribute.name === 'positions') positionField = walked;
395
- if (attribute.name === 'normals') normalField = walked;
396
- walked += attribute.components;
397
- }
412
+ /* A dynamic mesh's two moving streams: copied from as well as written, for last frame's positions. */
413
+ const streamBytes = align4(vertexCount * 12);
414
+ const streams = dynamic
415
+ ? {
416
+ positions: device.createBuffer({
417
+ label: 'mesh.positions',
418
+ size: streamBytes,
419
+ usage: USAGE.VERTEX | USAGE.COPY_DST | USAGE.COPY_SRC,
420
+ }),
421
+ normals: device.createBuffer({
422
+ label: 'mesh.normals',
423
+ size: streamBytes,
424
+ usage: USAGE.VERTEX | USAGE.COPY_DST,
425
+ }),
426
+ }
427
+ : null;
398
428
 
399
429
  /*
400
430
  * The chunks, in whole vertices and whole indices: a step never writes half of either, so the
401
431
  * buffer never holds a torn one even for the frame between two steps.
402
432
  */
403
- const verticesPerStep = Math.max(1, Math.floor(UPLOAD_BYTES_PER_STEP / stride));
433
+ const verticesPerStep = Math.max(
434
+ 1,
435
+ Math.floor(UPLOAD_BYTES_PER_STEP / (stride + (dynamic ? 24 : 0))),
436
+ );
404
437
  const indexWidth = data.indices.BYTES_PER_ELEMENT;
405
438
  const indicesPerStep = Math.max(1, Math.floor(UPLOAD_BYTES_PER_STEP / indexWidth));
406
439
  const totalSteps =
@@ -420,15 +453,18 @@ export function createGpuMeshIncremental(
420
453
  const mesh = gpuMeshOf({
421
454
  vertices,
422
455
  constants,
456
+ streams,
423
457
  indexBuffer,
424
- stride,
458
+ stride: streams === null ? stride : 12,
425
459
  indexCount: data.indices.length,
426
460
  bounds,
427
461
  progress,
428
462
  hasTangents: data.tangents !== undefined,
429
463
  isSkinned: data.joints !== undefined,
464
+ isSkinnedEight: data.joints !== undefined && data.joints2 !== undefined,
465
+ vertexCount,
430
466
  hasUvs: data.uvs !== undefined,
431
- skinOffsets: skinOffsetsOf(supplied),
467
+ skinOffsets: streams === null ? skinOffsetsOf(supplied) : null,
432
468
  hasChannel: data.channel !== undefined,
433
469
  morph:
434
470
  data.morphTargets === undefined || data.morphTargetCount === undefined
@@ -439,9 +475,10 @@ export function createGpuMeshIncremental(
439
475
  data.positions.length / 3,
440
476
  data.morphTargetCount,
441
477
  ),
442
- update: !dynamic
443
- ? null
444
- : dynamicUpdate(interleaved, vertices, bounds, vertexCount, step, positionField, normalField),
478
+ update:
479
+ streams === null
480
+ ? null
481
+ : streamUpdate(streams.positions, streams.normals, streamBytes, bounds, vertexCount),
445
482
  });
446
483
 
447
484
  const job: UploadJob = {
@@ -449,6 +486,7 @@ export function createGpuMeshIncremental(
449
486
  interleaved,
450
487
  device,
451
488
  supplied,
489
+ streams,
452
490
  vertices,
453
491
  indexBuffer,
454
492
  vertexCount,
@@ -471,6 +509,7 @@ interface UploadJob {
471
509
  interleaved: Float32Array | null;
472
510
  readonly device: GPUDevice;
473
511
  readonly supplied: readonly VertexAttribute[];
512
+ readonly streams: { readonly positions: GPUBuffer; readonly normals: GPUBuffer } | null;
474
513
  readonly vertices: GPUBuffer;
475
514
  readonly indexBuffer: GPUBuffer;
476
515
  readonly vertexCount: number;
@@ -510,6 +549,11 @@ function* uploadSteps(job: UploadJob): Generator<void, void, void> {
510
549
  fieldOffset += attribute.components;
511
550
  }
512
551
  device.queue.writeBuffer(vertices, from * stride, interleaved, from * step, (to - from) * step);
552
+ if (job.streams !== null) {
553
+ const count = (to - from) * 3;
554
+ device.queue.writeBuffer(job.streams.positions, from * 12, data.positions, from * 3, count);
555
+ device.queue.writeBuffer(job.streams.normals, from * 12, data.normals, from * 3, count);
556
+ }
513
557
  taken += 1;
514
558
  if (taken < job.totalSteps) yield;
515
559
  }
@@ -579,18 +623,26 @@ function skinOffsetsOf(supplied: readonly VertexAttribute[]): GpuMesh['skinOffse
579
623
  let floats = 0;
580
624
  let joints = -1;
581
625
  let weights = -1;
626
+ let joints2 = -1;
627
+ let weights2 = -1;
582
628
  for (const attribute of supplied) {
583
629
  if (attribute.name === 'joints') joints = floats * 4;
584
630
  if (attribute.name === 'weights') weights = floats * 4;
631
+ if (attribute.name === 'joints2') joints2 = floats * 4;
632
+ if (attribute.name === 'weights2') weights2 = floats * 4;
585
633
  floats += attribute.components;
586
634
  }
587
- return joints >= 0 && weights >= 0 ? { joints, weights } : null;
635
+ if (joints < 0 || weights < 0) return null;
636
+ return joints2 >= 0 && weights2 >= 0
637
+ ? { joints, weights, joints2, weights2 }
638
+ : { joints, weights, joints2: -1, weights2: -1 };
588
639
  }
589
640
 
590
641
  /** The handle, made where it can see only what it hands out. */
591
642
  function gpuMeshOf(parts: {
592
643
  readonly vertices: GPUBuffer;
593
644
  readonly constants: GPUBuffer;
645
+ readonly streams: { readonly positions: GPUBuffer; readonly normals: GPUBuffer } | null;
594
646
  readonly indexBuffer: GPUBuffer;
595
647
  readonly stride: number;
596
648
  readonly indexCount: number;
@@ -598,6 +650,8 @@ function gpuMeshOf(parts: {
598
650
  readonly progress: { readonly uploaded: boolean };
599
651
  readonly hasTangents: boolean;
600
652
  readonly isSkinned: boolean;
653
+ readonly isSkinnedEight: boolean;
654
+ readonly vertexCount: number;
601
655
  readonly hasUvs: boolean;
602
656
  readonly skinOffsets: GpuMesh['skinOffsets'];
603
657
  readonly hasChannel: boolean;
@@ -605,9 +659,12 @@ function gpuMeshOf(parts: {
605
659
  readonly update: GpuMesh['update'];
606
660
  }): GpuMesh {
607
661
  const motion = parts.update === null ? null : { previous: null as GPUBuffer | null, changed: -1 };
608
- const { vertices, constants, indexBuffer, progress } = parts;
662
+ const { vertices, constants, streams, indexBuffer, progress } = parts;
609
663
  return {
610
- vertexBuffers: [vertices, constants],
664
+ vertexBuffers:
665
+ streams === null
666
+ ? [vertices, constants]
667
+ : [streams.positions, streams.normals, vertices, constants],
611
668
  vertexStride: parts.stride,
612
669
  skinOffsets: parts.skinOffsets,
613
670
  indexBuffer,
@@ -618,6 +675,8 @@ function gpuMeshOf(parts: {
618
675
  },
619
676
  hasTangents: parts.hasTangents,
620
677
  isSkinned: parts.isSkinned,
678
+ isSkinnedEight: parts.isSkinnedEight,
679
+ vertexCount: parts.vertexCount,
621
680
  hasUvs: parts.hasUvs,
622
681
  hasChannel: parts.hasChannel,
623
682
  morph: parts.morph,
@@ -625,6 +684,8 @@ function gpuMeshOf(parts: {
625
684
  motion,
626
685
  dispose(): void {
627
686
  motion?.previous?.destroy();
687
+ streams?.positions.destroy();
688
+ streams?.normals.destroy();
628
689
  vertices.destroy();
629
690
  constants.destroy();
630
691
  indexBuffer.destroy();
@@ -632,19 +693,23 @@ function gpuMeshOf(parts: {
632
693
  };
633
694
  }
634
695
 
696
+ /** Whether an attribute is one a dynamic mesh keeps in a buffer of its own. */
697
+ function isStreamed(attribute: VertexAttribute): boolean {
698
+ return attribute.name === 'positions' || attribute.name === 'normals';
699
+ }
700
+
635
701
  /**
636
- * A dynamic mesh's `update`, which keeps the interleaved rows and nothing else: it rewrites the
637
- * positions and normals in them and uploads the whole buffer, so the other attributes it does not
638
- * receive have to still be there.
702
+ * A dynamic mesh's `update`: the caller's positions, and its normals where it has them, each
703
+ * written to its own buffer as it came. Last frame's positions, where a reconstruction wants them,
704
+ * are copied on the device before the write — a copy submitted ahead of a `writeBuffer` runs ahead
705
+ * of it on the queue — so no CPU copy of anything is kept.
639
706
  */
640
- function dynamicUpdate(
641
- interleaved: Float32Array,
642
- vertices: GPUBuffer,
707
+ function streamUpdate(
708
+ positionsBuffer: GPUBuffer,
709
+ normalsBuffer: GPUBuffer,
710
+ streamBytes: number,
643
711
  bounds: Bounds,
644
712
  vertexCount: number,
645
- step: number,
646
- positionField: number,
647
- normalField: number,
648
713
  ): NonNullable<GpuMesh['update']> {
649
714
  return (
650
715
  target: GPUDevice,
@@ -652,30 +717,27 @@ function dynamicUpdate(
652
717
  normals?: Float32Array,
653
718
  previous: GPUBuffer | null = null,
654
719
  ): void => {
655
- if (positions.length !== vertexCount * 3) {
720
+ for (const [name, values] of [
721
+ ['update', positions],
722
+ ['normals', normals],
723
+ ] as const) {
724
+ if (values === undefined || values.length === vertexCount * 3) continue;
656
725
  throw new Error(
657
- `this mesh has ${vertexCount} vertices and the update has ${positions.length / 3}. ` +
726
+ `this mesh has ${vertexCount} vertices and the ${name} has ${values.length / 3}. ` +
658
727
  "A mesh's vertex count is fixed at creation: the index buffer, the pipeline and " +
659
728
  'every other attribute are sized against it.',
660
729
  );
661
730
  }
662
- /* The rows as they stand are last frame's, handed over before a position of them changes.
663
- `writeBuffer` copies at the call, so patching the array afterwards cannot reach this copy. */
664
- if (previous !== null) target.queue.writeBuffer(previous, 0, interleaved);
731
+ if (previous !== null) {
732
+ const encoder = target.createCommandEncoder({ label: 'mesh.previousPositions' });
733
+ encoder.copyBufferToBuffer(positionsBuffer, 0, previous, 0, streamBytes);
734
+ target.queue.submit([encoder.finish()]);
735
+ }
665
736
  /* Everything that decides whether this is on screen starts from the bounds, so a cloth
666
737
  that blew sideways out of its original box would be culled while still visible. */
667
738
  boundsOfPositions(positions, bounds);
668
- for (let vertex = 0; vertex < vertexCount; vertex++) {
669
- const at = vertex * step;
670
- interleaved[at + positionField] = positions[vertex * 3] ?? 0;
671
- interleaved[at + positionField + 1] = positions[vertex * 3 + 1] ?? 0;
672
- interleaved[at + positionField + 2] = positions[vertex * 3 + 2] ?? 0;
673
- if (normals === undefined) continue;
674
- interleaved[at + normalField] = normals[vertex * 3] ?? 0;
675
- interleaved[at + normalField + 1] = normals[vertex * 3 + 1] ?? 0;
676
- interleaved[at + normalField + 2] = normals[vertex * 3 + 2] ?? 0;
677
- }
678
- target.queue.writeBuffer(vertices, 0, interleaved);
739
+ target.queue.writeBuffer(positionsBuffer, 0, positions);
740
+ if (normals !== undefined) target.queue.writeBuffer(normalsBuffer, 0, normals);
679
741
  };
680
742
  }
681
743
 
@@ -728,8 +790,20 @@ export function vertexBufferLayouts(
728
790
  let offset = 0;
729
791
 
730
792
  const layered = present['layers'] === true;
793
+ /* A dynamic mesh's positions and normals: buffers of their own, ahead of the rest. */
794
+ const dynamic = present['dynamic'] === true;
795
+ const layouts: GPUVertexBufferLayout[] = [];
731
796
  VERTEX_LAYOUT.forEach((declared, index) => {
732
797
  const attribute = laidOut(declared, layered);
798
+ if (dynamic && isStreamed(attribute)) {
799
+ layouts.push({
800
+ arrayStride: attribute.components * 4,
801
+ attributes: [
802
+ { shaderLocation: attribute.shaderLocation, offset: 0, format: attribute.format },
803
+ ],
804
+ });
805
+ return;
806
+ }
733
807
  /*
734
808
  * **The instanced pipeline reclaims the two skinning locations, and it is allowed to because
735
809
  * it cannot skin.** Locations 11 and 12 are the joint indices and weights; the INSTANCED
@@ -780,7 +854,7 @@ export function vertexBufferLayouts(
780
854
  });
781
855
  });
782
856
 
783
- const layouts: GPUVertexBufferLayout[] = [{ arrayStride: offset, attributes: interleaved }];
857
+ layouts.push({ arrayStride: offset, attributes: interleaved });
784
858
  /*
785
859
  * The constants buffer is always bound, even with nothing in it: the pipeline layout has
786
860
  * to match the buffers a draw sets, and a mesh supplying every attribute would otherwise
@@ -0,0 +1,161 @@
1
+ /**
2
+ * A skinned cloth's set-up as the buffers `shaders/clothSolve.wgsl.ts` reads: per-particle
3
+ * statics in sections, and every constraint as a record sorted by batch.
4
+ *
5
+ * **Sorted by batch so a batch is a range**: a dispatch runs records `begin` to `end`, and a
6
+ * multiplier sits at its record's index. The batches are `batchesOf`'s — the set-up's own where it
7
+ * gives them, coloured otherwise — the same ones the CPU solver walks, so the two solve each batch
8
+ * in the same order.
9
+ *
10
+ * **What a device cannot be handed is said another way here.** An infinite max distance is −1, an
11
+ * infinite stop distance is a stop of radius 0: shader compilers may assume no infinity, and a
12
+ * comparison against one is then whatever the optimiser made of it. Tethers are coloured too, with
13
+ * every particle counted as movable, so two tethers on one particle never run in one dispatch —
14
+ * which keeps the CPU's order for that particle while the rest run at once.
15
+ *
16
+ * A cloth with no rig is skinned by its model matrix alone: one joint, weight 1, whose matrix the
17
+ * solver uploads as the model.
18
+ */
19
+ import { batchesOf } from '@driftengine/physics';
20
+ import type { ClothBatches, SkinnedClothSetup } from '@driftengine/physics';
21
+
22
+ /** Floats of statics a particle, across its sections. */
23
+ export const CLOTH_STATIC_FLOATS = 29;
24
+
25
+ /** Bit flags of the limits a set-up has: a max distance, a backstop, a frontstop. */
26
+ export const CLOTH_LIMIT_MAX = 1;
27
+ export const CLOTH_LIMIT_BACK = 2;
28
+ export const CLOTH_LIMIT_FRONT = 4;
29
+
30
+ export interface PackedCloth {
31
+ readonly count: number;
32
+ /** inverse mass, max distance, backstop ×2, frontstop ×2, thickness, rest ×3, normal ×3, influences ×16. */
33
+ readonly statics: Float32Array;
34
+ /** Distance records (a, b, rest, compliance), bending (four ids, rest, compliance), tethers (particle, anchor, length). */
35
+ readonly constraints: Uint32Array;
36
+ readonly distances: number;
37
+ readonly bendings: number;
38
+ readonly tethers: number;
39
+ /** Each kind's batches as offsets into its sorted records: batch b is `[b]` up to `[b + 1]`. */
40
+ readonly distanceBatches: Uint32Array;
41
+ readonly bendingBatches: Uint32Array;
42
+ readonly tetherBatches: Uint32Array;
43
+ readonly limits: number;
44
+ /** Skin matrices the solver uploads a pose: the rig's joints, or one for the model alone. */
45
+ readonly joints: number;
46
+ readonly rigged: boolean;
47
+ readonly colliders: number;
48
+ }
49
+
50
+ /** Whether a set-up is skinned by a rig, as `ClothTargets` decides it. */
51
+ function riggedOf(setup: SkinnedClothSetup): boolean {
52
+ return (
53
+ setup.joints !== undefined &&
54
+ setup.weights !== undefined &&
55
+ (setup.inverseBind?.length ?? 0) >= 16
56
+ );
57
+ }
58
+
59
+ export function packCloth(setup: SkinnedClothSetup): PackedCloth {
60
+ const n = setup.positions.length / 3;
61
+ const inverseMass = setup.inverseMass;
62
+ const limits = setup.limits ?? {};
63
+ const statics = new Float32Array(n * CLOTH_STATIC_FLOATS);
64
+ statics.set(inverseMass, 0);
65
+ const rigged = riggedOf(setup);
66
+ for (let i = 0; i < n; i++) {
67
+ const max = limits.maxDistance?.[i] ?? Infinity;
68
+ statics[n + i] = max < Infinity ? max : -1;
69
+ for (const [stops, at] of [
70
+ [limits.backstop, 2 * n],
71
+ [limits.frontstop, 4 * n],
72
+ ] as const) {
73
+ const distance = stops?.[i * 2] ?? Infinity;
74
+ const radius = stops?.[i * 2 + 1] ?? 0;
75
+ const real = distance < Infinity && radius > 0;
76
+ statics[at + i * 2] = real ? distance : 0;
77
+ statics[at + i * 2 + 1] = real ? radius : 0;
78
+ }
79
+ statics[6 * n + i] = limits.thickness?.[i] ?? 0;
80
+ for (let k = 0; k < 3; k++) {
81
+ statics[7 * n + i * 3 + k] = setup.positions[i * 3 + k] as number;
82
+ statics[10 * n + i * 3 + k] = setup.normals?.[i * 3 + k] ?? (k === 2 ? 1 : 0);
83
+ }
84
+ const at = 13 * n + i * 16;
85
+ if (!rigged) {
86
+ statics[at + 4] = 1;
87
+ continue;
88
+ }
89
+ for (let k = 0; k < 4; k++) {
90
+ statics[at + k] = setup.joints?.[i * 4 + k] ?? 0;
91
+ statics[at + 4 + k] = setup.weights?.[i * 4 + k] ?? 0;
92
+ statics[at + 8 + k] = setup.joints2?.[i * 4 + k] ?? 0;
93
+ statics[at + 12 + k] = setup.weights2?.[i * 4 + k] ?? 0;
94
+ }
95
+ }
96
+
97
+ const { distance, bending, tethers } = setup;
98
+ const distances = distance.rest.length;
99
+ const bendings = bending?.rest.length ?? 0;
100
+ const tetherCount = tethers?.lengths.length ?? 0;
101
+ const words = new Uint32Array(distances * 4 + bendings * 6 + tetherCount * 3);
102
+ const floats = new Float32Array(words.buffer);
103
+
104
+ const distanceBatches = batchesOf('distance', distance.pairs, 2, inverseMass, distance.batches);
105
+ sorted(distanceBatches, (k, r) => {
106
+ const at = r * 4;
107
+ words[at] = distance.pairs[k * 2] as number;
108
+ words[at + 1] = distance.pairs[k * 2 + 1] as number;
109
+ floats[at + 2] = distance.rest[k] as number;
110
+ floats[at + 3] = distance.compliance[k] as number;
111
+ });
112
+
113
+ let bendingBatches: Uint32Array = new Uint32Array([0]);
114
+ if (bending !== undefined) {
115
+ const batches = batchesOf('bending', bending.quads, 4, inverseMass, bending.batches);
116
+ bendingBatches = batches.batches;
117
+ sorted(batches, (k, r) => {
118
+ const at = distances * 4 + r * 6;
119
+ for (let j = 0; j < 4; j++) words[at + j] = bending.quads[k * 4 + j] as number;
120
+ floats[at + 4] = bending.rest[k] as number;
121
+ floats[at + 5] = bending.compliance[k] as number;
122
+ });
123
+ }
124
+
125
+ let tetherBatches: Uint32Array = new Uint32Array([0]);
126
+ if (tethers !== undefined) {
127
+ const movable = new Float32Array(n).fill(1);
128
+ const batches = batchesOf('tethers', tethers.particles, 1, movable, undefined);
129
+ tetherBatches = batches.batches;
130
+ sorted(batches, (k, r) => {
131
+ const at = distances * 4 + bendings * 6 + r * 3;
132
+ words[at] = tethers.particles[k] as number;
133
+ words[at + 1] = tethers.anchors[k] as number;
134
+ floats[at + 2] = tethers.lengths[k] as number;
135
+ });
136
+ }
137
+
138
+ return {
139
+ count: n,
140
+ statics,
141
+ constraints: words,
142
+ distances,
143
+ bendings,
144
+ tethers: tetherCount,
145
+ distanceBatches: distanceBatches.batches,
146
+ bendingBatches,
147
+ tetherBatches,
148
+ limits:
149
+ (limits.maxDistance !== undefined ? CLOTH_LIMIT_MAX : 0) |
150
+ (limits.backstop !== undefined ? CLOTH_LIMIT_BACK : 0) |
151
+ (limits.frontstop !== undefined ? CLOTH_LIMIT_FRONT : 0),
152
+ joints: rigged ? (setup.inverseBind?.length ?? 0) / 16 : 1,
153
+ rigged,
154
+ colliders: setup.colliders?.length ?? 0,
155
+ };
156
+ }
157
+
158
+ /** Each constraint `k` with the place `r` it takes in batch order. */
159
+ function sorted(batches: ClothBatches, write: (k: number, r: number) => void): void {
160
+ for (let r = 0; r < batches.order.length; r++) write(batches.order[r] as number, r);
161
+ }