@driftengine/core 4.9.0 → 4.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (231) hide show
  1. package/CHANGELOG.json +133 -11
  2. package/README.md +43 -38
  3. package/dist/index.d.ts +7 -4
  4. package/dist/index.js +5 -4
  5. package/dist/render/ambientHarmonics.d.ts +1 -1
  6. package/dist/render/ambientHarmonics.js +1 -1
  7. package/dist/render/areaLights.d.ts +4 -4
  8. package/dist/render/areaLights.js +1 -1
  9. package/dist/render/backend/api.d.ts +10 -3
  10. package/dist/render/backend/select.js +11 -5
  11. package/dist/render/backend/webgl2/boneAnimations.d.ts +29 -0
  12. package/dist/render/backend/webgl2/boneAnimations.js +80 -0
  13. package/dist/render/backend/webgl2/instanced.d.ts +5 -1
  14. package/dist/render/backend/webgl2/instanced.js +13 -1
  15. package/dist/render/backend/webgl2/renderer.d.ts +189 -4
  16. package/dist/render/backend/webgl2/renderer.js +614 -66
  17. package/dist/render/backend/webgl2/skinPaletteTexture.d.ts +5 -3
  18. package/dist/render/backend/webgl2/skinPaletteTexture.js +6 -4
  19. package/dist/render/backend/webgpu/boneAnimations.d.ts +34 -0
  20. package/dist/render/backend/webgpu/boneAnimations.js +76 -0
  21. package/dist/render/backend/webgpu/buffers.d.ts +24 -4
  22. package/dist/render/backend/webgpu/buffers.js +116 -59
  23. package/dist/render/backend/webgpu/clothKernels.d.ts +8 -0
  24. package/dist/render/backend/webgpu/clothKernels.js +76 -0
  25. package/dist/render/backend/webgpu/clothOps.d.ts +39 -0
  26. package/dist/render/backend/webgpu/clothOps.js +78 -0
  27. package/dist/render/backend/webgpu/clothSetLayout.d.ts +25 -0
  28. package/dist/render/backend/webgpu/clothSetLayout.js +84 -0
  29. package/dist/render/backend/webgpu/clothSetPack.d.ts +33 -0
  30. package/dist/render/backend/webgpu/clothSetPack.js +144 -0
  31. package/dist/render/backend/webgpu/compressedUpload.d.ts +0 -1
  32. package/dist/render/backend/webgpu/compressedUpload.js +53 -8
  33. package/dist/render/backend/webgpu/depthPass.d.ts +59 -3
  34. package/dist/render/backend/webgpu/depthPass.js +49 -14
  35. package/dist/render/backend/webgpu/drawCommand.d.ts +20 -4
  36. package/dist/render/backend/webgpu/drawCommand.js +11 -3
  37. package/dist/render/backend/webgpu/flatPass.d.ts +85 -42
  38. package/dist/render/backend/webgpu/flatPass.js +137 -20
  39. package/dist/render/backend/webgpu/gpuClothSet.d.ts +80 -0
  40. package/dist/render/backend/webgpu/gpuClothSet.js +462 -0
  41. package/dist/render/backend/webgpu/instanced.d.ts +14 -1
  42. package/dist/render/backend/webgpu/instanced.js +24 -1
  43. package/dist/render/backend/webgpu/latePass.js +1 -0
  44. package/dist/render/backend/webgpu/materialStore.d.ts +101 -0
  45. package/dist/render/backend/webgpu/materialStore.js +265 -0
  46. package/dist/render/backend/webgpu/motionPass.js +15 -6
  47. package/dist/render/backend/webgpu/pipelineCache.d.ts +28 -1
  48. package/dist/render/backend/webgpu/pipelineCache.js +46 -1
  49. package/dist/render/backend/webgpu/postPass.d.ts +1 -1
  50. package/dist/render/backend/webgpu/renderer.d.ts +200 -35
  51. package/dist/render/backend/webgpu/renderer.js +993 -168
  52. package/dist/render/backend/webgpu/shaders/clothSolve.wgsl.d.ts +9 -2
  53. package/dist/render/backend/webgpu/shaders/clothSolve.wgsl.js +121 -47
  54. package/dist/render/backend/webgpu/skinPaletteRing.d.ts +7 -3
  55. package/dist/render/backend/webgpu/skinPaletteRing.js +13 -7
  56. package/dist/render/backend/webgpu/skinPaletteTexture.d.ts +6 -0
  57. package/dist/render/backend/webgpu/skinPaletteTexture.js +7 -1
  58. package/dist/render/backend/webgpu/staticBundles.d.ts +125 -0
  59. package/dist/render/backend/webgpu/staticBundles.js +242 -0
  60. package/dist/render/backend/webgpu/surfaceTexturePass.d.ts +10 -3
  61. package/dist/render/backend/webgpu/surfaceTexturePass.js +20 -2
  62. package/dist/render/backend/webgpu/uniformRing.d.ts +21 -0
  63. package/dist/render/backend/webgpu/uniformRing.js +9 -1
  64. package/dist/render/backend/webgpu/vertexFormats.d.ts +22 -0
  65. package/dist/render/backend/webgpu/vertexFormats.js +97 -0
  66. package/dist/render/bloomChain.d.ts +3 -3
  67. package/dist/render/boneAnimation.d.ts +92 -0
  68. package/dist/render/boneAnimation.js +133 -0
  69. package/dist/render/compressedSource.d.ts +33 -13
  70. package/dist/render/compressedSource.js +70 -25
  71. package/dist/render/glCompressed.js +83 -0
  72. package/dist/render/globalMediumPass.d.ts +2 -0
  73. package/dist/render/globalMediumPass.js +1 -0
  74. package/dist/render/indexWidth.d.ts +17 -0
  75. package/dist/render/indexWidth.js +23 -0
  76. package/dist/render/instances.d.ts +26 -0
  77. package/dist/render/lightBudget.d.ts +10 -0
  78. package/dist/render/lightBudget.js +10 -0
  79. package/dist/render/lightMatrix.d.ts +11 -0
  80. package/dist/render/lightMatrix.js +14 -0
  81. package/dist/render/lightmap.d.ts +1 -1
  82. package/dist/render/mesh.d.ts +14 -0
  83. package/dist/render/mesh.js +44 -14
  84. package/dist/render/pointLightSelection.d.ts +4 -4
  85. package/dist/render/renderQuality.d.ts +30 -0
  86. package/dist/render/renderQuality.js +5 -0
  87. package/dist/render/rendererHarness.d.ts +7 -0
  88. package/dist/render/rendererHarness.js +12 -0
  89. package/dist/render/screenSpaceReflection.d.ts +2 -1
  90. package/dist/render/shader.d.ts +15 -0
  91. package/dist/render/shader.js +41 -0
  92. package/dist/render/shaders/boneAnimation.d.ts +19 -0
  93. package/dist/render/shaders/boneAnimation.js +63 -0
  94. package/dist/render/shaders/depth.d.ts +7 -0
  95. package/dist/render/shaders/depth.js +35 -10
  96. package/dist/render/shaders/flat/directionalShadow.js +84 -10
  97. package/dist/render/shaders/flat/glassTint.d.ts +1 -1
  98. package/dist/render/shaders/flat/glassTint.js +3 -0
  99. package/dist/render/shaders/flat/index.d.ts +21 -0
  100. package/dist/render/shaders/flat/index.js +42 -7
  101. package/dist/render/shaders/flat/layered.d.ts +19 -0
  102. package/dist/render/shaders/flat/layered.js +115 -0
  103. package/dist/render/shaders/flat/main.d.ts +1 -1
  104. package/dist/render/shaders/flat/main.js +58 -21
  105. package/dist/render/shaders/flat/mapReads.d.ts +10 -0
  106. package/dist/render/shaders/flat/mapReads.js +38 -0
  107. package/dist/render/shaders/flat/models.js +1 -1
  108. package/dist/render/shaders/flat/movingSun.d.ts +21 -0
  109. package/dist/render/shaders/flat/movingSun.js +46 -0
  110. package/dist/render/shaders/flat/normalMap.d.ts +6 -2
  111. package/dist/render/shaders/flat/normalMap.js +14 -3
  112. package/dist/render/shaders/flat/overlay.d.ts +3 -2
  113. package/dist/render/shaders/flat/overlay.js +42 -20
  114. package/dist/render/shaders/flat/pointShadow.js +2 -2
  115. package/dist/render/shaders/flat/preamble.js +59 -27
  116. package/dist/render/shaders/flat/worldUv.d.ts +22 -0
  117. package/dist/render/shaders/flat/worldUv.js +113 -0
  118. package/dist/render/shaders/generated/depth.wgsl.d.ts +93 -0
  119. package/dist/render/shaders/generated/depth.wgsl.js +93 -0
  120. package/dist/render/shaders/generated/flat.wgsl.d.ts +3616 -3042
  121. package/dist/render/shaders/generated/flat.wgsl.js +3688 -3113
  122. package/dist/render/shaders/generated/globalMedium.wgsl.d.ts +8 -3
  123. package/dist/render/shaders/generated/globalMedium.wgsl.js +8 -3
  124. package/dist/render/shaders/generated/lightVolume.wgsl.d.ts +15 -10
  125. package/dist/render/shaders/generated/lightVolume.wgsl.js +16 -11
  126. package/dist/render/shaders/globalMedium.js +9 -1
  127. package/dist/render/shaders/lightVolume.js +9 -1
  128. package/dist/render/shaders/outputTransform.d.ts +7 -0
  129. package/dist/render/shaders/outputTransform.js +28 -14
  130. package/dist/render/shaders/tangentFrame.d.ts +1 -1
  131. package/dist/render/shaders/tangentFrame.js +16 -3
  132. package/dist/render/shaders/vertexChannel.d.ts +7 -0
  133. package/dist/render/shaders/vertexChannel.js +26 -0
  134. package/dist/render/skinnedClothRun.d.ts +28 -0
  135. package/dist/render/skinnedClothRun.js +103 -35
  136. package/dist/render/staticDraws.d.ts +47 -0
  137. package/dist/render/staticDraws.js +43 -0
  138. package/dist/render/surfaceLayers.d.ts +39 -0
  139. package/dist/render/surfaceLayers.js +26 -0
  140. package/dist/render/surfaceOverlay.d.ts +16 -3
  141. package/dist/render/surfaceOverlay.js +11 -6
  142. package/dist/render/surfaceProjection.d.ts +34 -0
  143. package/dist/render/surfaceProjection.js +30 -0
  144. package/dist/render/surfaceTexture.d.ts +68 -7
  145. package/dist/render/surfaceTexture.js +56 -17
  146. package/dist/render/vertexPacking.d.ts +48 -0
  147. package/dist/render/vertexPacking.js +68 -0
  148. package/package.json +6 -6
  149. package/src/index.ts +12 -4
  150. package/src/render/ambientHarmonics.ts +1 -1
  151. package/src/render/areaLights.ts +4 -4
  152. package/src/render/backend/api.ts +13 -3
  153. package/src/render/backend/select.ts +10 -5
  154. package/src/render/backend/webgl2/boneAnimations.ts +105 -0
  155. package/src/render/backend/webgl2/instanced.ts +20 -1
  156. package/src/render/backend/webgl2/renderer.ts +723 -79
  157. package/src/render/backend/webgl2/skinPaletteTexture.ts +6 -4
  158. package/src/render/backend/webgpu/boneAnimations.ts +97 -0
  159. package/src/render/backend/webgpu/buffers.ts +195 -70
  160. package/src/render/backend/webgpu/clothKernels.ts +86 -0
  161. package/src/render/backend/webgpu/clothOps.ts +86 -0
  162. package/src/render/backend/webgpu/clothSetLayout.ts +102 -0
  163. package/src/render/backend/webgpu/clothSetPack.ts +197 -0
  164. package/src/render/backend/webgpu/compressedUpload.ts +59 -9
  165. package/src/render/backend/webgpu/depthPass.ts +51 -10
  166. package/src/render/backend/webgpu/drawCommand.ts +25 -4
  167. package/src/render/backend/webgpu/flatPass.ts +212 -41
  168. package/src/render/backend/webgpu/gpuClothSet.ts +564 -0
  169. package/src/render/backend/webgpu/instanced.ts +32 -1
  170. package/src/render/backend/webgpu/latePass.ts +1 -0
  171. package/src/render/backend/webgpu/materialStore.ts +276 -0
  172. package/src/render/backend/webgpu/motionPass.ts +18 -7
  173. package/src/render/backend/webgpu/pipelineCache.ts +53 -7
  174. package/src/render/backend/webgpu/renderer.ts +1201 -202
  175. package/src/render/backend/webgpu/shaders/clothSolve.wgsl.ts +121 -47
  176. package/src/render/backend/webgpu/skinPaletteRing.ts +13 -7
  177. package/src/render/backend/webgpu/skinPaletteTexture.ts +7 -1
  178. package/src/render/backend/webgpu/staticBundles.ts +364 -0
  179. package/src/render/backend/webgpu/surfaceTexturePass.ts +29 -3
  180. package/src/render/backend/webgpu/uniformRing.ts +10 -1
  181. package/src/render/backend/webgpu/vertexFormats.ts +113 -0
  182. package/src/render/bloomChain.ts +3 -3
  183. package/src/render/boneAnimation.ts +223 -0
  184. package/src/render/compressedSource.ts +121 -29
  185. package/src/render/glCompressed.ts +83 -0
  186. package/src/render/globalMediumPass.ts +7 -0
  187. package/src/render/indexWidth.ts +27 -0
  188. package/src/render/instances.ts +27 -0
  189. package/src/render/lightBudget.ts +11 -0
  190. package/src/render/lightMatrix.ts +15 -0
  191. package/src/render/lightmap.ts +1 -1
  192. package/src/render/mesh.ts +72 -14
  193. package/src/render/pointLightSelection.ts +4 -4
  194. package/src/render/renderQuality.ts +38 -0
  195. package/src/render/rendererHarness.ts +15 -0
  196. package/src/render/screenSpaceReflection.ts +2 -1
  197. package/src/render/shader.ts +59 -0
  198. package/src/render/shaders/boneAnimation.ts +63 -0
  199. package/src/render/shaders/depth.ts +45 -10
  200. package/src/render/shaders/flat/directionalShadow.ts +84 -10
  201. package/src/render/shaders/flat/glassTint.ts +7 -1
  202. package/src/render/shaders/flat/index.ts +63 -7
  203. package/src/render/shaders/flat/layered.ts +115 -0
  204. package/src/render/shaders/flat/main.ts +58 -21
  205. package/src/render/shaders/flat/mapReads.ts +38 -0
  206. package/src/render/shaders/flat/models.ts +1 -1
  207. package/src/render/shaders/flat/movingSun.ts +46 -0
  208. package/src/render/shaders/flat/normalMap.ts +14 -3
  209. package/src/render/shaders/flat/overlay.ts +42 -20
  210. package/src/render/shaders/flat/pointShadow.ts +2 -2
  211. package/src/render/shaders/flat/preamble.ts +59 -27
  212. package/src/render/shaders/flat/worldUv.ts +113 -0
  213. package/src/render/shaders/generated/depth.wgsl.ts +95 -0
  214. package/src/render/shaders/generated/flat.wgsl.ts +3688 -3113
  215. package/src/render/shaders/generated/globalMedium.wgsl.ts +8 -3
  216. package/src/render/shaders/generated/lightVolume.wgsl.ts +16 -11
  217. package/src/render/shaders/globalMedium.ts +9 -1
  218. package/src/render/shaders/lightVolume.ts +9 -1
  219. package/src/render/shaders/outputTransform.ts +30 -14
  220. package/src/render/shaders/tangentFrame.ts +16 -3
  221. package/src/render/shaders/vertexChannel.ts +27 -0
  222. package/src/render/skinnedClothRun.ts +138 -38
  223. package/src/render/staticDraws.ts +95 -0
  224. package/src/render/surfaceLayers.ts +57 -0
  225. package/src/render/surfaceOverlay.ts +24 -7
  226. package/src/render/surfaceProjection.ts +57 -0
  227. package/src/render/surfaceTexture.ts +124 -27
  228. package/src/render/vertexPacking.ts +109 -0
  229. package/dist/render/backend/webgpu/gpuSkinnedCloth.d.ts +0 -74
  230. package/dist/render/backend/webgpu/gpuSkinnedCloth.js +0 -452
  231. package/src/render/backend/webgpu/gpuSkinnedCloth.ts +0 -502
