@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
@@ -0,0 +1,33 @@
1
+ /**
2
+ * The skinned cloth's solver on the device, in WGSL, hand-authored under the compute exception
3
+ * `AGENTS.md` records (2026-08-24): a compute shader has no WebGL2 twin to generate from, and
4
+ * `npm run wgsl` neither writes nor polices this file.
5
+ *
6
+ * **Written to be read beside `@driftengine/physics`' `skinnedClothSolve.ts` and `clothLimits.ts`**:
7
+ * each kernel there is an entry point here, one invocation a particle or a constraint, and the loop
8
+ * over a batch is the dispatch. `clothPose.ts`' `skinParticles` is `pose`. The order of a step —
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.
11
+ *
12
+ * **Where it differs, it says so.** Single precision throughout, where the CPU rounds to single
13
+ * only on a store. And the bending angle is `atan2(|n1 × n2|, n1 · n2)` rather than `acos(n1 · n2)`
14
+ * with its sine from `sqrt(1 − d²)`: the same function, but a garment spends its life near flat,
15
+ * where `d` is near −1 and `1 − d²` in single precision is rounding — a sine of a few ten-thousandths
16
+ * out of nothing, and a fold pushed by it. The cross product's length is exact there.
17
+ *
18
+ * **How far the two solvers part, measured** (`demo/dev/skinnedCloth.html?parity=1`, RX 9070 XT):
19
+ * a cape hanging in still air agrees to 0.016 mm after 150 frames. With tethers, backstops and a
20
+ * gusting wind it parts by up to 11 mm — and the CPU solver against itself, nudged a tenth of a
21
+ * micrometre a frame, parts by 9 mm at the same frame, and by as much as the device at every
22
+ * checkpoint from the sixtieth on. Contact, buckling and gusts carry a rounding difference that
23
+ * far; the device adds rounding and nothing else. Dropping one multiplier reset parts them by
24
+ * 67 mm, which is the instrument seeing a real difference.
25
+ *
26
+ * **Every particle's state in a few buffers of sections**, because WebGPU grants eight storage
27
+ * buffers to a stage: `state` is the positions, the substep's start, the velocities and the step's
28
+ * start; `pose` the two skinned poses a step interpolates between and the latest skinned normals;
29
+ * `statics` everything per particle that never changes. A section's offset is a multiple of the
30
+ * particle count, which the constants carry — so no offset is a uniform to keep in step.
31
+ */
32
+ 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";
@@ -0,0 +1,469 @@
1
+ /**
2
+ * The skinned cloth's solver on the device, in WGSL, hand-authored under the compute exception
3
+ * `AGENTS.md` records (2026-08-24): a compute shader has no WebGL2 twin to generate from, and
4
+ * `npm run wgsl` neither writes nor polices this file.
5
+ *
6
+ * **Written to be read beside `@driftengine/physics`' `skinnedClothSolve.ts` and `clothLimits.ts`**:
7
+ * each kernel there is an entry point here, one invocation a particle or a constraint, and the loop
8
+ * over a batch is the dispatch. `clothPose.ts`' `skinParticles` is `pose`. The order of a step —
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.
11
+ *
12
+ * **Where it differs, it says so.** Single precision throughout, where the CPU rounds to single
13
+ * only on a store. And the bending angle is `atan2(|n1 × n2|, n1 · n2)` rather than `acos(n1 · n2)`
14
+ * with its sine from `sqrt(1 − d²)`: the same function, but a garment spends its life near flat,
15
+ * where `d` is near −1 and `1 − d²` in single precision is rounding — a sine of a few ten-thousandths
16
+ * out of nothing, and a fold pushed by it. The cross product's length is exact there.
17
+ *
18
+ * **How far the two solvers part, measured** (`demo/dev/skinnedCloth.html?parity=1`, RX 9070 XT):
19
+ * a cape hanging in still air agrees to 0.016 mm after 150 frames. With tethers, backstops and a
20
+ * gusting wind it parts by up to 11 mm — and the CPU solver against itself, nudged a tenth of a
21
+ * micrometre a frame, parts by 9 mm at the same frame, and by as much as the device at every
22
+ * checkpoint from the sixtieth on. Contact, buckling and gusts carry a rounding difference that
23
+ * far; the device adds rounding and nothing else. Dropping one multiplier reset parts them by
24
+ * 67 mm, which is the instrument seeing a real difference.
25
+ *
26
+ * **Every particle's state in a few buffers of sections**, because WebGPU grants eight storage
27
+ * buffers to a stage: `state` is the positions, the substep's start, the velocities and the step's
28
+ * start; `pose` the two skinned poses a step interpolates between and the latest skinned normals;
29
+ * `statics` everything per particle that never changes. A section's offset is a multiple of the
30
+ * particle count, which the constants carry — so no offset is a uniform to keep in step.
31
+ */
32
+ export const CLOTH_WORKGROUP = 64;
33
+ export const CLOTH_SOLVE_WGSL = /* wgsl */ `
34
+ struct Constants {
35
+ count: u32,
36
+ distances: u32,
37
+ bendings: u32,
38
+ joints: u32,
39
+ 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
+ pad0: u32,
44
+ h: f32,
45
+ invH: f32,
46
+ invH2: f32,
47
+ keep: f32,
48
+ dragKeep: f32,
49
+ maxDistanceScale: f32,
50
+ margin: f32,
51
+ pad1: f32,
52
+ gravity: vec4<f32>,
53
+ }
54
+
55
+ /* One step's numbers, at a dynamic offset: every step of a frame has its own slot. */
56
+ struct Step {
57
+ fraction: f32,
58
+ blend: f32,
59
+ alpha: f32,
60
+ carry: u32,
61
+ /* Where the pose a step starts from and the one it moves toward sit in pose, in floats. */
62
+ fromAt: u32,
63
+ toAt: u32,
64
+ pad0: u32,
65
+ pad1: u32,
66
+ /* The character's turn since the last step, by columns; origin.w is the share carried. */
67
+ turn0: vec4<f32>,
68
+ turn1: vec4<f32>,
69
+ turn2: vec4<f32>,
70
+ origin: vec4<f32>,
71
+ shift: vec4<f32>,
72
+ /* The frame's wind: one wind, sampled once a frame, so a step's rather than the set-up's. */
73
+ wind: vec4<f32>,
74
+ }
75
+
76
+ /* One batch of one kind of constraint, at a dynamic offset; first resets the multipliers. */
77
+ struct Batch {
78
+ begin: u32,
79
+ end: u32,
80
+ first: u32,
81
+ pad: u32,
82
+ }
83
+
84
+ @group(0) @binding(0) var<uniform> c: Constants;
85
+ @group(0) @binding(1) var<uniform> pace: Step;
86
+ @group(0) @binding(2) var<uniform> batch: Batch;
87
+ @group(0) @binding(3) var<storage, read_write> state: array<f32>;
88
+ @group(0) @binding(4) var<storage, read_write> pose: array<f32>;
89
+ @group(0) @binding(5) var<storage, read> statics: array<f32>;
90
+ @group(0) @binding(6) var<storage, read> constraints: array<u32>;
91
+ @group(0) @binding(7) var<storage, read_write> lambda: array<f32>;
92
+ @group(0) @binding(8) var<storage, read> frame: array<f32>;
93
+
94
+ fn stateAt(section: u32, i: u32) -> u32 {
95
+ return section * 3u * c.count + i * 3u;
96
+ }
97
+
98
+ fn load(at: u32) -> vec3<f32> {
99
+ return vec3<f32>(state[at], state[at + 1u], state[at + 2u]);
100
+ }
101
+
102
+ fn store(at: u32, v: vec3<f32>) {
103
+ state[at] = v.x;
104
+ state[at + 1u] = v.y;
105
+ state[at + 2u] = v.z;
106
+ }
107
+
108
+ fn poseOf(at: u32) -> vec3<f32> {
109
+ return vec3<f32>(pose[at], pose[at + 1u], pose[at + 2u]);
110
+ }
111
+
112
+ /* Sections of state. */
113
+ const POSITION = 0u;
114
+ const PREVIOUS = 1u;
115
+ const VELOCITY = 2u;
116
+ const START = 3u;
117
+
118
+ fn inverseMass(i: u32) -> f32 {
119
+ return statics[i];
120
+ }
121
+
122
+ /* 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;
127
+ }
128
+
129
+ fn skinMatrix(j: u32) -> mat4x4<f32> {
130
+ let at = j * 16u;
131
+ return mat4x4<f32>(
132
+ vec4<f32>(frame[at], frame[at + 1u], frame[at + 2u], frame[at + 3u]),
133
+ vec4<f32>(frame[at + 4u], frame[at + 5u], frame[at + 6u], frame[at + 7u]),
134
+ vec4<f32>(frame[at + 8u], frame[at + 9u], frame[at + 10u], frame[at + 11u]),
135
+ vec4<f32>(frame[at + 12u], frame[at + 13u], frame[at + 14u], frame[at + 15u]));
136
+ }
137
+
138
+ /* skinParticles: each particle's skinned position and normal by up to eight influences. */
139
+ @compute @workgroup_size(64)
140
+ fn clothPose(@builtin(global_invocation_id) id: vec3<u32>) {
141
+ let i = id.x;
142
+ if (i >= c.count) {
143
+ return;
144
+ }
145
+ let n = c.count;
146
+ let restAt = 7u * n + i * 3u;
147
+ let normalAt = 10u * n + i * 3u;
148
+ let x = vec4<f32>(statics[restAt], statics[restAt + 1u], statics[restAt + 2u], 1.0);
149
+ let q0 = vec3<f32>(statics[normalAt], statics[normalAt + 1u], statics[normalAt + 2u]);
150
+ var p = vec3<f32>(0.0);
151
+ var q = vec3<f32>(0.0);
152
+ for (var half = 0u; half < 2u; half++) {
153
+ for (var k = 0u; k < 4u; k++) {
154
+ let at = 13u * n + i * 16u + half * 8u;
155
+ let w = statics[at + 4u + k];
156
+ if (w == 0.0) {
157
+ continue;
158
+ }
159
+ let m = skinMatrix(u32(statics[at + k]));
160
+ p += w * (m * x).xyz;
161
+ q += w * (mat3x3<f32>(m[0].xyz, m[1].xyz, m[2].xyz) * q0);
162
+ }
163
+ }
164
+ let to = pace.toAt + i * 3u;
165
+ pose[to] = p.x;
166
+ pose[to + 1u] = p.y;
167
+ pose[to + 2u] = p.z;
168
+ let len = sqrt(dot(q, q));
169
+ let inv = select(0.0, 1.0 / len, len > 1e-12);
170
+ let normal = 6u * n + i * 3u;
171
+ pose[normal] = q.x * inv;
172
+ pose[normal + 1u] = q.y * inv;
173
+ pose[normal + 2u] = q.z * inv;
174
+ }
175
+
176
+ /* SkinnedCloth.rest: every particle at the latest skinned position, at rest. */
177
+ @compute @workgroup_size(64)
178
+ fn clothRest(@builtin(global_invocation_id) id: vec3<u32>) {
179
+ let i = id.x;
180
+ if (i >= c.count) {
181
+ return;
182
+ }
183
+ let p = poseOf(pace.toAt + i * 3u);
184
+ store(stateAt(POSITION, i), p);
185
+ store(stateAt(START, i), p);
186
+ store(stateAt(VELOCITY, i), vec3<f32>(0.0));
187
+ }
188
+
189
+ /* The step's start: the step start kept, a free particle carried, a kinematic one placed. */
190
+ @compute @workgroup_size(64)
191
+ fn clothBegin(@builtin(global_invocation_id) id: vec3<u32>) {
192
+ let i = id.x;
193
+ if (i >= c.count) {
194
+ return;
195
+ }
196
+ let at = stateAt(POSITION, i);
197
+ let p = load(at);
198
+ store(stateAt(START, i), p);
199
+ if (inverseMass(i) == 0.0) {
200
+ store(at, skinnedAt(i));
201
+ return;
202
+ }
203
+ if (pace.carry == 0u) {
204
+ return;
205
+ }
206
+ /* 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);
211
+ let vAt = stateAt(VELOCITY, i);
212
+ let v = load(vAt);
213
+ store(vAt, v + (turn * v - v) * share);
214
+ }
215
+
216
+ /* predictParticles. */
217
+ @compute @workgroup_size(64)
218
+ fn clothPredict(@builtin(global_invocation_id) id: vec3<u32>) {
219
+ let i = id.x;
220
+ if (i >= c.count) {
221
+ return;
222
+ }
223
+ let at = stateAt(POSITION, i);
224
+ let p = load(at);
225
+ store(stateAt(PREVIOUS, i), p);
226
+ if (inverseMass(i) == 0.0) {
227
+ return;
228
+ }
229
+ 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;
232
+ store(vAt, v);
233
+ store(at, p + v * c.h);
234
+ }
235
+
236
+ /* solveDistanceBatch: one invocation a constraint of the batch, constraints sorted by batch. */
237
+ @compute @workgroup_size(64)
238
+ fn clothDistance(@builtin(global_invocation_id) id: vec3<u32>) {
239
+ let k = batch.begin + id.x;
240
+ if (k >= batch.end) {
241
+ return;
242
+ }
243
+ /* The first pass of a substep starts the multiplier at nothing, as the CPU's fill(0) does —
244
+ written before any early return, so a skipped constraint does not carry the last one. */
245
+ if (batch.first == 1u) {
246
+ lambda[k] = 0.0;
247
+ }
248
+ let a = constraints[k * 4u];
249
+ let b = constraints[k * 4u + 1u];
250
+ let wa = inverseMass(a);
251
+ let wb = inverseMass(b);
252
+ let w = wa + wb;
253
+ if (w == 0.0) {
254
+ return;
255
+ }
256
+ let aAt = stateAt(POSITION, a);
257
+ let bAt = stateAt(POSITION, b);
258
+ let d = load(bAt) - load(aAt);
259
+ let len = sqrt(dot(d, d));
260
+ if (len < 1e-9) {
261
+ return;
262
+ }
263
+ let alpha = bitcast<f32>(constraints[k * 4u + 3u]) * c.invH2;
264
+ let old = lambda[k];
265
+ let delta = (-(len - bitcast<f32>(constraints[k * 4u + 2u])) - alpha * old) / (w + alpha);
266
+ lambda[k] = old + delta;
267
+ let s = delta / len;
268
+ /* A pinned particle is shared across a batch, so it is never written, even with itself. */
269
+ if (wa > 0.0) {
270
+ store(aAt, load(aAt) - d * s * wa);
271
+ }
272
+ if (wb > 0.0) {
273
+ store(bAt, load(bAt) + d * s * wb);
274
+ }
275
+ }
276
+
277
+ /* solveBendingBatch, with the angle from atan2 (see the header). */
278
+ @compute @workgroup_size(64)
279
+ fn clothBending(@builtin(global_invocation_id) id: vec3<u32>) {
280
+ let k = batch.begin + id.x;
281
+ if (k >= batch.end) {
282
+ return;
283
+ }
284
+ let at = c.distances * 4u + k * 6u;
285
+ let slot = c.distances + k;
286
+ if (batch.first == 1u) {
287
+ lambda[slot] = 0.0;
288
+ }
289
+ let ids = vec4<u32>(constraints[at], constraints[at + 1u], constraints[at + 2u], constraints[at + 3u]);
290
+ let w = vec4<f32>(inverseMass(ids.x), inverseMass(ids.y), inverseMass(ids.z), inverseMass(ids.w));
291
+ if (w.x + w.y + w.z + w.w == 0.0) {
292
+ return;
293
+ }
294
+ let origin = load(stateAt(POSITION, ids.x));
295
+ let e = load(stateAt(POSITION, ids.y)) - origin;
296
+ let p3 = load(stateAt(POSITION, ids.z)) - origin;
297
+ let p4 = load(stateAt(POSITION, ids.w)) - origin;
298
+ var n1 = cross(e, p3);
299
+ var n2 = cross(e, p4);
300
+ let l1 = sqrt(dot(n1, n1));
301
+ let l2 = sqrt(dot(n2, n2));
302
+ if (l1 < 1e-12 || l2 < 1e-12) {
303
+ return;
304
+ }
305
+ n1 = n1 / l1;
306
+ n2 = n2 / l2;
307
+ let d = clamp(dot(n1, n2), -1.0, 1.0);
308
+ let sine = sqrt(dot(cross(n1, n2), cross(n1, n2)));
309
+ if (sine < 1e-6) {
310
+ return;
311
+ }
312
+ /* The q vectors: the negative of the derivative of n1 · n2. See solveBendingBatch. */
313
+ let q3 = (cross(e, n2) + cross(n1, e) * d) / l1;
314
+ let q4 = (cross(e, n1) + cross(n2, e) * d) / l2;
315
+ let q2 = -((cross(p3, n2) + cross(n1, p3) * d) / l1 + (cross(p4, n1) + cross(n2, p4) * d) / l2);
316
+ let q1 = -(q2 + q3 + q4);
317
+ var sum = w.x * dot(q1, q1) + w.y * dot(q2, q2) + w.z * dot(q3, q3) + w.w * dot(q4, q4);
318
+ sum = sum / (sine * sine);
319
+ if (sum < 1e-12) {
320
+ return;
321
+ }
322
+ let alpha = bitcast<f32>(constraints[at + 5u]) * c.invH2;
323
+ let old = lambda[slot];
324
+ let angle = atan2(sine, d);
325
+ let delta = (-(angle - bitcast<f32>(constraints[at + 4u])) - alpha * old) / (sum + alpha);
326
+ lambda[slot] = old + delta;
327
+ let scale = delta / sine;
328
+ if (w.x > 0.0) {
329
+ let p = stateAt(POSITION, ids.x);
330
+ store(p, load(p) + w.x * scale * q1);
331
+ }
332
+ if (w.y > 0.0) {
333
+ let p = stateAt(POSITION, ids.y);
334
+ store(p, load(p) + w.y * scale * q2);
335
+ }
336
+ if (w.z > 0.0) {
337
+ let p = stateAt(POSITION, ids.z);
338
+ store(p, load(p) + w.z * scale * q3);
339
+ }
340
+ if (w.w > 0.0) {
341
+ let p = stateAt(POSITION, ids.w);
342
+ store(p, load(p) + w.w * scale * q4);
343
+ }
344
+ }
345
+
346
+ /* solveTethers, one invocation a tether of the batch: a batch holds a particle once. */
347
+ @compute @workgroup_size(64)
348
+ fn clothTether(@builtin(global_invocation_id) id: vec3<u32>) {
349
+ let k = batch.begin + id.x;
350
+ if (k >= batch.end) {
351
+ return;
352
+ }
353
+ let at = c.distances * 4u + c.bendings * 6u + k * 3u;
354
+ let pAt = stateAt(POSITION, constraints[at]);
355
+ let anchor = load(stateAt(POSITION, constraints[at + 1u]));
356
+ let d = load(pAt) - anchor;
357
+ let dist = sqrt(dot(d, d));
358
+ let reach = bitcast<f32>(constraints[at + 2u]);
359
+ if (dist <= reach || dist < 1e-9) {
360
+ return;
361
+ }
362
+ store(pAt, anchor + d * (reach / dist));
363
+ }
364
+
365
+ /* pushOut: the point onto the sphere's surface if it is inside. A point at the centre stays. */
366
+ fn pushOut(p: vec3<f32>, centre: vec3<f32>, radius: f32) -> vec3<f32> {
367
+ let d = p - centre;
368
+ let d2 = dot(d, d);
369
+ if (d2 >= radius * radius || d2 < 1e-18) {
370
+ return p;
371
+ }
372
+ return centre + d * (radius / sqrt(d2));
373
+ }
374
+
375
+ /* applyMaxDistance, applyStops (back, then front) and applyColliders, in that order. */
376
+ @compute @workgroup_size(64)
377
+ fn clothLimit(@builtin(global_invocation_id) id: vec3<u32>) {
378
+ let i = id.x;
379
+ if (i >= c.count || inverseMass(i) == 0.0) {
380
+ return;
381
+ }
382
+ let n = c.count;
383
+ let at = stateAt(POSITION, i);
384
+ var p = load(at);
385
+ let t = skinnedAt(i);
386
+ /* A max distance below zero is none: the CPU's Infinity, which a device may not be handed. */
387
+ let maxDistance = statics[n + i];
388
+ if ((c.limits & 1u) != 0u && maxDistance >= 0.0) {
389
+ let limit = maxDistance * c.maxDistanceScale;
390
+ let d = p - t;
391
+ let d2 = dot(d, d);
392
+ if (d2 > limit * limit) {
393
+ p = t + d * (limit / sqrt(d2));
394
+ }
395
+ }
396
+ let normal = poseOf(6u * n + i * 3u);
397
+ if ((c.limits & 2u) != 0u) {
398
+ let radius = statics[2u * n + i * 2u + 1u];
399
+ if (radius > 0.0) {
400
+ p = pushOut(p, t - normal * (statics[2u * n + i * 2u] + radius), radius);
401
+ }
402
+ }
403
+ if ((c.limits & 4u) != 0u) {
404
+ let radius = statics[4u * n + i * 2u + 1u];
405
+ if (radius > 0.0) {
406
+ p = pushOut(p, t + normal * (statics[4u * n + i * 2u] + radius), radius);
407
+ }
408
+ }
409
+ let pad = statics[6u * n + i] + c.margin;
410
+ let ends = c.joints * 16u;
411
+ let radii = ends + c.colliders * 6u;
412
+ for (var k = 0u; k < c.colliders; k++) {
413
+ let a = vec3<f32>(frame[ends + k * 6u], frame[ends + k * 6u + 1u], frame[ends + k * 6u + 2u]);
414
+ let e = vec3<f32>(frame[ends + k * 6u + 3u], frame[ends + k * 6u + 4u], frame[ends + k * 6u + 5u]) - a;
415
+ let e2 = dot(e, e);
416
+ var s = 0.0;
417
+ if (e2 > 1e-18) {
418
+ s = clamp(dot(p - a, e) / e2, 0.0, 1.0);
419
+ }
420
+ let r0 = frame[radii + k * 2u];
421
+ let r1 = frame[radii + k * 2u + 1u];
422
+ p = pushOut(p, a + e * s, r0 + (r1 - r0) * s + pad);
423
+ }
424
+ store(at, p);
425
+ }
426
+
427
+ /* finishParticles. */
428
+ @compute @workgroup_size(64)
429
+ fn clothFinish(@builtin(global_invocation_id) id: vec3<u32>) {
430
+ let i = id.x;
431
+ if (i >= c.count) {
432
+ return;
433
+ }
434
+ let vAt = stateAt(VELOCITY, i);
435
+ if (inverseMass(i) == 0.0) {
436
+ store(vAt, vec3<f32>(0.0));
437
+ return;
438
+ }
439
+ store(vAt, (load(stateAt(POSITION, i)) - load(stateAt(PREVIOUS, i))) * c.invH);
440
+ }
441
+
442
+ /* SkinnedCloth.blend: after a reset, the simulation's share grows over the blend's steps. */
443
+ @compute @workgroup_size(64)
444
+ fn clothBlend(@builtin(global_invocation_id) id: vec3<u32>) {
445
+ let i = id.x;
446
+ if (i >= c.count) {
447
+ return;
448
+ }
449
+ let t = skinnedAt(i);
450
+ let at = stateAt(POSITION, i);
451
+ store(at, t + (load(at) - t) * pace.blend);
452
+ let vAt = stateAt(VELOCITY, i);
453
+ store(vAt, load(vAt) * pace.blend);
454
+ }
455
+
456
+ @group(1) @binding(0) var output: texture_storage_2d<rgba32float, write>;
457
+
458
+ /* SkinnedCloth.interpolate, into the particle texture the vertex stage reads. */
459
+ @compute @workgroup_size(64)
460
+ fn clothPublish(@builtin(global_invocation_id) id: vec3<u32>) {
461
+ let i = id.x;
462
+ if (i >= c.count) {
463
+ return;
464
+ }
465
+ 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));
468
+ }
469
+ `;
@@ -0,0 +1,51 @@
1
+ import type { DrawCommand } from './drawCommand.ts';
2
+ import type { PipelineCache } from './pipelineCache.ts';
3
+ /** The format the diffuse lands in: half floats, because it is linear light before any grade. */
4
+ export declare const SKIN_TARGET_FORMAT: GPUTextureFormat;
5
+ export declare class SkinScatterPass {
6
+ private readonly device;
7
+ private readonly layout;
8
+ private readonly uniforms;
9
+ private readonly staging;
10
+ private readonly linear;
11
+ private readonly nearest;
12
+ private readonly commands;
13
+ /** Each profile's scatter distance per channel, in metres: four floats a profile. */
14
+ private readonly profiles;
15
+ private diffuse;
16
+ private diffuseMsaa;
17
+ private across;
18
+ private diffuseView;
19
+ private diffuseMsaaView;
20
+ private acrossView;
21
+ private groupAcross;
22
+ private groupDown;
23
+ private groupsDepth;
24
+ constructor(device: GPUDevice);
25
+ /** Whether any skin was kept for this frame. */
26
+ get pending(): boolean;
27
+ /** A command for one skin draw's diffuse half, filled by the caller. */
28
+ take(): DrawCommand;
29
+ /** What one profile scatters, per channel, in metres: the last material to name it says. */
30
+ setProfile(index: number, r: number, g: number, b: number): void;
31
+ /** The targets at the frame's size and sample count, made again only when either changes. */
32
+ size(width: number, height: number, samples: number): void;
33
+ /**
34
+ * Every kept draw's diffuse into the target, against the frame's own depth, which it reads and
35
+ * does not write. Called with no pass open on `encoder`.
36
+ */
37
+ drawDiffuse(encoder: GPUCommandEncoder, depth: GPUTextureView, issue: (pass: GPURenderPassEncoder, command: DrawCommand) => void, timestampWrites: GPURenderPassTimestampWrites | undefined): void;
38
+ /**
39
+ * Across into the second target, then down and added into the frame's colour — `frame` the
40
+ * attachment its pass draws into, `resolve` where a multisampled one resolves, `samples` its
41
+ * count. `projection` is the frame's, `height` the frame's height in pixels.
42
+ */
43
+ spread(encoder: GPUCommandEncoder, pipelines: PipelineCache, depth: GPUTextureView, frame: GPUTextureView, resolve: GPUTextureView | null, samples: number, projection: ArrayLike<number>, inverseProjection: ArrayLike<number>, timestampWrites: () => GPURenderPassTimestampWrites | undefined): void;
44
+ /** Spent at the frame's start: the kept draws were this frame's. */
45
+ reset(): void;
46
+ dispose(): void;
47
+ private group;
48
+ /** Both axes' blocks: the step along each, and what they share. Column-major matrices. */
49
+ private writeUniforms;
50
+ private releaseTargets;
51
+ }