@driftengine/core 4.10.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 (140) hide show
  1. package/CHANGELOG.json +71 -0
  2. package/README.md +1 -1
  3. package/dist/index.d.ts +4 -3
  4. package/dist/index.js +1 -1
  5. package/dist/render/backend/webgl2/renderer.d.ts +99 -2
  6. package/dist/render/backend/webgl2/renderer.js +361 -42
  7. package/dist/render/backend/webgpu/buffers.d.ts +5 -0
  8. package/dist/render/backend/webgpu/buffers.js +98 -48
  9. package/dist/render/backend/webgpu/clothKernels.d.ts +8 -0
  10. package/dist/render/backend/webgpu/clothKernels.js +76 -0
  11. package/dist/render/backend/webgpu/clothOps.d.ts +39 -0
  12. package/dist/render/backend/webgpu/clothOps.js +78 -0
  13. package/dist/render/backend/webgpu/clothSetLayout.d.ts +25 -0
  14. package/dist/render/backend/webgpu/clothSetLayout.js +84 -0
  15. package/dist/render/backend/webgpu/clothSetPack.d.ts +33 -0
  16. package/dist/render/backend/webgpu/clothSetPack.js +144 -0
  17. package/dist/render/backend/webgpu/drawCommand.d.ts +2 -0
  18. package/dist/render/backend/webgpu/drawCommand.js +1 -0
  19. package/dist/render/backend/webgpu/flatPass.d.ts +1 -1
  20. package/dist/render/backend/webgpu/flatPass.js +2 -1
  21. package/dist/render/backend/webgpu/gpuClothSet.d.ts +80 -0
  22. package/dist/render/backend/webgpu/gpuClothSet.js +462 -0
  23. package/dist/render/backend/webgpu/latePass.js +1 -0
  24. package/dist/render/backend/webgpu/motionPass.js +15 -6
  25. package/dist/render/backend/webgpu/pipelineCache.d.ts +22 -1
  26. package/dist/render/backend/webgpu/pipelineCache.js +39 -1
  27. package/dist/render/backend/webgpu/postPass.d.ts +1 -1
  28. package/dist/render/backend/webgpu/renderer.d.ts +48 -0
  29. package/dist/render/backend/webgpu/renderer.js +207 -27
  30. package/dist/render/backend/webgpu/shaders/clothSolve.wgsl.d.ts +9 -2
  31. package/dist/render/backend/webgpu/shaders/clothSolve.wgsl.js +121 -47
  32. package/dist/render/backend/webgpu/staticBundles.js +2 -1
  33. package/dist/render/backend/webgpu/vertexFormats.d.ts +22 -0
  34. package/dist/render/backend/webgpu/vertexFormats.js +97 -0
  35. package/dist/render/globalMediumPass.d.ts +2 -0
  36. package/dist/render/globalMediumPass.js +1 -0
  37. package/dist/render/indexWidth.d.ts +17 -0
  38. package/dist/render/indexWidth.js +23 -0
  39. package/dist/render/lightMatrix.d.ts +11 -0
  40. package/dist/render/lightMatrix.js +14 -0
  41. package/dist/render/mesh.d.ts +9 -0
  42. package/dist/render/mesh.js +38 -14
  43. package/dist/render/renderQuality.d.ts +30 -0
  44. package/dist/render/renderQuality.js +5 -0
  45. package/dist/render/rendererHarness.d.ts +7 -0
  46. package/dist/render/rendererHarness.js +12 -0
  47. package/dist/render/shader.d.ts +15 -0
  48. package/dist/render/shader.js +41 -0
  49. package/dist/render/shaders/flat/directionalShadow.js +84 -10
  50. package/dist/render/shaders/flat/glassTint.d.ts +1 -1
  51. package/dist/render/shaders/flat/glassTint.js +3 -0
  52. package/dist/render/shaders/flat/index.d.ts +15 -0
  53. package/dist/render/shaders/flat/index.js +15 -2
  54. package/dist/render/shaders/flat/layered.d.ts +19 -0
  55. package/dist/render/shaders/flat/layered.js +115 -0
  56. package/dist/render/shaders/flat/main.d.ts +1 -1
  57. package/dist/render/shaders/flat/main.js +43 -16
  58. package/dist/render/shaders/flat/mapReads.d.ts +10 -0
  59. package/dist/render/shaders/flat/mapReads.js +38 -0
  60. package/dist/render/shaders/flat/movingSun.d.ts +21 -0
  61. package/dist/render/shaders/flat/movingSun.js +46 -0
  62. package/dist/render/shaders/flat/normalMap.d.ts +6 -2
  63. package/dist/render/shaders/flat/normalMap.js +14 -3
  64. package/dist/render/shaders/flat/overlay.d.ts +3 -2
  65. package/dist/render/shaders/flat/overlay.js +41 -19
  66. package/dist/render/shaders/flat/preamble.js +23 -5
  67. package/dist/render/shaders/flat/worldUv.d.ts +22 -0
  68. package/dist/render/shaders/flat/worldUv.js +113 -0
  69. package/dist/render/shaders/generated/flat.wgsl.d.ts +768 -368
  70. package/dist/render/shaders/generated/flat.wgsl.js +791 -391
  71. package/dist/render/shaders/generated/globalMedium.wgsl.d.ts +8 -3
  72. package/dist/render/shaders/generated/globalMedium.wgsl.js +8 -3
  73. package/dist/render/shaders/generated/lightVolume.wgsl.d.ts +15 -10
  74. package/dist/render/shaders/generated/lightVolume.wgsl.js +16 -11
  75. package/dist/render/shaders/globalMedium.js +9 -1
  76. package/dist/render/shaders/lightVolume.js +9 -1
  77. package/dist/render/shaders/tangentFrame.d.ts +1 -1
  78. package/dist/render/shaders/tangentFrame.js +16 -3
  79. package/dist/render/skinnedClothRun.d.ts +28 -0
  80. package/dist/render/skinnedClothRun.js +103 -35
  81. package/dist/render/surfaceLayers.d.ts +39 -0
  82. package/dist/render/surfaceLayers.js +26 -0
  83. package/dist/render/surfaceOverlay.d.ts +16 -3
  84. package/dist/render/surfaceOverlay.js +11 -6
  85. package/dist/render/surfaceProjection.d.ts +34 -0
  86. package/dist/render/surfaceProjection.js +30 -0
  87. package/dist/render/surfaceTexture.d.ts +30 -0
  88. package/dist/render/vertexPacking.d.ts +48 -0
  89. package/dist/render/vertexPacking.js +68 -0
  90. package/package.json +6 -6
  91. package/src/index.ts +4 -2
  92. package/src/render/backend/webgl2/renderer.ts +408 -55
  93. package/src/render/backend/webgpu/buffers.ts +153 -54
  94. package/src/render/backend/webgpu/clothKernels.ts +86 -0
  95. package/src/render/backend/webgpu/clothOps.ts +86 -0
  96. package/src/render/backend/webgpu/clothSetLayout.ts +102 -0
  97. package/src/render/backend/webgpu/clothSetPack.ts +197 -0
  98. package/src/render/backend/webgpu/drawCommand.ts +3 -0
  99. package/src/render/backend/webgpu/flatPass.ts +26 -28
  100. package/src/render/backend/webgpu/gpuClothSet.ts +564 -0
  101. package/src/render/backend/webgpu/latePass.ts +1 -0
  102. package/src/render/backend/webgpu/motionPass.ts +18 -7
  103. package/src/render/backend/webgpu/pipelineCache.ts +46 -7
  104. package/src/render/backend/webgpu/renderer.ts +274 -48
  105. package/src/render/backend/webgpu/shaders/clothSolve.wgsl.ts +121 -47
  106. package/src/render/backend/webgpu/staticBundles.ts +2 -1
  107. package/src/render/backend/webgpu/vertexFormats.ts +113 -0
  108. package/src/render/globalMediumPass.ts +7 -0
  109. package/src/render/indexWidth.ts +27 -0
  110. package/src/render/lightMatrix.ts +15 -0
  111. package/src/render/mesh.ts +66 -14
  112. package/src/render/renderQuality.ts +38 -0
  113. package/src/render/rendererHarness.ts +15 -0
  114. package/src/render/shader.ts +59 -0
  115. package/src/render/shaders/flat/directionalShadow.ts +84 -10
  116. package/src/render/shaders/flat/glassTint.ts +7 -1
  117. package/src/render/shaders/flat/index.ts +30 -2
  118. package/src/render/shaders/flat/layered.ts +115 -0
  119. package/src/render/shaders/flat/main.ts +43 -16
  120. package/src/render/shaders/flat/mapReads.ts +38 -0
  121. package/src/render/shaders/flat/movingSun.ts +46 -0
  122. package/src/render/shaders/flat/normalMap.ts +14 -3
  123. package/src/render/shaders/flat/overlay.ts +41 -19
  124. package/src/render/shaders/flat/preamble.ts +23 -5
  125. package/src/render/shaders/flat/worldUv.ts +113 -0
  126. package/src/render/shaders/generated/flat.wgsl.ts +791 -391
  127. package/src/render/shaders/generated/globalMedium.wgsl.ts +8 -3
  128. package/src/render/shaders/generated/lightVolume.wgsl.ts +16 -11
  129. package/src/render/shaders/globalMedium.ts +9 -1
  130. package/src/render/shaders/lightVolume.ts +9 -1
  131. package/src/render/shaders/tangentFrame.ts +16 -3
  132. package/src/render/skinnedClothRun.ts +138 -38
  133. package/src/render/surfaceLayers.ts +57 -0
  134. package/src/render/surfaceOverlay.ts +24 -7
  135. package/src/render/surfaceProjection.ts +57 -0
  136. package/src/render/surfaceTexture.ts +30 -0
  137. package/src/render/vertexPacking.ts +109 -0
  138. package/dist/render/backend/webgpu/gpuSkinnedCloth.d.ts +0 -74
  139. package/dist/render/backend/webgpu/gpuSkinnedCloth.js +0 -452
  140. 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
  `;
@@ -175,6 +175,7 @@ export function takeRecorded(pool) {
175
175
  command.offsetCount = 0;
176
176
  command.vertexCount = 0;
177
177
  command.indexBuffer = null;
178
+ command.indexFormat = 'uint32';
178
179
  command.indexed = false;
179
180
  command.instances = 1;
180
181
  command.indirect = null;
@@ -213,7 +214,7 @@ export function encodeBundle(device, target, pool, passOffset, viewOffset, label
213
214
  encoder.setVertexBuffer(slot, buffer);
214
215
  }
215
216
  if (command.indexed && command.indexBuffer !== null) {
216
- encoder.setIndexBuffer(command.indexBuffer, 'uint32');
217
+ encoder.setIndexBuffer(command.indexBuffer, command.indexFormat);
217
218
  if (command.indirect !== null)
218
219
  encoder.drawIndexedIndirect(command.indirect, 0);
219
220
  else
@@ -0,0 +1,22 @@
1
+ import { type AttributePacking, type VertexPacking } from '../../vertexPacking.ts';
2
+ /** The key's suffix for this packing: one word each attribute not travelling as floats. */
3
+ export declare function packingKey(packing: VertexPacking): string;
4
+ /** Mark this packing in a pipeline's `present`, which is what `vertexBufferLayouts` reads. */
5
+ export declare function markPacking(present: Record<string, boolean>, packing: VertexPacking): void;
6
+ /** Mark the packing a mesh's key names, for a pass that has the mesh and not its data. */
7
+ export declare function markPackingOfKey(present: Record<string, boolean>, key: string): void;
8
+ /** How `present` says this attribute travels: floats unless a word says otherwise. */
9
+ export declare function packingIn(present: Readonly<Record<string, boolean>>, name: string): AttributePacking;
10
+ /** The format an attribute is read with, packed this way, or `float` as given. */
11
+ export declare function packedFormat(float: GPUVertexFormat, packing: AttributePacking): GPUVertexFormat;
12
+ /** Bytes a vertex spends on one attribute read with `format`. */
13
+ export declare function formatBytes(format: GPUVertexFormat): number;
14
+ /**
15
+ * One attribute of vertices `from` to `to` into its sixteen-bit field of the interleaved rows,
16
+ * `scale` being `SNORM16_ONE` or `UNORM16_ONE`. Rounded by `toFixed16`, as `fixedArray` rounds
17
+ * for the other backend; the range was checked when the packing was chosen.
18
+ *
19
+ * Unrolled by width, as `interleaveField` is and for its reason: the general loop was most of a
20
+ * heavy load's upload. Three and four are the widths a packed attribute has.
21
+ */
22
+ export declare function interleaveFixed(rows: Int16Array | Uint16Array, source: Float32Array, width: number, step: number, fieldOffset: number, from: number, to: number, scale: number): void;
@@ -0,0 +1,97 @@
1
+ import { PACKED_ATTRIBUTES, toFixed16, } from '../../vertexPacking.js';
2
+ /**
3
+ * A mesh's packed attributes as a WebGPU vertex reads them: each one's format and bytes, the words
4
+ * a pipeline key carries for them, and the writers that put sixteen-bit fixed point into the
5
+ * interleaved rows. What is packed how is `vertexPacking.ts`'s decision; this is only its binding.
6
+ *
7
+ * **Sixteen-bit fields are four wide, never three**, because WebGPU has no three-component
8
+ * sixteen-bit format: a normal or a colour takes `x4` and leaves the fourth zero, and the shader's
9
+ * `vec3` input reads the first three. So a normal costs eight bytes here against six on WebGL2,
10
+ * which reads three. Every field stays a whole number of four-byte words, so a vertex does too.
11
+ */
12
+ /** The ways an attribute can travel other than as floats, as the key words name them. */
13
+ const PACKINGS = ['snorm16', 'unorm16', 'constant'];
14
+ /** The pipeline key's word for one attribute's packing. Not a prefix of any other word. */
15
+ function word(name, packing) {
16
+ return `${name}~${packing}`;
17
+ }
18
+ /** The key's suffix for this packing: one word each attribute not travelling as floats. */
19
+ export function packingKey(packing) {
20
+ let key = '';
21
+ for (const name of PACKED_ATTRIBUTES) {
22
+ if (packing[name] !== 'float')
23
+ key += `:${word(name, packing[name])}`;
24
+ }
25
+ return key;
26
+ }
27
+ /** Mark this packing in a pipeline's `present`, which is what `vertexBufferLayouts` reads. */
28
+ export function markPacking(present, packing) {
29
+ for (const name of PACKED_ATTRIBUTES) {
30
+ if (packing[name] !== 'float')
31
+ present[word(name, packing[name])] = true;
32
+ }
33
+ }
34
+ /** Mark the packing a mesh's key names, for a pass that has the mesh and not its data. */
35
+ export function markPackingOfKey(present, key) {
36
+ for (const name of PACKED_ATTRIBUTES) {
37
+ for (const packing of PACKINGS) {
38
+ if (key.includes(`:${word(name, packing)}`))
39
+ present[word(name, packing)] = true;
40
+ }
41
+ }
42
+ }
43
+ /** How `present` says this attribute travels: floats unless a word says otherwise. */
44
+ export function packingIn(present, name) {
45
+ for (const packing of PACKINGS) {
46
+ if (present[word(name, packing)] === true)
47
+ return packing;
48
+ }
49
+ return 'float';
50
+ }
51
+ /** The format an attribute is read with, packed this way, or `float` as given. */
52
+ export function packedFormat(float, packing) {
53
+ if (packing === 'snorm16')
54
+ return 'snorm16x4';
55
+ if (packing === 'unorm16')
56
+ return 'unorm16x4';
57
+ return float;
58
+ }
59
+ /** Bytes a vertex spends on one attribute read with `format`. */
60
+ export function formatBytes(format) {
61
+ if (format === 'snorm16x4' || format === 'unorm16x4')
62
+ return 8;
63
+ if (format === 'float32x4')
64
+ return 16;
65
+ if (format === 'float32x3')
66
+ return 12;
67
+ if (format === 'float32x2')
68
+ return 8;
69
+ return 4;
70
+ }
71
+ /**
72
+ * One attribute of vertices `from` to `to` into its sixteen-bit field of the interleaved rows,
73
+ * `scale` being `SNORM16_ONE` or `UNORM16_ONE`. Rounded by `toFixed16`, as `fixedArray` rounds
74
+ * for the other backend; the range was checked when the packing was chosen.
75
+ *
76
+ * Unrolled by width, as `interleaveField` is and for its reason: the general loop was most of a
77
+ * heavy load's upload. Three and four are the widths a packed attribute has.
78
+ */
79
+ export function interleaveFixed(rows, source, width, step, fieldOffset, from, to, scale) {
80
+ let write = from * step + fieldOffset;
81
+ const end = to * width;
82
+ if (width === 3) {
83
+ for (let read = from * 3; read < end; read += 3, write += step) {
84
+ rows[write] = toFixed16(source[read], scale);
85
+ rows[write + 1] = toFixed16(source[read + 1], scale);
86
+ rows[write + 2] = toFixed16(source[read + 2], scale);
87
+ }
88
+ }
89
+ else {
90
+ for (let read = from * 4; read < end; read += 4, write += step) {
91
+ rows[write] = toFixed16(source[read], scale);
92
+ rows[write + 1] = toFixed16(source[read + 1], scale);
93
+ rows[write + 2] = toFixed16(source[read + 2], scale);
94
+ rows[write + 3] = toFixed16(source[read + 3], scale);
95
+ }
96
+ }
97
+ }
@@ -11,6 +11,8 @@ export interface MediumLight {
11
11
  readonly sunShadow: number;
12
12
  /** World → light clip, **uncorrected**: the shader does its own `* 0.5 + 0.5`. */
13
13
  readonly lightViewProj: ReadonlyMat4;
14
+ /** The same for the sun's moving layer, whose shadow pass may be drawn with its own. */
15
+ readonly movingLightViewProj: ReadonlyMat4;
14
16
  /** The sun's static, moving and peeled maps, as one array. See `shadowMap.ts`. */
15
17
  readonly sunShadows: WebGLTexture;
16
18
  readonly peeledEnabled: boolean;
@@ -173,6 +173,7 @@ export class GlobalMediumPass {
173
173
  gl.uniform1i(u['uSteps'] ?? null, steps);
174
174
  gl.uniform1f(u['uSunShadow'] ?? null, light.sunShadow);
175
175
  gl.uniformMatrix4fv(u['uLightViewProj'] ?? null, false, light.lightViewProj);
176
+ gl.uniformMatrix4fv(u['uMovingLightViewProj'] ?? null, false, light.movingLightViewProj);
176
177
  gl.uniform1i(u['uPeeledShadowEnabled'] ?? null, light.peeledEnabled ? 1 : 0);
177
178
  gl.drawArrays(gl.TRIANGLES, 0, 3);
178
179
  gl.bindVertexArray(null);
@@ -0,0 +1,17 @@
1
+ /**
2
+ * A mesh's indices in the narrowest type that holds them: sixteen bits where every vertex can be
3
+ * named in sixteen, thirty-two otherwise — half the index memory for every mesh of 65,536 vertices
4
+ * or fewer, which is most of them. Lossless because `validateMeshData` has already refused an
5
+ * index past the last vertex.
6
+ *
7
+ * **Padded to an even count**, so the sixteen-bit copy is a whole number of four-byte words: WebGPU
8
+ * writes a buffer only in whole words. The extra index is never drawn, since every draw counts the
9
+ * mesh's own indices. What would make this wrong is a draw that counts the buffer instead.
10
+ *
11
+ * **What it costs** is a copy of the indices while the mesh is built, let go once they are on the
12
+ * device; and every draw of the mesh has to say which width it holds, which is why the width
13
+ * travels with the mesh rather than being worked out where it is drawn.
14
+ */
15
+ /** The most vertices sixteen-bit indices can name: every index below 65,536. */
16
+ export declare const MAX_UINT16_VERTICES = 65536;
17
+ export declare function compactIndices(indices: Uint32Array, vertexCount: number): Uint16Array | Uint32Array;
@@ -0,0 +1,23 @@
1
+ /**
2
+ * A mesh's indices in the narrowest type that holds them: sixteen bits where every vertex can be
3
+ * named in sixteen, thirty-two otherwise — half the index memory for every mesh of 65,536 vertices
4
+ * or fewer, which is most of them. Lossless because `validateMeshData` has already refused an
5
+ * index past the last vertex.
6
+ *
7
+ * **Padded to an even count**, so the sixteen-bit copy is a whole number of four-byte words: WebGPU
8
+ * writes a buffer only in whole words. The extra index is never drawn, since every draw counts the
9
+ * mesh's own indices. What would make this wrong is a draw that counts the buffer instead.
10
+ *
11
+ * **What it costs** is a copy of the indices while the mesh is built, let go once they are on the
12
+ * device; and every draw of the mesh has to say which width it holds, which is why the width
13
+ * travels with the mesh rather than being worked out where it is drawn.
14
+ */
15
+ /** The most vertices sixteen-bit indices can name: every index below 65,536. */
16
+ export const MAX_UINT16_VERTICES = 65536;
17
+ export function compactIndices(indices, vertexCount) {
18
+ if (vertexCount > MAX_UINT16_VERTICES)
19
+ return indices;
20
+ const out = new Uint16Array(indices.length + (indices.length % 2));
21
+ out.set(indices);
22
+ return out;
23
+ }