@@ -7,7 +7,7 @@
7
7
  * each kernel there is an entry point here, one invocation a particle or a constraint, and the loop
8
8
  * over a batch is the dispatch. `clothPose.ts`' `skinParticles` is `pose`. The order of a step —
9
9
  * begin, then per substep predict, the batches, the limits, finish; then the blend — is
10
- * `gpuSkinnedCloth.ts`', taken from the same `ClothControl` the CPU solver is driven by.
10
+ * `gpuClothSet.ts`', taken from the same `ClothControl` the CPU solver is driven by.
11
11
  *
12
12
  * **Where it differs, it says so.** Single precision throughout, where the CPU rounds to single
13
13
  * only on a store. And the bending angle is `atan2(|n1 × n2|, n1 · n2)` rather than `acos(n1 · n2)`
@@ -28,6 +28,13 @@
28
28
  * start; `pose` the two skinned poses a step interpolates between and the latest skinned normals;
29
29
  * `statics` everything per particle that never changes. A section's offset is a multiple of the
30
30
  * particle count, which the constants carry — so no offset is a uniform to keep in step.
31
+ *
32
+ * **One dispatch serves every garment of a set** (`clothSetPack.ts`): the particles, constraints,
33
+ * joints and colliders of all of them in those buffers, and each garment's numbers — its step's
34
+ * fraction and blend, its damping, gravity and limits — a record of its own in `paces`, at the
35
+ * round's place. A kernel finds its garment from the particle, and returns at once where that
36
+ * garment has nothing to do this round: a garment that took fewer steps, or one settling alone after
37
+ * a reset, sits the round out while the rest of the set runs it.
31
38
  */
32
39
  export declare const CLOTH_WORKGROUP = 64;
33
- export declare const CLOTH_SOLVE_WGSL = "\nstruct Constants {\n count: u32,\n distances: u32,\n bendings: u32,\n joints: u32,\n colliders: u32,\n /* 1 a max distance, 2 a backstop, 4 a frontstop: which limits the set-up has at all. */\n limits: u32,\n width: u32,\n pad0: u32,\n h: f32,\n invH: f32,\n invH2: f32,\n keep: f32,\n dragKeep: f32,\n maxDistanceScale: f32,\n margin: f32,\n pad1: f32,\n gravity: vec4<f32>,\n}\n\n/* One step's numbers, at a dynamic offset: every step of a frame has its own slot. */\nstruct Step {\n fraction: f32,\n blend: f32,\n alpha: f32,\n carry: u32,\n /* Where the pose a step starts from and the one it moves toward sit in pose, in floats. */\n fromAt: u32,\n toAt: u32,\n pad0: u32,\n pad1: u32,\n /* The character's turn since the last step, by columns; origin.w is the share carried. */\n turn0: vec4<f32>,\n turn1: vec4<f32>,\n turn2: vec4<f32>,\n origin: vec4<f32>,\n shift: vec4<f32>,\n /* The frame's wind: one wind, sampled once a frame, so a step's rather than the set-up's. */\n wind: vec4<f32>,\n}\n\n/* One batch of one kind of constraint, at a dynamic offset; first resets the multipliers. */\nstruct Batch {\n begin: u32,\n end: u32,\n first: u32,\n pad: u32,\n}\n\n@group(0) @binding(0) var<uniform> c: Constants;\n@group(0) @binding(1) var<uniform> pace: Step;\n@group(0) @binding(2) var<uniform> batch: Batch;\n@group(0) @binding(3) var<storage, read_write> state: array<f32>;\n@group(0) @binding(4) var<storage, read_write> pose: array<f32>;\n@group(0) @binding(5) var<storage, read> statics: array<f32>;\n@group(0) @binding(6) var<storage, read> constraints: array<u32>;\n@group(0) @binding(7) var<storage, read_write> lambda: array<f32>;\n@group(0) @binding(8) var<storage, read> frame: array<f32>;\n\nfn stateAt(section: u32, i: u32) -> u32 {\n return section * 3u * c.count + i * 3u;\n}\n\nfn load(at: u32) -> vec3<f32> {\n return vec3<f32>(state[at], state[at + 1u], state[at + 2u]);\n}\n\nfn store(at: u32, v: vec3<f32>) {\n state[at] = v.x;\n state[at + 1u] = v.y;\n state[at + 2u] = v.z;\n}\n\nfn poseOf(at: u32) -> vec3<f32> {\n return vec3<f32>(pose[at], pose[at + 1u], pose[at + 2u]);\n}\n\n/* Sections of state. */\nconst POSITION = 0u;\nconst PREVIOUS = 1u;\nconst VELOCITY = 2u;\nconst START = 3u;\n\nfn inverseMass(i: u32) -> f32 {\n return statics[i];\n}\n\n/* Where skinning puts particle i, fraction of the way from the last pose to the latest. */\nfn skinnedAt(i: u32) -> vec3<f32> {\n let a = poseOf(pace.fromAt + i * 3u);\n let b = poseOf(pace.toAt + i * 3u);\n return a + (b - a) * pace.fraction;\n}\n\nfn skinMatrix(j: u32) -> mat4x4<f32> {\n let at = j * 16u;\n return mat4x4<f32>(\n vec4<f32>(frame[at], frame[at + 1u], frame[at + 2u], frame[at + 3u]),\n vec4<f32>(frame[at + 4u], frame[at + 5u], frame[at + 6u], frame[at + 7u]),\n vec4<f32>(frame[at + 8u], frame[at + 9u], frame[at + 10u], frame[at + 11u]),\n vec4<f32>(frame[at + 12u], frame[at + 13u], frame[at + 14u], frame[at + 15u]));\n}\n\n/* skinParticles: each particle's skinned position and normal by up to eight influences. */\n@compute @workgroup_size(64)\nfn clothPose(@builtin(global_invocation_id) id: vec3<u32>) {\n let i = id.x;\n if (i >= c.count) {\n return;\n }\n let n = c.count;\n let restAt = 7u * n + i * 3u;\n let normalAt = 10u * n + i * 3u;\n let x = vec4<f32>(statics[restAt], statics[restAt + 1u], statics[restAt + 2u], 1.0);\n let q0 = vec3<f32>(statics[normalAt], statics[normalAt + 1u], statics[normalAt + 2u]);\n var p = vec3<f32>(0.0);\n var q = vec3<f32>(0.0);\n for (var half = 0u; half < 2u; half++) {\n for (var k = 0u; k < 4u; k++) {\n let at = 13u * n + i * 16u + half * 8u;\n let w = statics[at + 4u + k];\n if (w == 0.0) {\n continue;\n }\n let m = skinMatrix(u32(statics[at + k]));\n p += w * (m * x).xyz;\n q += w * (mat3x3<f32>(m[0].xyz, m[1].xyz, m[2].xyz) * q0);\n }\n }\n let to = pace.toAt + i * 3u;\n pose[to] = p.x;\n pose[to + 1u] = p.y;\n pose[to + 2u] = p.z;\n let len = sqrt(dot(q, q));\n let inv = select(0.0, 1.0 / len, len > 1e-12);\n let normal = 6u * n + i * 3u;\n pose[normal] = q.x * inv;\n pose[normal + 1u] = q.y * inv;\n pose[normal + 2u] = q.z * inv;\n}\n\n/* SkinnedCloth.rest: every particle at the latest skinned position, at rest. */\n@compute @workgroup_size(64)\nfn clothRest(@builtin(global_invocation_id) id: vec3<u32>) {\n let i = id.x;\n if (i >= c.count) {\n return;\n }\n let p = poseOf(pace.toAt + i * 3u);\n store(stateAt(POSITION, i), p);\n store(stateAt(START, i), p);\n store(stateAt(VELOCITY, i), vec3<f32>(0.0));\n}\n\n/* The step's start: the step start kept, a free particle carried, a kinematic one placed. */\n@compute @workgroup_size(64)\nfn clothBegin(@builtin(global_invocation_id) id: vec3<u32>) {\n let i = id.x;\n if (i >= c.count) {\n return;\n }\n let at = stateAt(POSITION, i);\n let p = load(at);\n store(stateAt(START, i), p);\n if (inverseMass(i) == 0.0) {\n store(at, skinnedAt(i));\n return;\n }\n if (pace.carry == 0u) {\n return;\n }\n /* carryParticles: the share of the character's turn about its old origin, and of its move. */\n let turn = mat3x3<f32>(pace.turn0.xyz, pace.turn1.xyz, pace.turn2.xyz);\n let share = pace.origin.w;\n let x = p - pace.origin.xyz;\n store(at, p + (turn * x - x) * share + pace.shift.xyz);\n let vAt = stateAt(VELOCITY, i);\n let v = load(vAt);\n store(vAt, v + (turn * v - v) * share);\n}\n\n/* predictParticles. */\n@compute @workgroup_size(64)\nfn clothPredict(@builtin(global_invocation_id) id: vec3<u32>) {\n let i = id.x;\n if (i >= c.count) {\n return;\n }\n let at = stateAt(POSITION, i);\n let p = load(at);\n store(stateAt(PREVIOUS, i), p);\n if (inverseMass(i) == 0.0) {\n return;\n }\n let vAt = stateAt(VELOCITY, i);\n let air = pace.wind.xyz;\n let v = air + (load(vAt) * c.keep - air) * c.dragKeep + c.gravity.xyz * c.h;\n store(vAt, v);\n store(at, p + v * c.h);\n}\n\n/* solveDistanceBatch: one invocation a constraint of the batch, constraints sorted by batch. */\n@compute @workgroup_size(64)\nfn clothDistance(@builtin(global_invocation_id) id: vec3<u32>) {\n let k = batch.begin + id.x;\n if (k >= batch.end) {\n return;\n }\n /* The first pass of a substep starts the multiplier at nothing, as the CPU's fill(0) does \u2014\n written before any early return, so a skipped constraint does not carry the last one. */\n if (batch.first == 1u) {\n lambda[k] = 0.0;\n }\n let a = constraints[k * 4u];\n let b = constraints[k * 4u + 1u];\n let wa = inverseMass(a);\n let wb = inverseMass(b);\n let w = wa + wb;\n if (w == 0.0) {\n return;\n }\n let aAt = stateAt(POSITION, a);\n let bAt = stateAt(POSITION, b);\n let d = load(bAt) - load(aAt);\n let len = sqrt(dot(d, d));\n if (len < 1e-9) {\n return;\n }\n let alpha = bitcast<f32>(constraints[k * 4u + 3u]) * c.invH2;\n let old = lambda[k];\n let delta = (-(len - bitcast<f32>(constraints[k * 4u + 2u])) - alpha * old) / (w + alpha);\n lambda[k] = old + delta;\n let s = delta / len;\n /* A pinned particle is shared across a batch, so it is never written, even with itself. */\n if (wa > 0.0) {\n store(aAt, load(aAt) - d * s * wa);\n }\n if (wb > 0.0) {\n store(bAt, load(bAt) + d * s * wb);\n }\n}\n\n/* solveBendingBatch, with the angle from atan2 (see the header). */\n@compute @workgroup_size(64)\nfn clothBending(@builtin(global_invocation_id) id: vec3<u32>) {\n let k = batch.begin + id.x;\n if (k >= batch.end) {\n return;\n }\n let at = c.distances * 4u + k * 6u;\n let slot = c.distances + k;\n if (batch.first == 1u) {\n lambda[slot] = 0.0;\n }\n let ids = vec4<u32>(constraints[at], constraints[at + 1u], constraints[at + 2u], constraints[at + 3u]);\n let w = vec4<f32>(inverseMass(ids.x), inverseMass(ids.y), inverseMass(ids.z), inverseMass(ids.w));\n if (w.x + w.y + w.z + w.w == 0.0) {\n return;\n }\n let origin = load(stateAt(POSITION, ids.x));\n let e = load(stateAt(POSITION, ids.y)) - origin;\n let p3 = load(stateAt(POSITION, ids.z)) - origin;\n let p4 = load(stateAt(POSITION, ids.w)) - origin;\n var n1 = cross(e, p3);\n var n2 = cross(e, p4);\n let l1 = sqrt(dot(n1, n1));\n let l2 = sqrt(dot(n2, n2));\n if (l1 < 1e-12 || l2 < 1e-12) {\n return;\n }\n n1 = n1 / l1;\n n2 = n2 / l2;\n let d = clamp(dot(n1, n2), -1.0, 1.0);\n let sine = sqrt(dot(cross(n1, n2), cross(n1, n2)));\n if (sine < 1e-6) {\n return;\n }\n /* The q vectors: the negative of the derivative of n1 \u00B7 n2. See solveBendingBatch. */\n let q3 = (cross(e, n2) + cross(n1, e) * d) / l1;\n let q4 = (cross(e, n1) + cross(n2, e) * d) / l2;\n let q2 = -((cross(p3, n2) + cross(n1, p3) * d) / l1 + (cross(p4, n1) + cross(n2, p4) * d) / l2);\n let q1 = -(q2 + q3 + q4);\n var sum = w.x * dot(q1, q1) + w.y * dot(q2, q2) + w.z * dot(q3, q3) + w.w * dot(q4, q4);\n sum = sum / (sine * sine);\n if (sum < 1e-12) {\n return;\n }\n let alpha = bitcast<f32>(constraints[at + 5u]) * c.invH2;\n let old = lambda[slot];\n let angle = atan2(sine, d);\n let delta = (-(angle - bitcast<f32>(constraints[at + 4u])) - alpha * old) / (sum + alpha);\n lambda[slot] = old + delta;\n let scale = delta / sine;\n if (w.x > 0.0) {\n let p = stateAt(POSITION, ids.x);\n store(p, load(p) + w.x * scale * q1);\n }\n if (w.y > 0.0) {\n let p = stateAt(POSITION, ids.y);\n store(p, load(p) + w.y * scale * q2);\n }\n if (w.z > 0.0) {\n let p = stateAt(POSITION, ids.z);\n store(p, load(p) + w.z * scale * q3);\n }\n if (w.w > 0.0) {\n let p = stateAt(POSITION, ids.w);\n store(p, load(p) + w.w * scale * q4);\n }\n}\n\n/* solveTethers, one invocation a tether of the batch: a batch holds a particle once. */\n@compute @workgroup_size(64)\nfn clothTether(@builtin(global_invocation_id) id: vec3<u32>) {\n let k = batch.begin + id.x;\n if (k >= batch.end) {\n return;\n }\n let at = c.distances * 4u + c.bendings * 6u + k * 3u;\n let pAt = stateAt(POSITION, constraints[at]);\n let anchor = load(stateAt(POSITION, constraints[at + 1u]));\n let d = load(pAt) - anchor;\n let dist = sqrt(dot(d, d));\n let reach = bitcast<f32>(constraints[at + 2u]);\n if (dist <= reach || dist < 1e-9) {\n return;\n }\n store(pAt, anchor + d * (reach / dist));\n}\n\n/* pushOut: the point onto the sphere's surface if it is inside. A point at the centre stays. */\nfn pushOut(p: vec3<f32>, centre: vec3<f32>, radius: f32) -> vec3<f32> {\n let d = p - centre;\n let d2 = dot(d, d);\n if (d2 >= radius * radius || d2 < 1e-18) {\n return p;\n }\n return centre + d * (radius / sqrt(d2));\n}\n\n/* applyMaxDistance, applyStops (back, then front) and applyColliders, in that order. */\n@compute @workgroup_size(64)\nfn clothLimit(@builtin(global_invocation_id) id: vec3<u32>) {\n let i = id.x;\n if (i >= c.count || inverseMass(i) == 0.0) {\n return;\n }\n let n = c.count;\n let at = stateAt(POSITION, i);\n var p = load(at);\n let t = skinnedAt(i);\n /* A max distance below zero is none: the CPU's Infinity, which a device may not be handed. */\n let maxDistance = statics[n + i];\n if ((c.limits & 1u) != 0u && maxDistance >= 0.0) {\n let limit = maxDistance * c.maxDistanceScale;\n let d = p - t;\n let d2 = dot(d, d);\n if (d2 > limit * limit) {\n p = t + d * (limit / sqrt(d2));\n }\n }\n let normal = poseOf(6u * n + i * 3u);\n if ((c.limits & 2u) != 0u) {\n let radius = statics[2u * n + i * 2u + 1u];\n if (radius > 0.0) {\n p = pushOut(p, t - normal * (statics[2u * n + i * 2u] + radius), radius);\n }\n }\n if ((c.limits & 4u) != 0u) {\n let radius = statics[4u * n + i * 2u + 1u];\n if (radius > 0.0) {\n p = pushOut(p, t + normal * (statics[4u * n + i * 2u] + radius), radius);\n }\n }\n let pad = statics[6u * n + i] + c.margin;\n let ends = c.joints * 16u;\n let radii = ends + c.colliders * 6u;\n for (var k = 0u; k < c.colliders; k++) {\n let a = vec3<f32>(frame[ends + k * 6u], frame[ends + k * 6u + 1u], frame[ends + k * 6u + 2u]);\n let e = vec3<f32>(frame[ends + k * 6u + 3u], frame[ends + k * 6u + 4u], frame[ends + k * 6u + 5u]) - a;\n let e2 = dot(e, e);\n var s = 0.0;\n if (e2 > 1e-18) {\n s = clamp(dot(p - a, e) / e2, 0.0, 1.0);\n }\n let r0 = frame[radii + k * 2u];\n let r1 = frame[radii + k * 2u + 1u];\n p = pushOut(p, a + e * s, r0 + (r1 - r0) * s + pad);\n }\n store(at, p);\n}\n\n/* finishParticles. */\n@compute @workgroup_size(64)\nfn clothFinish(@builtin(global_invocation_id) id: vec3<u32>) {\n let i = id.x;\n if (i >= c.count) {\n return;\n }\n let vAt = stateAt(VELOCITY, i);\n if (inverseMass(i) == 0.0) {\n store(vAt, vec3<f32>(0.0));\n return;\n }\n store(vAt, (load(stateAt(POSITION, i)) - load(stateAt(PREVIOUS, i))) * c.invH);\n}\n\n/* SkinnedCloth.blend: after a reset, the simulation's share grows over the blend's steps. */\n@compute @workgroup_size(64)\nfn clothBlend(@builtin(global_invocation_id) id: vec3<u32>) {\n let i = id.x;\n if (i >= c.count) {\n return;\n }\n let t = skinnedAt(i);\n let at = stateAt(POSITION, i);\n store(at, t + (load(at) - t) * pace.blend);\n let vAt = stateAt(VELOCITY, i);\n store(vAt, load(vAt) * pace.blend);\n}\n\n@group(1) @binding(0) var output: texture_storage_2d<rgba32float, write>;\n\n/* SkinnedCloth.interpolate, into the particle texture the vertex stage reads. */\n@compute @workgroup_size(64)\nfn clothPublish(@builtin(global_invocation_id) id: vec3<u32>) {\n let i = id.x;\n if (i >= c.count) {\n return;\n }\n let a = load(stateAt(START, i));\n let p = a + (load(stateAt(POSITION, i)) - a) * pace.alpha;\n textureStore(output, vec2<u32>(i % c.width, i / c.width), vec4<f32>(p, 0.0));\n}\n";
40
+ export declare const CLOTH_SOLVE_WGSL = "\n/* The set's: every garment's particles, records, skin matrices and colliders, and the step length\n they all share. */\nstruct Constants {\n count: u32,\n distances: u32,\n bendings: u32,\n joints: u32,\n colliders: u32,\n pad0: u32,\n pad1: u32,\n pad2: u32,\n h: f32,\n invH: f32,\n invH2: f32,\n pad3: f32,\n}\n\n/* Where this dispatch's round of records starts in paces, at a dynamic offset. */\nstruct Round {\n at: u32,\n pad0: u32,\n pad1: u32,\n pad2: u32,\n}\n\n/* One garment's numbers for one round: its step's, and its own that never change. */\nstruct Pace {\n fraction: f32,\n blend: f32,\n alpha: f32,\n carry: u32,\n /* Where the pose a step starts from and the one it moves toward sit in pose, in floats. */\n fromAt: u32,\n toAt: u32,\n /* 0 where the garment sits this round out. Not active, which WGSL reserves. */\n live: u32,\n /* 1 a max distance, 2 a backstop, 4 a frontstop: which limits the garment has at all. */\n limits: u32,\n /* The character's turn since the last step, by columns; origin.w is the share carried. */\n turn0: vec4<f32>,\n turn1: vec4<f32>,\n turn2: vec4<f32>,\n origin: vec4<f32>,\n shift: vec4<f32>,\n /* The frame's wind: one wind, sampled once a frame, so a step's rather than the set-up's. */\n wind: vec4<f32>,\n colliderBase: u32,\n colliders: u32,\n width: u32,\n pad0: u32,\n keep: f32,\n dragKeep: f32,\n maxDistanceScale: f32,\n margin: f32,\n gravity: vec4<f32>,\n}\n\n/* One colour of one kind of constraint across the set, at a dynamic offset; first resets the\n multipliers. Publishing reads one garment's particles as begin to end, and first is the garment. */\nstruct Batch {\n begin: u32,\n end: u32,\n first: u32,\n pad: u32,\n}\n\n@group(0) @binding(0) var<uniform> c: Constants;\n@group(0) @binding(1) var<uniform> round: Round;\n@group(0) @binding(2) var<uniform> batch: Batch;\n@group(0) @binding(3) var<storage, read_write> state: array<f32>;\n@group(0) @binding(4) var<storage, read_write> pose: array<f32>;\n@group(0) @binding(5) var<storage, read> statics: array<f32>;\n@group(0) @binding(6) var<storage, read> constraints: array<u32>;\n@group(0) @binding(7) var<storage, read_write> lambda: array<f32>;\n@group(0) @binding(8) var<storage, read> frame: array<f32>;\n@group(0) @binding(9) var<storage, read> paces: array<Pace>;\n\n/* The record of particle i's garment this round: which garment a particle is, in statics. */\nfn paceAt(i: u32) -> u32 {\n return round.at + u32(statics[29u * c.count + i]);\n}\n\nfn stateAt(section: u32, i: u32) -> u32 {\n return section * 3u * c.count + i * 3u;\n}\n\nfn load(at: u32) -> vec3<f32> {\n return vec3<f32>(state[at], state[at + 1u], state[at + 2u]);\n}\n\nfn store(at: u32, v: vec3<f32>) {\n state[at] = v.x;\n state[at + 1u] = v.y;\n state[at + 2u] = v.z;\n}\n\nfn poseOf(at: u32) -> vec3<f32> {\n return vec3<f32>(pose[at], pose[at + 1u], pose[at + 2u]);\n}\n\n/* Sections of state. */\nconst POSITION = 0u;\nconst PREVIOUS = 1u;\nconst VELOCITY = 2u;\nconst START = 3u;\n\nfn inverseMass(i: u32) -> f32 {\n return statics[i];\n}\n\n/* Where skinning puts particle i, fraction of the way from the last pose to the latest. */\nfn skinnedAt(i: u32, g: u32) -> vec3<f32> {\n let a = poseOf(paces[g].fromAt + i * 3u);\n let b = poseOf(paces[g].toAt + i * 3u);\n return a + (b - a) * paces[g].fraction;\n}\n\nfn skinMatrix(j: u32) -> mat4x4<f32> {\n let at = j * 16u;\n return mat4x4<f32>(\n vec4<f32>(frame[at], frame[at + 1u], frame[at + 2u], frame[at + 3u]),\n vec4<f32>(frame[at + 4u], frame[at + 5u], frame[at + 6u], frame[at + 7u]),\n vec4<f32>(frame[at + 8u], frame[at + 9u], frame[at + 10u], frame[at + 11u]),\n vec4<f32>(frame[at + 12u], frame[at + 13u], frame[at + 14u], frame[at + 15u]));\n}\n\n/* skinParticles: each particle's skinned position and normal by up to eight influences. */\n@compute @workgroup_size(64)\nfn clothPose(@builtin(global_invocation_id) id: vec3<u32>) {\n let i = id.x;\n if (i >= c.count) {\n return;\n }\n let g = paceAt(i);\n if (paces[g].live == 0u) {\n return;\n }\n let n = c.count;\n let restAt = 7u * n + i * 3u;\n let normalAt = 10u * n + i * 3u;\n let x = vec4<f32>(statics[restAt], statics[restAt + 1u], statics[restAt + 2u], 1.0);\n let q0 = vec3<f32>(statics[normalAt], statics[normalAt + 1u], statics[normalAt + 2u]);\n var p = vec3<f32>(0.0);\n var q = vec3<f32>(0.0);\n for (var half = 0u; half < 2u; half++) {\n for (var k = 0u; k < 4u; k++) {\n let at = 13u * n + i * 16u + half * 8u;\n let w = statics[at + 4u + k];\n if (w == 0.0) {\n continue;\n }\n let m = skinMatrix(u32(statics[at + k]));\n p += w * (m * x).xyz;\n q += w * (mat3x3<f32>(m[0].xyz, m[1].xyz, m[2].xyz) * q0);\n }\n }\n let to = paces[g].toAt + i * 3u;\n pose[to] = p.x;\n pose[to + 1u] = p.y;\n pose[to + 2u] = p.z;\n let len = sqrt(dot(q, q));\n let inv = select(0.0, 1.0 / len, len > 1e-12);\n let normal = 6u * n + i * 3u;\n pose[normal] = q.x * inv;\n pose[normal + 1u] = q.y * inv;\n pose[normal + 2u] = q.z * inv;\n}\n\n/* SkinnedCloth.rest: every particle at the latest skinned position, at rest. */\n@compute @workgroup_size(64)\nfn clothRest(@builtin(global_invocation_id) id: vec3<u32>) {\n let i = id.x;\n if (i >= c.count) {\n return;\n }\n let g = paceAt(i);\n if (paces[g].live == 0u) {\n return;\n }\n let p = poseOf(paces[g].toAt + i * 3u);\n store(stateAt(POSITION, i), p);\n store(stateAt(START, i), p);\n store(stateAt(VELOCITY, i), vec3<f32>(0.0));\n}\n\n/* The step's start: the step start kept, a free particle carried, a kinematic one placed. */\n@compute @workgroup_size(64)\nfn clothBegin(@builtin(global_invocation_id) id: vec3<u32>) {\n let i = id.x;\n if (i >= c.count) {\n return;\n }\n let g = paceAt(i);\n if (paces[g].live == 0u) {\n return;\n }\n let at = stateAt(POSITION, i);\n let p = load(at);\n store(stateAt(START, i), p);\n if (inverseMass(i) == 0.0) {\n store(at, skinnedAt(i, g));\n return;\n }\n if (paces[g].carry == 0u) {\n return;\n }\n /* carryParticles: the share of the character's turn about its old origin, and of its move. */\n let turn = mat3x3<f32>(paces[g].turn0.xyz, paces[g].turn1.xyz, paces[g].turn2.xyz);\n let share = paces[g].origin.w;\n let x = p - paces[g].origin.xyz;\n store(at, p + (turn * x - x) * share + paces[g].shift.xyz);\n let vAt = stateAt(VELOCITY, i);\n let v = load(vAt);\n store(vAt, v + (turn * v - v) * share);\n}\n\n/* predictParticles. */\n@compute @workgroup_size(64)\nfn clothPredict(@builtin(global_invocation_id) id: vec3<u32>) {\n let i = id.x;\n if (i >= c.count) {\n return;\n }\n let g = paceAt(i);\n if (paces[g].live == 0u) {\n return;\n }\n let at = stateAt(POSITION, i);\n let p = load(at);\n store(stateAt(PREVIOUS, i), p);\n if (inverseMass(i) == 0.0) {\n return;\n }\n let vAt = stateAt(VELOCITY, i);\n let air = paces[g].wind.xyz;\n let v = air + (load(vAt) * paces[g].keep - air) * paces[g].dragKeep + paces[g].gravity.xyz * c.h;\n store(vAt, v);\n store(at, p + v * c.h);\n}\n\n/* solveDistanceBatch: one invocation a constraint of the batch, constraints sorted by batch. */\n@compute @workgroup_size(64)\nfn clothDistance(@builtin(global_invocation_id) id: vec3<u32>) {\n let k = batch.begin + id.x;\n if (k >= batch.end) {\n return;\n }\n let a = constraints[k * 4u];\n let b = constraints[k * 4u + 1u];\n /* A garment sitting the round out solves nothing, and its next step's first pass resets. */\n if (paces[paceAt(a)].live == 0u) {\n return;\n }\n /* The first pass of a substep starts the multiplier at nothing, as the CPU's fill(0) does \u2014\n written before any early return, so a skipped constraint does not carry the last one. */\n if (batch.first == 1u) {\n lambda[k] = 0.0;\n }\n let wa = inverseMass(a);\n let wb = inverseMass(b);\n let w = wa + wb;\n if (w == 0.0) {\n return;\n }\n let aAt = stateAt(POSITION, a);\n let bAt = stateAt(POSITION, b);\n let d = load(bAt) - load(aAt);\n let len = sqrt(dot(d, d));\n if (len < 1e-9) {\n return;\n }\n let alpha = bitcast<f32>(constraints[k * 4u + 3u]) * c.invH2;\n let old = lambda[k];\n let delta = (-(len - bitcast<f32>(constraints[k * 4u + 2u])) - alpha * old) / (w + alpha);\n lambda[k] = old + delta;\n let s = delta / len;\n /* A pinned particle is shared across a batch, so it is never written, even with itself. */\n if (wa > 0.0) {\n store(aAt, load(aAt) - d * s * wa);\n }\n if (wb > 0.0) {\n store(bAt, load(bAt) + d * s * wb);\n }\n}\n\n/* solveBendingBatch, with the angle from atan2 (see the header). */\n@compute @workgroup_size(64)\nfn clothBending(@builtin(global_invocation_id) id: vec3<u32>) {\n let k = batch.begin + id.x;\n if (k >= batch.end) {\n return;\n }\n let at = c.distances * 4u + k * 6u;\n let slot = c.distances + k;\n if (paces[paceAt(constraints[at])].live == 0u) {\n return;\n }\n if (batch.first == 1u) {\n lambda[slot] = 0.0;\n }\n let ids = vec4<u32>(constraints[at], constraints[at + 1u], constraints[at + 2u], constraints[at + 3u]);\n let w = vec4<f32>(inverseMass(ids.x), inverseMass(ids.y), inverseMass(ids.z), inverseMass(ids.w));\n if (w.x + w.y + w.z + w.w == 0.0) {\n return;\n }\n let origin = load(stateAt(POSITION, ids.x));\n let e = load(stateAt(POSITION, ids.y)) - origin;\n let p3 = load(stateAt(POSITION, ids.z)) - origin;\n let p4 = load(stateAt(POSITION, ids.w)) - origin;\n var n1 = cross(e, p3);\n var n2 = cross(e, p4);\n let l1 = sqrt(dot(n1, n1));\n let l2 = sqrt(dot(n2, n2));\n if (l1 < 1e-12 || l2 < 1e-12) {\n return;\n }\n n1 = n1 / l1;\n n2 = n2 / l2;\n let d = clamp(dot(n1, n2), -1.0, 1.0);\n let sine = sqrt(dot(cross(n1, n2), cross(n1, n2)));\n if (sine < 1e-6) {\n return;\n }\n /* The q vectors: the negative of the derivative of n1 \u00B7 n2. See solveBendingBatch. */\n let q3 = (cross(e, n2) + cross(n1, e) * d) / l1;\n let q4 = (cross(e, n1) + cross(n2, e) * d) / l2;\n let q2 = -((cross(p3, n2) + cross(n1, p3) * d) / l1 + (cross(p4, n1) + cross(n2, p4) * d) / l2);\n let q1 = -(q2 + q3 + q4);\n var sum = w.x * dot(q1, q1) + w.y * dot(q2, q2) + w.z * dot(q3, q3) + w.w * dot(q4, q4);\n sum = sum / (sine * sine);\n if (sum < 1e-12) {\n return;\n }\n let alpha = bitcast<f32>(constraints[at + 5u]) * c.invH2;\n let old = lambda[slot];\n let angle = atan2(sine, d);\n let delta = (-(angle - bitcast<f32>(constraints[at + 4u])) - alpha * old) / (sum + alpha);\n lambda[slot] = old + delta;\n let scale = delta / sine;\n if (w.x > 0.0) {\n let p = stateAt(POSITION, ids.x);\n store(p, load(p) + w.x * scale * q1);\n }\n if (w.y > 0.0) {\n let p = stateAt(POSITION, ids.y);\n store(p, load(p) + w.y * scale * q2);\n }\n if (w.z > 0.0) {\n let p = stateAt(POSITION, ids.z);\n store(p, load(p) + w.z * scale * q3);\n }\n if (w.w > 0.0) {\n let p = stateAt(POSITION, ids.w);\n store(p, load(p) + w.w * scale * q4);\n }\n}\n\n/* solveTethers, one invocation a tether of the batch: a batch holds a particle once. */\n@compute @workgroup_size(64)\nfn clothTether(@builtin(global_invocation_id) id: vec3<u32>) {\n let k = batch.begin + id.x;\n if (k >= batch.end) {\n return;\n }\n let at = c.distances * 4u + c.bendings * 6u + k * 3u;\n if (paces[paceAt(constraints[at])].live == 0u) {\n return;\n }\n let pAt = stateAt(POSITION, constraints[at]);\n let anchor = load(stateAt(POSITION, constraints[at + 1u]));\n let d = load(pAt) - anchor;\n let dist = sqrt(dot(d, d));\n let reach = bitcast<f32>(constraints[at + 2u]);\n if (dist <= reach || dist < 1e-9) {\n return;\n }\n store(pAt, anchor + d * (reach / dist));\n}\n\n/* pushOut: the point onto the sphere's surface if it is inside. A point at the centre stays. */\nfn pushOut(p: vec3<f32>, centre: vec3<f32>, radius: f32) -> vec3<f32> {\n let d = p - centre;\n let d2 = dot(d, d);\n if (d2 >= radius * radius || d2 < 1e-18) {\n return p;\n }\n return centre + d * (radius / sqrt(d2));\n}\n\n/* applyMaxDistance, applyStops (back, then front) and applyColliders, in that order. */\n@compute @workgroup_size(64)\nfn clothLimit(@builtin(global_invocation_id) id: vec3<u32>) {\n let i = id.x;\n if (i >= c.count || inverseMass(i) == 0.0) {\n return;\n }\n let g = paceAt(i);\n if (paces[g].live == 0u) {\n return;\n }\n let limits = paces[g].limits;\n let n = c.count;\n let at = stateAt(POSITION, i);\n var p = load(at);\n let t = skinnedAt(i, g);\n /* A max distance below zero is none: the CPU's Infinity, which a device may not be handed. */\n let maxDistance = statics[n + i];\n if ((limits & 1u) != 0u && maxDistance >= 0.0) {\n let limit = maxDistance * paces[g].maxDistanceScale;\n let d = p - t;\n let d2 = dot(d, d);\n if (d2 > limit * limit) {\n p = t + d * (limit / sqrt(d2));\n }\n }\n let normal = poseOf(6u * n + i * 3u);\n if ((limits & 2u) != 0u) {\n let radius = statics[2u * n + i * 2u + 1u];\n if (radius > 0.0) {\n p = pushOut(p, t - normal * (statics[2u * n + i * 2u] + radius), radius);\n }\n }\n if ((limits & 4u) != 0u) {\n let radius = statics[4u * n + i * 2u + 1u];\n if (radius > 0.0) {\n p = pushOut(p, t + normal * (statics[4u * n + i * 2u] + radius), radius);\n }\n }\n let pad = statics[6u * n + i] + paces[g].margin;\n let ends = c.joints * 16u;\n let radii = ends + c.colliders * 6u;\n let first = paces[g].colliderBase;\n for (var k = first; k < first + paces[g].colliders; k++) {\n let a = vec3<f32>(frame[ends + k * 6u], frame[ends + k * 6u + 1u], frame[ends + k * 6u + 2u]);\n let e = vec3<f32>(frame[ends + k * 6u + 3u], frame[ends + k * 6u + 4u], frame[ends + k * 6u + 5u]) - a;\n let e2 = dot(e, e);\n var s = 0.0;\n if (e2 > 1e-18) {\n s = clamp(dot(p - a, e) / e2, 0.0, 1.0);\n }\n let r0 = frame[radii + k * 2u];\n let r1 = frame[radii + k * 2u + 1u];\n p = pushOut(p, a + e * s, r0 + (r1 - r0) * s + pad);\n }\n store(at, p);\n}\n\n/* finishParticles. */\n@compute @workgroup_size(64)\nfn clothFinish(@builtin(global_invocation_id) id: vec3<u32>) {\n let i = id.x;\n if (i >= c.count) {\n return;\n }\n if (paces[paceAt(i)].live == 0u) {\n return;\n }\n let vAt = stateAt(VELOCITY, i);\n if (inverseMass(i) == 0.0) {\n store(vAt, vec3<f32>(0.0));\n return;\n }\n store(vAt, (load(stateAt(POSITION, i)) - load(stateAt(PREVIOUS, i))) * c.invH);\n}\n\n/* SkinnedCloth.blend: after a reset, the simulation's share grows over the blend's steps. */\n@compute @workgroup_size(64)\nfn clothBlend(@builtin(global_invocation_id) id: vec3<u32>) {\n let i = id.x;\n if (i >= c.count) {\n return;\n }\n /* A blend of 0 is none, not a last step of one: see ClothDevice.step. */\n let g = paceAt(i);\n let blend = paces[g].blend;\n if (paces[g].live == 0u || blend <= 0.0) {\n return;\n }\n let t = skinnedAt(i, g);\n let at = stateAt(POSITION, i);\n store(at, t + (load(at) - t) * blend);\n let vAt = stateAt(VELOCITY, i);\n store(vAt, load(vAt) * blend);\n}\n\n@group(1) @binding(0) var output: texture_storage_2d<rgba32float, write>;\n\n/* SkinnedCloth.interpolate, into the particle texture the vertex stage reads: one garment's, whose\n particles are batch.begin to batch.end and whose record is batch.first's. */\n@compute @workgroup_size(64)\nfn clothPublish(@builtin(global_invocation_id) id: vec3<u32>) {\n let i = batch.begin + id.x;\n if (i >= batch.end) {\n return;\n }\n let g = round.at + batch.first;\n let a = load(stateAt(START, i));\n let p = a + (load(stateAt(POSITION, i)) - a) * paces[g].alpha;\n let local = id.x;\n let width = paces[g].width;\n textureStore(output, vec2<u32>(local % width, local / width), vec4<f32>(p, 0.0));\n}\n";
@@ -7,7 +7,7 @@
7
7
  * each kernel there is an entry point here, one invocation a particle or a constraint, and the loop
8
8
  * over a batch is the dispatch. `clothPose.ts`' `skinParticles` is `pose`. The order of a step —
9
9
  * begin, then per substep predict, the batches, the limits, finish; then the blend — is
10
- * `gpuSkinnedCloth.ts`', taken from the same `ClothControl` the CPU solver is driven by.
10
+ * `gpuClothSet.ts`', taken from the same `ClothControl` the CPU solver is driven by.
11
11
  *
12
12
  * **Where it differs, it says so.** Single precision throughout, where the CPU rounds to single
13
13
  * only on a store. And the bending angle is `atan2(|n1 × n2|, n1 · n2)` rather than `acos(n1 · n2)`
@@ -28,32 +28,43 @@
28
28
  * start; `pose` the two skinned poses a step interpolates between and the latest skinned normals;
29
29
  * `statics` everything per particle that never changes. A section's offset is a multiple of the
30
30
  * particle count, which the constants carry — so no offset is a uniform to keep in step.
31
+ *
32
+ * **One dispatch serves every garment of a set** (`clothSetPack.ts`): the particles, constraints,
33
+ * joints and colliders of all of them in those buffers, and each garment's numbers — its step's
34
+ * fraction and blend, its damping, gravity and limits — a record of its own in `paces`, at the
35
+ * round's place. A kernel finds its garment from the particle, and returns at once where that
36
+ * garment has nothing to do this round: a garment that took fewer steps, or one settling alone after
37
+ * a reset, sits the round out while the rest of the set runs it.
31
38
  */
32
39
  export const CLOTH_WORKGROUP = 64;
33
40
  export const CLOTH_SOLVE_WGSL = /* wgsl */ `
41
+ /* The set's: every garment's particles, records, skin matrices and colliders, and the step length
42
+ they all share. */
34
43
  struct Constants {
35
44
  count: u32,
36
45
  distances: u32,
37
46
  bendings: u32,
38
47
  joints: u32,
39
48
  colliders: u32,
40
- /* 1 a max distance, 2 a backstop, 4 a frontstop: which limits the set-up has at all. */
41
- limits: u32,
42
- width: u32,
43
49
  pad0: u32,
50
+ pad1: u32,
51
+ pad2: u32,
44
52
  h: f32,
45
53
  invH: f32,
46
54
  invH2: f32,
47
- keep: f32,
48
- dragKeep: f32,
49
- maxDistanceScale: f32,
50
- margin: f32,
51
- pad1: f32,
52
- gravity: vec4<f32>,
55
+ pad3: f32,
56
+ }
57
+
58
+ /* Where this dispatch's round of records starts in paces, at a dynamic offset. */
59
+ struct Round {
60
+ at: u32,
61
+ pad0: u32,
62
+ pad1: u32,
63
+ pad2: u32,
53
64
  }
54
65
 
55
- /* One step's numbers, at a dynamic offset: every step of a frame has its own slot. */
56
- struct Step {
66
+ /* One garment's numbers for one round: its step's, and its own that never change. */
67
+ struct Pace {
57
68
  fraction: f32,
58
69
  blend: f32,
59
70
  alpha: f32,
@@ -61,8 +72,10 @@ struct Step {
61
72
  /* Where the pose a step starts from and the one it moves toward sit in pose, in floats. */
62
73
  fromAt: u32,
63
74
  toAt: u32,
64
- pad0: u32,
65
- pad1: u32,
75
+ /* 0 where the garment sits this round out. Not active, which WGSL reserves. */
76
+ live: u32,
77
+ /* 1 a max distance, 2 a backstop, 4 a frontstop: which limits the garment has at all. */
78
+ limits: u32,
66
79
  /* The character's turn since the last step, by columns; origin.w is the share carried. */
67
80
  turn0: vec4<f32>,
68
81
  turn1: vec4<f32>,
@@ -71,9 +84,19 @@ struct Step {
71
84
  shift: vec4<f32>,
72
85
  /* The frame's wind: one wind, sampled once a frame, so a step's rather than the set-up's. */
73
86
  wind: vec4<f32>,
87
+ colliderBase: u32,
88
+ colliders: u32,
89
+ width: u32,
90
+ pad0: u32,
91
+ keep: f32,
92
+ dragKeep: f32,
93
+ maxDistanceScale: f32,
94
+ margin: f32,
95
+ gravity: vec4<f32>,
74
96
  }
75
97
 
76
- /* One batch of one kind of constraint, at a dynamic offset; first resets the multipliers. */
98
+ /* One colour of one kind of constraint across the set, at a dynamic offset; first resets the
99
+ multipliers. Publishing reads one garment's particles as begin to end, and first is the garment. */
77
100
  struct Batch {
78
101
  begin: u32,
79
102
  end: u32,
@@ -82,7 +105,7 @@ struct Batch {
82
105
  }
83
106
 
84
107
  @group(0) @binding(0) var<uniform> c: Constants;
85
- @group(0) @binding(1) var<uniform> pace: Step;
108
+ @group(0) @binding(1) var<uniform> round: Round;
86
109
  @group(0) @binding(2) var<uniform> batch: Batch;
87
110
  @group(0) @binding(3) var<storage, read_write> state: array<f32>;
88
111
  @group(0) @binding(4) var<storage, read_write> pose: array<f32>;
@@ -90,6 +113,12 @@ struct Batch {
90
113
  @group(0) @binding(6) var<storage, read> constraints: array<u32>;
91
114
  @group(0) @binding(7) var<storage, read_write> lambda: array<f32>;
92
115
  @group(0) @binding(8) var<storage, read> frame: array<f32>;
116
+ @group(0) @binding(9) var<storage, read> paces: array<Pace>;
117
+
118
+ /* The record of particle i's garment this round: which garment a particle is, in statics. */
119
+ fn paceAt(i: u32) -> u32 {
120
+ return round.at + u32(statics[29u * c.count + i]);
121
+ }
93
122
 
94
123
  fn stateAt(section: u32, i: u32) -> u32 {
95
124
  return section * 3u * c.count + i * 3u;
@@ -120,10 +149,10 @@ fn inverseMass(i: u32) -> f32 {
120
149
  }
121
150
 
122
151
  /* Where skinning puts particle i, fraction of the way from the last pose to the latest. */
123
- fn skinnedAt(i: u32) -> vec3<f32> {
124
- let a = poseOf(pace.fromAt + i * 3u);
125
- let b = poseOf(pace.toAt + i * 3u);
126
- return a + (b - a) * pace.fraction;
152
+ fn skinnedAt(i: u32, g: u32) -> vec3<f32> {
153
+ let a = poseOf(paces[g].fromAt + i * 3u);
154
+ let b = poseOf(paces[g].toAt + i * 3u);
155
+ return a + (b - a) * paces[g].fraction;
127
156
  }
128
157
 
129
158
  fn skinMatrix(j: u32) -> mat4x4<f32> {
@@ -142,6 +171,10 @@ fn clothPose(@builtin(global_invocation_id) id: vec3<u32>) {
142
171
  if (i >= c.count) {
143
172
  return;
144
173
  }
174
+ let g = paceAt(i);
175
+ if (paces[g].live == 0u) {
176
+ return;
177
+ }
145
178
  let n = c.count;
146
179
  let restAt = 7u * n + i * 3u;
147
180
  let normalAt = 10u * n + i * 3u;
@@ -161,7 +194,7 @@ fn clothPose(@builtin(global_invocation_id) id: vec3<u32>) {
161
194
  q += w * (mat3x3<f32>(m[0].xyz, m[1].xyz, m[2].xyz) * q0);
162
195
  }
163
196
  }
164
- let to = pace.toAt + i * 3u;
197
+ let to = paces[g].toAt + i * 3u;
165
198
  pose[to] = p.x;
166
199
  pose[to + 1u] = p.y;
167
200
  pose[to + 2u] = p.z;
@@ -180,7 +213,11 @@ fn clothRest(@builtin(global_invocation_id) id: vec3<u32>) {
180
213
  if (i >= c.count) {
181
214
  return;
182
215
  }
183
- let p = poseOf(pace.toAt + i * 3u);
216
+ let g = paceAt(i);
217
+ if (paces[g].live == 0u) {
218
+ return;
219
+ }
220
+ let p = poseOf(paces[g].toAt + i * 3u);
184
221
  store(stateAt(POSITION, i), p);
185
222
  store(stateAt(START, i), p);
186
223
  store(stateAt(VELOCITY, i), vec3<f32>(0.0));
@@ -193,21 +230,25 @@ fn clothBegin(@builtin(global_invocation_id) id: vec3<u32>) {
193
230
  if (i >= c.count) {
194
231
  return;
195
232
  }
233
+ let g = paceAt(i);
234
+ if (paces[g].live == 0u) {
235
+ return;
236
+ }
196
237
  let at = stateAt(POSITION, i);
197
238
  let p = load(at);
198
239
  store(stateAt(START, i), p);
199
240
  if (inverseMass(i) == 0.0) {
200
- store(at, skinnedAt(i));
241
+ store(at, skinnedAt(i, g));
201
242
  return;
202
243
  }
203
- if (pace.carry == 0u) {
244
+ if (paces[g].carry == 0u) {
204
245
  return;
205
246
  }
206
247
  /* carryParticles: the share of the character's turn about its old origin, and of its move. */
207
- let turn = mat3x3<f32>(pace.turn0.xyz, pace.turn1.xyz, pace.turn2.xyz);
208
- let share = pace.origin.w;
209
- let x = p - pace.origin.xyz;
210
- store(at, p + (turn * x - x) * share + pace.shift.xyz);
248
+ let turn = mat3x3<f32>(paces[g].turn0.xyz, paces[g].turn1.xyz, paces[g].turn2.xyz);
249
+ let share = paces[g].origin.w;
250
+ let x = p - paces[g].origin.xyz;
251
+ store(at, p + (turn * x - x) * share + paces[g].shift.xyz);
211
252
  let vAt = stateAt(VELOCITY, i);
212
253
  let v = load(vAt);
213
254
  store(vAt, v + (turn * v - v) * share);
@@ -220,6 +261,10 @@ fn clothPredict(@builtin(global_invocation_id) id: vec3<u32>) {
220
261
  if (i >= c.count) {
221
262
  return;
222
263
  }
264
+ let g = paceAt(i);
265
+ if (paces[g].live == 0u) {
266
+ return;
267
+ }
223
268
  let at = stateAt(POSITION, i);
224
269
  let p = load(at);
225
270
  store(stateAt(PREVIOUS, i), p);
@@ -227,8 +272,8 @@ fn clothPredict(@builtin(global_invocation_id) id: vec3<u32>) {
227
272
  return;
228
273
  }
229
274
  let vAt = stateAt(VELOCITY, i);
230
- let air = pace.wind.xyz;
231
- let v = air + (load(vAt) * c.keep - air) * c.dragKeep + c.gravity.xyz * c.h;
275
+ let air = paces[g].wind.xyz;
276
+ let v = air + (load(vAt) * paces[g].keep - air) * paces[g].dragKeep + paces[g].gravity.xyz * c.h;
232
277
  store(vAt, v);
233
278
  store(at, p + v * c.h);
234
279
  }
@@ -240,13 +285,17 @@ fn clothDistance(@builtin(global_invocation_id) id: vec3<u32>) {
240
285
  if (k >= batch.end) {
241
286
  return;
242
287
  }
288
+ let a = constraints[k * 4u];
289
+ let b = constraints[k * 4u + 1u];
290
+ /* A garment sitting the round out solves nothing, and its next step's first pass resets. */
291
+ if (paces[paceAt(a)].live == 0u) {
292
+ return;
293
+ }
243
294
  /* The first pass of a substep starts the multiplier at nothing, as the CPU's fill(0) does —
244
295
  written before any early return, so a skipped constraint does not carry the last one. */
245
296
  if (batch.first == 1u) {
246
297
  lambda[k] = 0.0;
247
298
  }
248
- let a = constraints[k * 4u];
249
- let b = constraints[k * 4u + 1u];
250
299
  let wa = inverseMass(a);
251
300
  let wb = inverseMass(b);
252
301
  let w = wa + wb;
@@ -283,6 +332,9 @@ fn clothBending(@builtin(global_invocation_id) id: vec3<u32>) {
283
332
  }
284
333
  let at = c.distances * 4u + k * 6u;
285
334
  let slot = c.distances + k;
335
+ if (paces[paceAt(constraints[at])].live == 0u) {
336
+ return;
337
+ }
286
338
  if (batch.first == 1u) {
287
339
  lambda[slot] = 0.0;
288
340
  }
@@ -351,6 +403,9 @@ fn clothTether(@builtin(global_invocation_id) id: vec3<u32>) {
351
403
  return;
352
404
  }
353
405
  let at = c.distances * 4u + c.bendings * 6u + k * 3u;
406
+ if (paces[paceAt(constraints[at])].live == 0u) {
407
+ return;
408
+ }
354
409
  let pAt = stateAt(POSITION, constraints[at]);
355
410
  let anchor = load(stateAt(POSITION, constraints[at + 1u]));
356
411
  let d = load(pAt) - anchor;
@@ -379,14 +434,19 @@ fn clothLimit(@builtin(global_invocation_id) id: vec3<u32>) {
379
434
  if (i >= c.count || inverseMass(i) == 0.0) {
380
435
  return;
381
436
  }
437
+ let g = paceAt(i);
438
+ if (paces[g].live == 0u) {
439
+ return;
440
+ }
441
+ let limits = paces[g].limits;
382
442
  let n = c.count;
383
443
  let at = stateAt(POSITION, i);
384
444
  var p = load(at);
385
- let t = skinnedAt(i);
445
+ let t = skinnedAt(i, g);
386
446
  /* A max distance below zero is none: the CPU's Infinity, which a device may not be handed. */
387
447
  let maxDistance = statics[n + i];
388
- if ((c.limits & 1u) != 0u && maxDistance >= 0.0) {
389
- let limit = maxDistance * c.maxDistanceScale;
448
+ if ((limits & 1u) != 0u && maxDistance >= 0.0) {
449
+ let limit = maxDistance * paces[g].maxDistanceScale;
390
450
  let d = p - t;
391
451
  let d2 = dot(d, d);
392
452
  if (d2 > limit * limit) {
@@ -394,22 +454,23 @@ fn clothLimit(@builtin(global_invocation_id) id: vec3<u32>) {
394
454
  }
395
455
  }
396
456
  let normal = poseOf(6u * n + i * 3u);
397
- if ((c.limits & 2u) != 0u) {
457
+ if ((limits & 2u) != 0u) {
398
458
  let radius = statics[2u * n + i * 2u + 1u];
399
459
  if (radius > 0.0) {
400
460
  p = pushOut(p, t - normal * (statics[2u * n + i * 2u] + radius), radius);
401
461
  }
402
462
  }
403
- if ((c.limits & 4u) != 0u) {
463
+ if ((limits & 4u) != 0u) {
404
464
  let radius = statics[4u * n + i * 2u + 1u];
405
465
  if (radius > 0.0) {
406
466
  p = pushOut(p, t + normal * (statics[4u * n + i * 2u] + radius), radius);
407
467
  }
408
468
  }
409
- let pad = statics[6u * n + i] + c.margin;
469
+ let pad = statics[6u * n + i] + paces[g].margin;
410
470
  let ends = c.joints * 16u;
411
471
  let radii = ends + c.colliders * 6u;
412
- for (var k = 0u; k < c.colliders; k++) {
472
+ let first = paces[g].colliderBase;
473
+ for (var k = first; k < first + paces[g].colliders; k++) {
413
474
  let a = vec3<f32>(frame[ends + k * 6u], frame[ends + k * 6u + 1u], frame[ends + k * 6u + 2u]);
414
475
  let e = vec3<f32>(frame[ends + k * 6u + 3u], frame[ends + k * 6u + 4u], frame[ends + k * 6u + 5u]) - a;
415
476
  let e2 = dot(e, e);
@@ -431,6 +492,9 @@ fn clothFinish(@builtin(global_invocation_id) id: vec3<u32>) {
431
492
  if (i >= c.count) {
432
493
  return;
433
494
  }
495
+ if (paces[paceAt(i)].live == 0u) {
496
+ return;
497
+ }
434
498
  let vAt = stateAt(VELOCITY, i);
435
499
  if (inverseMass(i) == 0.0) {
436
500
  store(vAt, vec3<f32>(0.0));
@@ -446,24 +510,34 @@ fn clothBlend(@builtin(global_invocation_id) id: vec3<u32>) {
446
510
  if (i >= c.count) {
447
511
  return;
448
512
  }
449
- let t = skinnedAt(i);
513
+ /* A blend of 0 is none, not a last step of one: see ClothDevice.step. */
514
+ let g = paceAt(i);
515
+ let blend = paces[g].blend;
516
+ if (paces[g].live == 0u || blend <= 0.0) {
517
+ return;
518
+ }
519
+ let t = skinnedAt(i, g);
450
520
  let at = stateAt(POSITION, i);
451
- store(at, t + (load(at) - t) * pace.blend);
521
+ store(at, t + (load(at) - t) * blend);
452
522
  let vAt = stateAt(VELOCITY, i);
453
- store(vAt, load(vAt) * pace.blend);
523
+ store(vAt, load(vAt) * blend);
454
524
  }
455
525
 
456
526
  @group(1) @binding(0) var output: texture_storage_2d<rgba32float, write>;
457
527
 
458
- /* SkinnedCloth.interpolate, into the particle texture the vertex stage reads. */
528
+ /* SkinnedCloth.interpolate, into the particle texture the vertex stage reads: one garment's, whose
529
+ particles are batch.begin to batch.end and whose record is batch.first's. */
459
530
  @compute @workgroup_size(64)
460
531
  fn clothPublish(@builtin(global_invocation_id) id: vec3<u32>) {
461
- let i = id.x;
462
- if (i >= c.count) {
532
+ let i = batch.begin + id.x;
533
+ if (i >= batch.end) {
463
534
  return;
464
535
  }
536
+ let g = round.at + batch.first;
465
537
  let a = load(stateAt(START, i));
466
- let p = a + (load(stateAt(POSITION, i)) - a) * pace.alpha;
467
- textureStore(output, vec2<u32>(i % c.width, i / c.width), vec4<f32>(p, 0.0));
538
+ let p = a + (load(stateAt(POSITION, i)) - a) * paces[g].alpha;
539
+ let local = id.x;
540
+ let width = paces[g].width;
541
+ textureStore(output, vec2<u32>(local % width, local / width), vec4<f32>(p, 0.0));
468
542
  }
469
543
  `;
@@ -27,8 +27,8 @@
27
27
  * different rig at every set — the comparison is what tells the two apart.
28
28
  *
29
29
  * **It was capped at 128 until 4.8.6, and the cap declined draws.** A stage of three dozen idling
30
- * extras beside two fighters filled it in one frame and every skinned draw past it was refused, so
31
- * the consumer stood its crowd in the bind pose. The ring now holds as many palettes as a frame
30
+ * extras beside two characters filled it in one frame and every skinned draw past it was refused,
31
+ * so the consumer stood its crowd in the bind pose. The ring now holds as many palettes as a frame
32
32
  * holds draws (`RenderQuality.drawsPerFrame`), since no frame can bind more palettes than it draws;
33
33
  * a slot still allocates its texture only the first time a frame reaches it, so a scene with two
34
34
  * characters holds two. **What it costs** is a texture and a cached bind group a slot, 1.5 KB for a
@@ -45,8 +45,12 @@
45
45
  export declare class SkinPaletteRing {
46
46
  private readonly capacity;
47
47
  private readonly slots;
48
- /** What each slot was last written with, for the comparison that lets a pass reuse it. */
48
+ /**
49
+ * What each slot was last written with, for the comparison that lets a pass reuse it: a copy
50
+ * that grows with the largest rig the slot has held, and how much of it the last rig filled.
51
+ */
49
52
  private readonly held;
53
+ private readonly heldLength;
50
54
  /** The slot each palette array last went into, by identity. Weak, so a caller's array can go. */
51
55
  private readonly lastSlot;
52
56
  private used;
@@ -28,8 +28,8 @@ import { SkinPaletteTexture } from './skinPaletteTexture.js';
28
28
  * different rig at every set — the comparison is what tells the two apart.
29
29
  *
30
30
  * **It was capped at 128 until 4.8.6, and the cap declined draws.** A stage of three dozen idling
31
- * extras beside two fighters filled it in one frame and every skinned draw past it was refused, so
32
- * the consumer stood its crowd in the bind pose. The ring now holds as many palettes as a frame
31
+ * extras beside two characters filled it in one frame and every skinned draw past it was refused,
32
+ * so the consumer stood its crowd in the bind pose. The ring now holds as many palettes as a frame
33
33
  * holds draws (`RenderQuality.drawsPerFrame`), since no frame can bind more palettes than it draws;
34
34
  * a slot still allocates its texture only the first time a frame reaches it, so a scene with two
35
35
  * characters holds two. **What it costs** is a texture and a cached bind group a slot, 1.5 KB for a
@@ -46,8 +46,12 @@ import { SkinPaletteTexture } from './skinPaletteTexture.js';
46
46
  export class SkinPaletteRing {
47
47
  capacity;
48
48
  slots = [];
49
- /** What each slot was last written with, for the comparison that lets a pass reuse it. */
49
+ /**
50
+ * What each slot was last written with, for the comparison that lets a pass reuse it: a copy
51
+ * that grows with the largest rig the slot has held, and how much of it the last rig filled.
52
+ */
50
53
  held = [];
54
+ heldLength = [];
51
55
  /** The slot each palette array last went into, by identity. Weak, so a caller's array can go. */
52
56
  lastSlot = new WeakMap();
53
57
  used = 0;
@@ -114,12 +118,13 @@ export class SkinPaletteRing {
114
118
  slot.update(device, palette);
115
119
  this.uploads += 1;
116
120
  let copy = this.held[at];
117
- if (copy === undefined || copy.length !== palette.length) {
118
- /* Once per slot and joint count, the way the slot's own texture is: never per frame. */
121
+ if (copy === undefined || copy.length < palette.length) {
122
+ /* Once per slot and larger rig, the way the slot's own texture is: never per frame. */
119
123
  copy = new Float32Array(palette.length);
120
124
  this.held[at] = copy;
121
125
  }
122
126
  copy.set(palette);
127
+ this.heldLength[at] = palette.length;
123
128
  this.lastSlot.set(palette, at);
124
129
  return at;
125
130
  }
@@ -132,14 +137,15 @@ export class SkinPaletteRing {
132
137
  slot.dispose();
133
138
  this.slots.length = 0;
134
139
  this.held.length = 0;
140
+ this.heldLength.length = 0;
135
141
  this.used = 0;
136
142
  }
137
143
  /** Whether a slot was last written with exactly these numbers. */
138
144
  holds(slot, palette) {
139
145
  const copy = this.held[slot];
140
- if (copy === undefined || copy.length !== palette.length)
146
+ if (copy === undefined || this.heldLength[slot] !== palette.length)
141
147
  return false;
142
- for (let i = 0; i < copy.length; i++)
148
+ for (let i = 0; i < palette.length; i++)
143
149
  if (copy[i] !== palette[i])
144
150
  return false;
145
151
  return true;
@@ -1,5 +1,11 @@
1
1
  export declare class SkinPaletteTexture {
2
2
  private texture;
3
+ /**
4
+ * How many joints the texture has room for, which is the most any rig in this slot has had.
5
+ * **It grows and never shrinks**: the shader reads a joint by index, so a texture wider than the
6
+ * rig is read exactly as one that fits, and a slot that a 10-joint prop and a 296-joint character
7
+ * take in turn keeps one texture rather than making one at every change of rig.
8
+ */
3
9
  private joints;
4
10
  /**
5
11
  * The view, cached rather than created per call.
@@ -20,6 +20,12 @@ const TEXTURE_USAGE = {
20
20
  };
21
21
  export class SkinPaletteTexture {
22
22
  texture = null;
23
+ /**
24
+ * How many joints the texture has room for, which is the most any rig in this slot has had.
25
+ * **It grows and never shrinks**: the shader reads a joint by index, so a texture wider than the
26
+ * rig is read exactly as one that fits, and a slot that a 10-joint prop and a 296-joint character
27
+ * take in turn keeps one texture rather than making one at every change of rig.
28
+ */
23
29
  joints = 0;
24
30
  /**
25
31
  * The view, cached rather than created per call.
@@ -34,7 +40,7 @@ export class SkinPaletteTexture {
34
40
  validateSkinPalette(palette);
35
41
  const joints = palette.length / 16;
36
42
  const width = paletteTextureWidth(joints);
37
- if (this.texture === null || this.joints !== joints) {
43
+ if (this.texture === null || joints > this.joints) {
38
44
  this.texture?.destroy();
39
45
  this.cachedView = null;
40
46
  this.texture = device.createTexture({