@codexo/exojs 0.7.12 → 0.8.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 (219) hide show
  1. package/CHANGELOG.md +737 -0
  2. package/dist/esm/core/Application.d.ts +3 -1
  3. package/dist/esm/core/Application.js +7 -6
  4. package/dist/esm/core/Application.js.map +1 -1
  5. package/dist/esm/core/Scene.d.ts +30 -0
  6. package/dist/esm/core/Scene.js +56 -0
  7. package/dist/esm/core/Scene.js.map +1 -1
  8. package/dist/esm/core/SceneManager.js +2 -2
  9. package/dist/esm/core/SceneManager.js.map +1 -1
  10. package/dist/esm/debug/DebugOverlay.js +2 -2
  11. package/dist/esm/debug/DebugOverlay.js.map +1 -1
  12. package/dist/esm/debug/PointerStackLayer.js +1 -1
  13. package/dist/esm/debug/PointerStackLayer.js.map +1 -1
  14. package/dist/esm/index.js +32 -10
  15. package/dist/esm/index.js.map +1 -1
  16. package/dist/esm/input/ArcadeStickGamepadMapping.js +18 -19
  17. package/dist/esm/input/ArcadeStickGamepadMapping.js.map +1 -1
  18. package/dist/esm/input/Gamepad.d.ts +164 -62
  19. package/dist/esm/input/Gamepad.js +290 -134
  20. package/dist/esm/input/Gamepad.js.map +1 -1
  21. package/dist/esm/input/GamepadAxis.d.ts +120 -0
  22. package/dist/esm/input/GamepadAxis.js +106 -0
  23. package/dist/esm/input/GamepadAxis.js.map +1 -0
  24. package/dist/esm/input/GamepadButton.d.ts +110 -0
  25. package/dist/esm/input/GamepadButton.js +99 -0
  26. package/dist/esm/input/GamepadButton.js.map +1 -0
  27. package/dist/esm/input/GamepadDefinitions.js +4 -0
  28. package/dist/esm/input/GamepadDefinitions.js.map +1 -1
  29. package/dist/esm/input/GamepadMapping.d.ts +28 -24
  30. package/dist/esm/input/GamepadMapping.js +33 -16
  31. package/dist/esm/input/GamepadMapping.js.map +1 -1
  32. package/dist/esm/input/GamepadPromptLayouts.d.ts +10 -8
  33. package/dist/esm/input/GamepadPromptLayouts.js +21 -20
  34. package/dist/esm/input/GamepadPromptLayouts.js.map +1 -1
  35. package/dist/esm/input/GenericDualAnalogGamepadMapping.d.ts +6 -3
  36. package/dist/esm/input/GenericDualAnalogGamepadMapping.js +55 -46
  37. package/dist/esm/input/GenericDualAnalogGamepadMapping.js.map +1 -1
  38. package/dist/esm/input/InputBinding.d.ts +74 -0
  39. package/dist/esm/input/InputBinding.js +100 -0
  40. package/dist/esm/input/InputBinding.js.map +1 -0
  41. package/dist/esm/input/InputManager.d.ts +79 -33
  42. package/dist/esm/input/InputManager.js +229 -104
  43. package/dist/esm/input/InputManager.js.map +1 -1
  44. package/dist/esm/input/InteractionManager.d.ts +1 -1
  45. package/dist/esm/input/InteractionManager.js +13 -13
  46. package/dist/esm/input/InteractionManager.js.map +1 -1
  47. package/dist/esm/input/JoyConLeftGamepadMapping.d.ts +14 -9
  48. package/dist/esm/input/JoyConLeftGamepadMapping.js +39 -9
  49. package/dist/esm/input/JoyConLeftGamepadMapping.js.map +1 -1
  50. package/dist/esm/input/JoyConRightGamepadMapping.d.ts +14 -9
  51. package/dist/esm/input/JoyConRightGamepadMapping.js +35 -9
  52. package/dist/esm/input/JoyConRightGamepadMapping.js.map +1 -1
  53. package/dist/esm/input/Pointer.d.ts +84 -71
  54. package/dist/esm/input/Pointer.js +71 -71
  55. package/dist/esm/input/Pointer.js.map +1 -1
  56. package/dist/esm/input/SteamDeckGamepadMapping.d.ts +18 -0
  57. package/dist/esm/input/SteamDeckGamepadMapping.js +76 -0
  58. package/dist/esm/input/SteamDeckGamepadMapping.js.map +1 -0
  59. package/dist/esm/input/index.d.ts +7 -4
  60. package/dist/esm/input/types.d.ts +0 -76
  61. package/dist/esm/input/types.js +1 -80
  62. package/dist/esm/input/types.js.map +1 -1
  63. package/dist/esm/particles/ParticleSystem.d.ts +180 -83
  64. package/dist/esm/particles/ParticleSystem.js +446 -133
  65. package/dist/esm/particles/ParticleSystem.js.map +1 -1
  66. package/dist/esm/particles/distributions/BoxArea.d.ts +17 -0
  67. package/dist/esm/particles/distributions/BoxArea.js +48 -0
  68. package/dist/esm/particles/distributions/BoxArea.js.map +1 -0
  69. package/dist/esm/particles/distributions/CircleArea.d.ts +19 -0
  70. package/dist/esm/particles/distributions/CircleArea.js +33 -0
  71. package/dist/esm/particles/distributions/CircleArea.js.map +1 -0
  72. package/dist/esm/particles/distributions/ConeDirection.d.ts +28 -0
  73. package/dist/esm/particles/distributions/ConeDirection.js +44 -0
  74. package/dist/esm/particles/distributions/ConeDirection.js.map +1 -0
  75. package/dist/esm/particles/distributions/Constant.d.ts +17 -0
  76. package/dist/esm/particles/distributions/Constant.js +35 -0
  77. package/dist/esm/particles/distributions/Constant.js.map +1 -0
  78. package/dist/esm/particles/distributions/Curve.d.ts +30 -0
  79. package/dist/esm/particles/distributions/Curve.js +53 -0
  80. package/dist/esm/particles/distributions/Curve.js.map +1 -0
  81. package/dist/esm/particles/distributions/Distribution.d.ts +45 -0
  82. package/dist/esm/particles/distributions/Gradient.d.ts +40 -0
  83. package/dist/esm/particles/distributions/Gradient.js +72 -0
  84. package/dist/esm/particles/distributions/Gradient.js.map +1 -0
  85. package/dist/esm/particles/distributions/LineSegment.d.ts +15 -0
  86. package/dist/esm/particles/distributions/LineSegment.js +27 -0
  87. package/dist/esm/particles/distributions/LineSegment.js.map +1 -0
  88. package/dist/esm/particles/distributions/Range.d.ts +12 -0
  89. package/dist/esm/particles/distributions/Range.js +19 -0
  90. package/dist/esm/particles/distributions/Range.js.map +1 -0
  91. package/dist/esm/particles/distributions/VectorRange.d.ts +20 -0
  92. package/dist/esm/particles/distributions/VectorRange.js +31 -0
  93. package/dist/esm/particles/distributions/VectorRange.js.map +1 -0
  94. package/dist/esm/particles/distributions/index.d.ts +12 -0
  95. package/dist/esm/particles/gpu/ParticleGpuState.d.ts +57 -0
  96. package/dist/esm/particles/gpu/ParticleGpuState.js +508 -0
  97. package/dist/esm/particles/gpu/ParticleGpuState.js.map +1 -0
  98. package/dist/esm/particles/index.d.ts +2 -10
  99. package/dist/esm/particles/modules/AlphaFadeOverLifetime.d.ts +24 -0
  100. package/dist/esm/particles/modules/AlphaFadeOverLifetime.js +60 -0
  101. package/dist/esm/particles/modules/AlphaFadeOverLifetime.js.map +1 -0
  102. package/dist/esm/particles/modules/ApplyForce.d.ts +20 -0
  103. package/dist/esm/particles/modules/ApplyForce.js +48 -0
  104. package/dist/esm/particles/modules/ApplyForce.js.map +1 -0
  105. package/dist/esm/particles/modules/AttractToPoint.d.ts +27 -0
  106. package/dist/esm/particles/modules/AttractToPoint.js +73 -0
  107. package/dist/esm/particles/modules/AttractToPoint.js.map +1 -0
  108. package/dist/esm/particles/modules/BurstSpawn.d.ts +53 -0
  109. package/dist/esm/particles/modules/BurstSpawn.js +94 -0
  110. package/dist/esm/particles/modules/BurstSpawn.js.map +1 -0
  111. package/dist/esm/particles/modules/ColorOverLifetime.d.ts +22 -0
  112. package/dist/esm/particles/modules/ColorOverLifetime.js +65 -0
  113. package/dist/esm/particles/modules/ColorOverLifetime.js.map +1 -0
  114. package/dist/esm/particles/modules/ColorOverSpeed.d.ts +27 -0
  115. package/dist/esm/particles/modules/ColorOverSpeed.js +86 -0
  116. package/dist/esm/particles/modules/ColorOverSpeed.js.map +1 -0
  117. package/dist/esm/particles/modules/DeathModule.d.ts +24 -0
  118. package/dist/esm/particles/modules/DeathModule.js +25 -0
  119. package/dist/esm/particles/modules/DeathModule.js.map +1 -0
  120. package/dist/esm/particles/modules/Drag.d.ts +20 -0
  121. package/dist/esm/particles/modules/Drag.js +45 -0
  122. package/dist/esm/particles/modules/Drag.js.map +1 -0
  123. package/dist/esm/particles/modules/OrbitalForce.d.ts +28 -0
  124. package/dist/esm/particles/modules/OrbitalForce.js +65 -0
  125. package/dist/esm/particles/modules/OrbitalForce.js.map +1 -0
  126. package/dist/esm/particles/modules/RateSpawn.d.ts +41 -0
  127. package/dist/esm/particles/modules/RateSpawn.js +76 -0
  128. package/dist/esm/particles/modules/RateSpawn.js.map +1 -0
  129. package/dist/esm/particles/modules/RepelFromPoint.d.ts +24 -0
  130. package/dist/esm/particles/modules/RepelFromPoint.js +76 -0
  131. package/dist/esm/particles/modules/RepelFromPoint.js.map +1 -0
  132. package/dist/esm/particles/modules/RotateOverLifetime.d.ts +20 -0
  133. package/dist/esm/particles/modules/RotateOverLifetime.js +43 -0
  134. package/dist/esm/particles/modules/RotateOverLifetime.js.map +1 -0
  135. package/dist/esm/particles/modules/ScaleOverLifetime.d.ts +26 -0
  136. package/dist/esm/particles/modules/ScaleOverLifetime.js +59 -0
  137. package/dist/esm/particles/modules/ScaleOverLifetime.js.map +1 -0
  138. package/dist/esm/particles/modules/SpawnModule.d.ts +30 -0
  139. package/dist/esm/particles/modules/SpawnModule.js +31 -0
  140. package/dist/esm/particles/modules/SpawnModule.js.map +1 -0
  141. package/dist/esm/particles/modules/SpawnOnDeath.d.ts +24 -0
  142. package/dist/esm/particles/modules/SpawnOnDeath.js +47 -0
  143. package/dist/esm/particles/modules/SpawnOnDeath.js.map +1 -0
  144. package/dist/esm/particles/modules/Turbulence.d.ts +30 -0
  145. package/dist/esm/particles/modules/Turbulence.js +122 -0
  146. package/dist/esm/particles/modules/Turbulence.js.map +1 -0
  147. package/dist/esm/particles/modules/UpdateModule.d.ts +95 -0
  148. package/dist/esm/particles/modules/UpdateModule.js +66 -0
  149. package/dist/esm/particles/modules/UpdateModule.js.map +1 -0
  150. package/dist/esm/particles/modules/VelocityOverLifetime.d.ts +30 -0
  151. package/dist/esm/particles/modules/VelocityOverLifetime.js +84 -0
  152. package/dist/esm/particles/modules/VelocityOverLifetime.js.map +1 -0
  153. package/dist/esm/particles/modules/WgslContribution.d.ts +81 -0
  154. package/dist/esm/particles/modules/WgslContribution.js +34 -0
  155. package/dist/esm/particles/modules/WgslContribution.js.map +1 -0
  156. package/dist/esm/particles/modules/index.d.ts +22 -0
  157. package/dist/esm/rendering/webgl2/WebGl2ParticleRenderer.d.ts +9 -14
  158. package/dist/esm/rendering/webgl2/WebGl2ParticleRenderer.js +90 -61
  159. package/dist/esm/rendering/webgl2/WebGl2ParticleRenderer.js.map +1 -1
  160. package/dist/esm/rendering/webgl2/glsl/particle.vert.js +1 -1
  161. package/dist/esm/rendering/webgpu/WebGpuParticleRenderer.d.ts +9 -0
  162. package/dist/esm/rendering/webgpu/WebGpuParticleRenderer.js +107 -23
  163. package/dist/esm/rendering/webgpu/WebGpuParticleRenderer.js.map +1 -1
  164. package/dist/esm/rendering/webgpu/compute/WebGpuComputePipeline.d.ts +52 -0
  165. package/dist/esm/rendering/webgpu/compute/WebGpuStorageBuffer.d.ts +29 -0
  166. package/dist/esm/rendering/webgpu/compute/index.d.ts +3 -0
  167. package/dist/esm/resources/CacheFirstStrategy.d.ts +7 -4
  168. package/dist/esm/resources/CacheFirstStrategy.js +11 -8
  169. package/dist/esm/resources/CacheFirstStrategy.js.map +1 -1
  170. package/dist/esm/resources/CacheStrategy.d.ts +14 -6
  171. package/dist/esm/resources/Loader.d.ts +8 -3
  172. package/dist/esm/resources/Loader.js +19 -37
  173. package/dist/esm/resources/Loader.js.map +1 -1
  174. package/dist/esm/resources/NetworkOnlyStrategy.d.ts +3 -0
  175. package/dist/esm/resources/NetworkOnlyStrategy.js +8 -3
  176. package/dist/esm/resources/NetworkOnlyStrategy.js.map +1 -1
  177. package/dist/esm/resources/factories/ImageFactory.d.ts +2 -2
  178. package/dist/esm/resources/factories/ImageFactory.js.map +1 -1
  179. package/dist/esm/resources/factories/TextureFactory.d.ts +2 -2
  180. package/dist/esm/resources/factories/TextureFactory.js.map +1 -1
  181. package/dist/esm/resources/factories/VttFactory.d.ts +3 -3
  182. package/dist/esm/resources/factories/VttFactory.js +83 -6
  183. package/dist/esm/resources/factories/VttFactory.js.map +1 -1
  184. package/dist/exo.esm.js +4028 -1518
  185. package/dist/exo.esm.js.map +1 -1
  186. package/package.json +2 -1
  187. package/dist/esm/input/GamepadChannels.d.ts +0 -47
  188. package/dist/esm/input/GamepadChannels.js +0 -53
  189. package/dist/esm/input/GamepadChannels.js.map +0 -1
  190. package/dist/esm/input/GamepadControl.d.ts +0 -33
  191. package/dist/esm/input/GamepadControl.js +0 -42
  192. package/dist/esm/input/GamepadControl.js.map +0 -1
  193. package/dist/esm/input/Input.d.ts +0 -52
  194. package/dist/esm/input/Input.js +0 -90
  195. package/dist/esm/input/Input.js.map +0 -1
  196. package/dist/esm/particles/Particle.d.ts +0 -77
  197. package/dist/esm/particles/Particle.js +0 -143
  198. package/dist/esm/particles/Particle.js.map +0 -1
  199. package/dist/esm/particles/ParticleProperties.d.ts +0 -29
  200. package/dist/esm/particles/affectors/ColorAffector.d.ts +0 -30
  201. package/dist/esm/particles/affectors/ColorAffector.js +0 -55
  202. package/dist/esm/particles/affectors/ColorAffector.js.map +0 -1
  203. package/dist/esm/particles/affectors/ForceAffector.d.ts +0 -24
  204. package/dist/esm/particles/affectors/ForceAffector.js +0 -39
  205. package/dist/esm/particles/affectors/ForceAffector.js.map +0 -1
  206. package/dist/esm/particles/affectors/ParticleAffector.d.ts +0 -19
  207. package/dist/esm/particles/affectors/ScaleAffector.d.ts +0 -23
  208. package/dist/esm/particles/affectors/ScaleAffector.js +0 -38
  209. package/dist/esm/particles/affectors/ScaleAffector.js.map +0 -1
  210. package/dist/esm/particles/affectors/TorqueAffector.d.ts +0 -23
  211. package/dist/esm/particles/affectors/TorqueAffector.js +0 -37
  212. package/dist/esm/particles/affectors/TorqueAffector.js.map +0 -1
  213. package/dist/esm/particles/emitters/ParticleEmitter.d.ts +0 -19
  214. package/dist/esm/particles/emitters/ParticleOptions.d.ts +0 -62
  215. package/dist/esm/particles/emitters/ParticleOptions.js +0 -120
  216. package/dist/esm/particles/emitters/ParticleOptions.js.map +0 -1
  217. package/dist/esm/particles/emitters/UniversalEmitter.d.ts +0 -40
  218. package/dist/esm/particles/emitters/UniversalEmitter.js +0 -68
  219. package/dist/esm/particles/emitters/UniversalEmitter.js.map +0 -1
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Uniform random number in `[min, max]`. Each `sample()` returns a fresh
3
+ * roll; the bounds are inclusive on both ends (modulo the rounding bias
4
+ * inherent to `Math.random()`).
5
+ */
6
+ class Range {
7
+ min;
8
+ max;
9
+ constructor(min, max) {
10
+ this.min = min;
11
+ this.max = max;
12
+ }
13
+ sample() {
14
+ return this.min + Math.random() * (this.max - this.min);
15
+ }
16
+ }
17
+
18
+ export { Range };
19
+ //# sourceMappingURL=Range.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Range.js","sources":["../../../../../src/particles/distributions/Range.ts"],"sourcesContent":[null],"names":[],"mappings":"AAEA;;;;AAIG;MACU,KAAK,CAAA;AACY,IAAA,GAAA;AAAoB,IAAA,GAAA;IAA9C,WAAA,CAA0B,GAAW,EAAS,GAAW,EAAA;QAA/B,IAAA,CAAA,GAAG,GAAH,GAAG;QAAiB,IAAA,CAAA,GAAG,GAAH,GAAG;IAAW;IAErD,MAAM,GAAA;AACT,QAAA,OAAO,IAAI,CAAC,GAAG,GAAG,IAAI,CAAC,MAAM,EAAE,IAAI,IAAI,CAAC,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC;IAC3D;AACH;;;;"}
@@ -0,0 +1,20 @@
1
+ import { Vector } from '@/math/Vector';
2
+ import type { Distribution } from './Distribution';
3
+ /**
4
+ * Uniform random vector with each axis sampled independently in its own
5
+ * `[min, max]` range. Each `sample()` writes into the provided `out` Vector
6
+ * (or an internal scratch instance when `out` is omitted).
7
+ *
8
+ * @example
9
+ * const knockback = new VectorRange(-300, 300, -800, -200); // any X, upward Y
10
+ * knockback.sample(particle.velocity); // writes into existing instance, no alloc
11
+ */
12
+ export declare class VectorRange implements Distribution<Vector> {
13
+ minX: number;
14
+ maxX: number;
15
+ minY: number;
16
+ maxY: number;
17
+ private readonly _scratch;
18
+ constructor(minX: number, maxX: number, minY: number, maxY: number);
19
+ sample(out?: Vector): Vector;
20
+ }
@@ -0,0 +1,31 @@
1
+ import { Vector } from '../../math/Vector.js';
2
+
3
+ /**
4
+ * Uniform random vector with each axis sampled independently in its own
5
+ * `[min, max]` range. Each `sample()` writes into the provided `out` Vector
6
+ * (or an internal scratch instance when `out` is omitted).
7
+ *
8
+ * @example
9
+ * const knockback = new VectorRange(-300, 300, -800, -200); // any X, upward Y
10
+ * knockback.sample(particle.velocity); // writes into existing instance, no alloc
11
+ */
12
+ class VectorRange {
13
+ minX;
14
+ maxX;
15
+ minY;
16
+ maxY;
17
+ _scratch = new Vector();
18
+ constructor(minX, maxX, minY, maxY) {
19
+ this.minX = minX;
20
+ this.maxX = maxX;
21
+ this.minY = minY;
22
+ this.maxY = maxY;
23
+ }
24
+ sample(out = this._scratch) {
25
+ out.set(this.minX + Math.random() * (this.maxX - this.minX), this.minY + Math.random() * (this.maxY - this.minY));
26
+ return out;
27
+ }
28
+ }
29
+
30
+ export { VectorRange };
31
+ //# sourceMappingURL=VectorRange.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"VectorRange.js","sources":["../../../../../src/particles/distributions/VectorRange.ts"],"sourcesContent":[null],"names":[],"mappings":";;AAGA;;;;;;;;AAQG;MACU,WAAW,CAAA;AAIT,IAAA,IAAA;AACA,IAAA,IAAA;AACA,IAAA,IAAA;AACA,IAAA,IAAA;AANM,IAAA,QAAQ,GAAG,IAAI,MAAM,EAAE;AAExC,IAAA,WAAA,CACW,IAAY,EACZ,IAAY,EACZ,IAAY,EACZ,IAAY,EAAA;QAHZ,IAAA,CAAA,IAAI,GAAJ,IAAI;QACJ,IAAA,CAAA,IAAI,GAAJ,IAAI;QACJ,IAAA,CAAA,IAAI,GAAJ,IAAI;QACJ,IAAA,CAAA,IAAI,GAAJ,IAAI;IACZ;AAEI,IAAA,MAAM,CAAC,GAAA,GAAc,IAAI,CAAC,QAAQ,EAAA;AACrC,QAAA,GAAG,CAAC,GAAG,CACH,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,MAAM,EAAE,IAAI,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,EACnD,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,MAAM,EAAE,IAAI,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,CACtD;AAED,QAAA,OAAO,GAAG;IACd;AACH;;;;"}
@@ -0,0 +1,12 @@
1
+ export type { Distribution, LifetimeFunction } from './Distribution';
2
+ export { Constant } from './Constant';
3
+ export { Range } from './Range';
4
+ export { VectorRange } from './VectorRange';
5
+ export { ConeDirection } from './ConeDirection';
6
+ export { CircleArea } from './CircleArea';
7
+ export { BoxArea } from './BoxArea';
8
+ export { LineSegment } from './LineSegment';
9
+ export { Curve } from './Curve';
10
+ export type { CurveKey } from './Curve';
11
+ export { Gradient } from './Gradient';
12
+ export type { GradientKey } from './Gradient';
@@ -0,0 +1,57 @@
1
+ import type { Rectangle } from '@/math/Rectangle';
2
+ import type { Texture } from '@/rendering/texture/Texture';
3
+ import type { ParticleSystem } from '@/particles/ParticleSystem';
4
+ import type { UpdateModule } from '@/particles/modules/UpdateModule';
5
+ export declare class ParticleGpuState {
6
+ readonly device: GPUDevice;
7
+ readonly capacity: number;
8
+ /** GPU buffer holding interleaved per-instance vertex data, written by compute, read as VERTEX by the renderer. */
9
+ readonly instanceBuffer: GPUBuffer;
10
+ private readonly _positions;
11
+ private readonly _velocities;
12
+ private readonly _scales;
13
+ private readonly _rotInfo;
14
+ private readonly _timing;
15
+ private readonly _color;
16
+ private readonly _textureIndex;
17
+ private readonly _simUniformBuffer;
18
+ private readonly _simUniformData;
19
+ private readonly _simUniformView;
20
+ private readonly _moduleUniformBuffer;
21
+ private readonly _moduleUniformData;
22
+ private readonly _moduleUniformView;
23
+ private readonly _moduleSlots;
24
+ private readonly _framesUniformBuffer;
25
+ private readonly _framesUniformData;
26
+ private readonly _framesUniformView;
27
+ private readonly _frameCount;
28
+ private readonly _moduleTextures;
29
+ private readonly _samplerFiltering;
30
+ private readonly _samplerNonFiltering;
31
+ private readonly _pipeline;
32
+ private readonly _bindGroup0;
33
+ private readonly _bindGroup1;
34
+ constructor(device: GPUDevice, capacity: number, modules: ReadonlyArray<UpdateModule>, frames: ReadonlyArray<Rectangle>, texture: Texture);
35
+ dispatch(system: ParticleSystem, dt: number): void;
36
+ destroy(): void;
37
+ private _writeFrames;
38
+ /**
39
+ * Push the listed CPU SoA slots to the GPU. Called by `ParticleSystem`
40
+ * with newly-spawned slots and just-expired slots (lifetime sentinel).
41
+ * Slots not in the dirty set are left alone — GPU keeps the integrated
42
+ * state from previous compute dispatches.
43
+ *
44
+ * Each dirty slot triggers 7 small `queue.writeBuffer` calls (one per
45
+ * SoA channel). For typical spawn rates (≤200/s) this is negligible
46
+ * (≤1400 calls/s); contiguous-range batching is a future optimisation.
47
+ */
48
+ uploadDirty(system: ParticleSystem, slots: Iterable<number>): void;
49
+ private readonly _dirtyScratchVec2;
50
+ private readonly _dirtyScratchU32;
51
+ private _writeSimUniforms;
52
+ private _writeModuleUniforms;
53
+ private _buildBindGroup0Layout;
54
+ private _buildBindGroup0;
55
+ private _buildShader;
56
+ private _renderModuleStruct;
57
+ }
@@ -0,0 +1,508 @@
1
+ import { wgslUniformByteSize } from '../modules/WgslContribution.js';
2
+
3
+ /// <reference types="@webgpu/types" />
4
+ /**
5
+ * GPU-side mirror of one {@link ParticleSystem}. Owns:
6
+ *
7
+ * - **8 packed storage buffers** for the per-particle SoA data:
8
+ * positions/velocities/scales/rotInfo/timing as `vec2<f32>`, color and
9
+ * textureIndex as `u32`, plus the instance output buffer. Sits at the
10
+ * default WebGPU `maxStorageBuffersPerShaderStage = 8` limit.
11
+ * - **One uniform buffer** for sim state (`dt`, `liveCount`).
12
+ * - **One uniform buffer** for module configs (concatenated per-module
13
+ * structs with WGSL std140-ish alignment).
14
+ * - **One uniform buffer** for frame UVs — `array<vec4<f32>, N>` where N
15
+ * is the system's frame count (or 1 when no atlas is declared). Each
16
+ * vec4 is `(uvMinX, uvMinY, uvMaxX, uvMaxY)` already flipY-adjusted.
17
+ * - **N 1D textures** for modules that use lookup tables (Curve / Gradient).
18
+ * - **Composite compute pipeline** built once at construction by
19
+ * concatenating the integration step + every registered module body +
20
+ * the pack-instances step into a single shader.
21
+ *
22
+ * The compute shader's pack-instances step reads `textureIndex[i]`, looks
23
+ * up the matching frame UV, and writes a 40-byte interleaved record into
24
+ * the instance output buffer (`STORAGE | VERTEX`). The renderer binds that
25
+ * buffer directly as instanced vertex source — no readback.
26
+ */
27
+ const workgroupSize = 64;
28
+ const instanceBytes = 40; // 5 × f32 + 1 × u32 + 4 × f32 (uvMin.xy, uvMax.xy)
29
+ class ParticleGpuState {
30
+ device;
31
+ capacity;
32
+ /** GPU buffer holding interleaved per-instance vertex data, written by compute, read as VERTEX by the renderer. */
33
+ instanceBuffer;
34
+ _positions;
35
+ _velocities;
36
+ _scales;
37
+ _rotInfo;
38
+ _timing;
39
+ _color;
40
+ _textureIndex;
41
+ _simUniformBuffer;
42
+ _simUniformData = new ArrayBuffer(16);
43
+ _simUniformView;
44
+ _moduleUniformBuffer;
45
+ _moduleUniformData;
46
+ _moduleUniformView;
47
+ _moduleSlots;
48
+ _framesUniformBuffer;
49
+ _framesUniformData;
50
+ _framesUniformView;
51
+ _frameCount;
52
+ _moduleTextures = new Map();
53
+ _samplerFiltering;
54
+ _samplerNonFiltering;
55
+ _pipeline;
56
+ _bindGroup0;
57
+ _bindGroup1;
58
+ constructor(device, capacity, modules, frames, texture) {
59
+ this.device = device;
60
+ this.capacity = capacity;
61
+ for (const m of modules) {
62
+ if (!m.wgsl) {
63
+ throw new Error(`ParticleGpuState: module ${m.constructor.name} has no wgsl() — `
64
+ + 'all registered UpdateModules must be GPU-eligible.');
65
+ }
66
+ }
67
+ // Module uniform layout.
68
+ const slots = [];
69
+ let uniformOffset = 0;
70
+ for (const m of modules) {
71
+ const c = m.wgsl();
72
+ const fields = c.uniforms ?? [];
73
+ const size = wgslUniformByteSize(fields);
74
+ uniformOffset = Math.ceil(uniformOffset / 16) * 16;
75
+ slots.push({
76
+ module: m,
77
+ contribution: c,
78
+ uniformByteOffset: uniformOffset,
79
+ uniformByteSize: size,
80
+ });
81
+ uniformOffset += size;
82
+ }
83
+ const totalUniformBytes = Math.max(16, Math.ceil(uniformOffset / 16) * 16);
84
+ this._moduleSlots = slots;
85
+ if (uniformOffset > 0) {
86
+ this._moduleUniformData = new ArrayBuffer(totalUniformBytes);
87
+ this._moduleUniformView = new DataView(this._moduleUniformData);
88
+ this._moduleUniformBuffer = device.createBuffer({
89
+ label: 'particle-module-uniforms',
90
+ size: totalUniformBytes,
91
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
92
+ });
93
+ }
94
+ else {
95
+ this._moduleUniformData = null;
96
+ this._moduleUniformView = null;
97
+ this._moduleUniformBuffer = null;
98
+ }
99
+ // Frames uniform buffer.
100
+ this._frameCount = Math.max(1, frames.length);
101
+ this._framesUniformData = new ArrayBuffer(this._frameCount * 16);
102
+ this._framesUniformView = new Float32Array(this._framesUniformData);
103
+ this._framesUniformBuffer = device.createBuffer({
104
+ label: 'particle-frames-uniforms',
105
+ size: this._framesUniformData.byteLength,
106
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
107
+ });
108
+ this._writeFrames(frames, texture);
109
+ const vec2Bytes = capacity * 8;
110
+ const u32Bytes = capacity * 4;
111
+ this._positions = device.createBuffer({ label: 'particle-positions', size: vec2Bytes, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST });
112
+ this._velocities = device.createBuffer({ label: 'particle-velocities', size: vec2Bytes, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST });
113
+ this._scales = device.createBuffer({ label: 'particle-scales', size: vec2Bytes, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST });
114
+ this._rotInfo = device.createBuffer({ label: 'particle-rotInfo', size: vec2Bytes, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST });
115
+ this._timing = device.createBuffer({ label: 'particle-timing', size: vec2Bytes, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST });
116
+ this._color = device.createBuffer({ label: 'particle-color', size: u32Bytes, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST });
117
+ this._textureIndex = device.createBuffer({ label: 'particle-textureIndex', size: u32Bytes, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST });
118
+ this.instanceBuffer = device.createBuffer({
119
+ label: 'particle-instance-output',
120
+ size: capacity * instanceBytes,
121
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
122
+ });
123
+ this._simUniformView = new DataView(this._simUniformData);
124
+ this._simUniformBuffer = device.createBuffer({
125
+ label: 'particle-sim-uniforms',
126
+ size: 16,
127
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
128
+ });
129
+ // r32float textures aren't filterable in core WebGPU (would require
130
+ // the optional `float32-filterable` feature). Use `nearest` for
131
+ // r32float curve LUTs (256 taps is fine without interpolation) and
132
+ // `linear` for rgba8unorm gradients which support filtering natively.
133
+ this._samplerFiltering = device.createSampler({
134
+ label: 'particle-lookup-sampler-filtering',
135
+ minFilter: 'linear',
136
+ magFilter: 'linear',
137
+ addressModeU: 'clamp-to-edge',
138
+ });
139
+ this._samplerNonFiltering = device.createSampler({
140
+ label: 'particle-lookup-sampler-non-filtering',
141
+ minFilter: 'nearest',
142
+ magFilter: 'nearest',
143
+ addressModeU: 'clamp-to-edge',
144
+ });
145
+ // Allocate textures for modules that need them.
146
+ for (const slot of slots) {
147
+ const c = slot.contribution;
148
+ if (!c.textures)
149
+ continue;
150
+ for (const t of c.textures) {
151
+ const tex = device.createTexture({
152
+ label: `particle-tex-${c.key}-${t.name}`,
153
+ size: { width: 256, height: 1, depthOrArrayLayers: 1 },
154
+ format: t.format,
155
+ usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
156
+ dimension: '1d',
157
+ });
158
+ this._moduleTextures.set(`${c.key}_${t.name}`, tex);
159
+ }
160
+ }
161
+ const wgsl = this._buildShader(slots);
162
+ const bindGroup0Layout = this._buildBindGroup0Layout(slots);
163
+ const bindGroup1Layout = device.createBindGroupLayout({
164
+ label: 'particle-soa-bgl',
165
+ entries: [
166
+ { binding: 0, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'storage' } },
167
+ { binding: 1, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'storage' } },
168
+ { binding: 2, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'storage' } },
169
+ { binding: 3, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'storage' } },
170
+ { binding: 4, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'storage' } },
171
+ { binding: 5, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'storage' } },
172
+ { binding: 6, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'read-only-storage' } }, // textureIndex (matches WGSL `var<storage, read>`)
173
+ { binding: 7, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'storage' } },
174
+ ],
175
+ });
176
+ const pipelineLayout = device.createPipelineLayout({
177
+ label: 'particle-compute-layout',
178
+ bindGroupLayouts: [bindGroup0Layout, bindGroup1Layout],
179
+ });
180
+ const shaderModule = device.createShaderModule({
181
+ label: 'particle-compute-shader',
182
+ code: wgsl,
183
+ });
184
+ this._pipeline = device.createComputePipeline({
185
+ label: 'particle-compute-pipeline',
186
+ layout: pipelineLayout,
187
+ compute: {
188
+ module: shaderModule,
189
+ entryPoint: 'main',
190
+ },
191
+ });
192
+ this._bindGroup0 = this._buildBindGroup0(bindGroup0Layout, slots);
193
+ this._bindGroup1 = device.createBindGroup({
194
+ label: 'particle-soa-bg',
195
+ layout: bindGroup1Layout,
196
+ entries: [
197
+ { binding: 0, resource: { buffer: this._positions } },
198
+ { binding: 1, resource: { buffer: this._velocities } },
199
+ { binding: 2, resource: { buffer: this._scales } },
200
+ { binding: 3, resource: { buffer: this._rotInfo } },
201
+ { binding: 4, resource: { buffer: this._timing } },
202
+ { binding: 5, resource: { buffer: this._color } },
203
+ { binding: 6, resource: { buffer: this._textureIndex } },
204
+ { binding: 7, resource: { buffer: this.instanceBuffer } },
205
+ ],
206
+ });
207
+ // Modules upload their lookup textures.
208
+ for (const slot of slots) {
209
+ if (!slot.module.uploadTextures)
210
+ continue;
211
+ const moduleTextures = new Map();
212
+ for (const t of slot.contribution.textures ?? []) {
213
+ const tex = this._moduleTextures.get(`${slot.contribution.key}_${t.name}`);
214
+ if (tex !== undefined) {
215
+ moduleTextures.set(t.name, tex);
216
+ }
217
+ }
218
+ slot.module.uploadTextures(device, moduleTextures);
219
+ }
220
+ }
221
+ dispatch(system, dt) {
222
+ const liveCount = system.liveCount;
223
+ if (liveCount <= 0) {
224
+ return;
225
+ }
226
+ this._writeSimUniforms(dt, liveCount);
227
+ this._writeModuleUniforms(dt);
228
+ const encoder = this.device.createCommandEncoder({ label: 'particle-compute' });
229
+ const pass = encoder.beginComputePass({ label: 'particle-compute-pass' });
230
+ pass.setPipeline(this._pipeline);
231
+ pass.setBindGroup(0, this._bindGroup0);
232
+ pass.setBindGroup(1, this._bindGroup1);
233
+ pass.dispatchWorkgroups(Math.ceil(liveCount / workgroupSize));
234
+ pass.end();
235
+ this.device.queue.submit([encoder.finish()]);
236
+ }
237
+ destroy() {
238
+ this._positions.destroy();
239
+ this._velocities.destroy();
240
+ this._scales.destroy();
241
+ this._rotInfo.destroy();
242
+ this._timing.destroy();
243
+ this._color.destroy();
244
+ this._textureIndex.destroy();
245
+ this.instanceBuffer.destroy();
246
+ this._simUniformBuffer.destroy();
247
+ this._framesUniformBuffer.destroy();
248
+ this._moduleUniformBuffer?.destroy();
249
+ for (const tex of this._moduleTextures.values()) {
250
+ tex.destroy();
251
+ }
252
+ this._moduleTextures.clear();
253
+ }
254
+ _writeFrames(frames, texture) {
255
+ const view = this._framesUniformView;
256
+ const w = texture.width;
257
+ const h = texture.height;
258
+ const flipY = texture.flipY;
259
+ if (frames.length === 0) {
260
+ // Single-frame fallback — full texture.
261
+ view[0] = 0;
262
+ view[1] = flipY ? 1 : 0;
263
+ view[2] = 1;
264
+ view[3] = flipY ? 0 : 1;
265
+ }
266
+ else {
267
+ for (let i = 0; i < frames.length; i++) {
268
+ const f = frames[i];
269
+ const o = i * 4;
270
+ const minU = f.left / w;
271
+ const maxU = f.right / w;
272
+ const topV = f.top / h;
273
+ const bottomV = f.bottom / h;
274
+ view[o + 0] = minU;
275
+ view[o + 1] = flipY ? bottomV : topV;
276
+ view[o + 2] = maxU;
277
+ view[o + 3] = flipY ? topV : bottomV;
278
+ }
279
+ }
280
+ this.device.queue.writeBuffer(this._framesUniformBuffer, 0, this._framesUniformData);
281
+ }
282
+ /**
283
+ * Push the listed CPU SoA slots to the GPU. Called by `ParticleSystem`
284
+ * with newly-spawned slots and just-expired slots (lifetime sentinel).
285
+ * Slots not in the dirty set are left alone — GPU keeps the integrated
286
+ * state from previous compute dispatches.
287
+ *
288
+ * Each dirty slot triggers 7 small `queue.writeBuffer` calls (one per
289
+ * SoA channel). For typical spawn rates (≤200/s) this is negligible
290
+ * (≤1400 calls/s); contiguous-range batching is a future optimisation.
291
+ */
292
+ uploadDirty(system, slots) {
293
+ const queue = this.device.queue;
294
+ const scratch2 = this._dirtyScratchVec2;
295
+ const scratch1 = this._dirtyScratchU32;
296
+ for (const slot of slots) {
297
+ const byteOffset2 = slot * 8;
298
+ const byteOffset1 = slot * 4;
299
+ scratch2[0] = system.posX[slot];
300
+ scratch2[1] = system.posY[slot];
301
+ queue.writeBuffer(this._positions, byteOffset2, scratch2.buffer, 0, 8);
302
+ scratch2[0] = system.velX[slot];
303
+ scratch2[1] = system.velY[slot];
304
+ queue.writeBuffer(this._velocities, byteOffset2, scratch2.buffer, 0, 8);
305
+ scratch2[0] = system.scaleX[slot];
306
+ scratch2[1] = system.scaleY[slot];
307
+ queue.writeBuffer(this._scales, byteOffset2, scratch2.buffer, 0, 8);
308
+ scratch2[0] = system.rotations[slot];
309
+ scratch2[1] = system.rotationSpeeds[slot];
310
+ queue.writeBuffer(this._rotInfo, byteOffset2, scratch2.buffer, 0, 8);
311
+ scratch2[0] = system.elapsed[slot];
312
+ scratch2[1] = system.lifetime[slot];
313
+ queue.writeBuffer(this._timing, byteOffset2, scratch2.buffer, 0, 8);
314
+ scratch1[0] = system.color[slot];
315
+ queue.writeBuffer(this._color, byteOffset1, scratch1.buffer, 0, 4);
316
+ scratch1[0] = system.textureIndex[slot];
317
+ queue.writeBuffer(this._textureIndex, byteOffset1, scratch1.buffer, 0, 4);
318
+ }
319
+ }
320
+ _dirtyScratchVec2 = new Float32Array(2);
321
+ _dirtyScratchU32 = new Uint32Array(1);
322
+ _writeSimUniforms(dt, liveCount) {
323
+ this._simUniformView.setFloat32(0, dt, true);
324
+ this._simUniformView.setUint32(4, liveCount, true);
325
+ this.device.queue.writeBuffer(this._simUniformBuffer, 0, this._simUniformData);
326
+ }
327
+ _writeModuleUniforms(dt) {
328
+ if (this._moduleUniformView === null || this._moduleUniformBuffer === null || this._moduleUniformData === null) {
329
+ return;
330
+ }
331
+ for (const slot of this._moduleSlots) {
332
+ slot.module.writeUniforms?.(this._moduleUniformView, slot.uniformByteOffset, dt);
333
+ }
334
+ this.device.queue.writeBuffer(this._moduleUniformBuffer, 0, this._moduleUniformData);
335
+ }
336
+ _buildBindGroup0Layout(slots) {
337
+ const entries = [
338
+ { binding: 0, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'uniform' } },
339
+ { binding: 1, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'uniform' } },
340
+ ];
341
+ if (this._moduleUniformBuffer !== null) {
342
+ entries.push({ binding: 2, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'uniform' } });
343
+ }
344
+ let textureBindingIdx = this._moduleUniformBuffer !== null ? 3 : 2;
345
+ for (const slot of slots) {
346
+ for (const t of slot.contribution.textures ?? []) {
347
+ const filterable = t.format !== 'r32float';
348
+ entries.push({
349
+ binding: textureBindingIdx++,
350
+ visibility: GPUShaderStage.COMPUTE,
351
+ texture: {
352
+ viewDimension: '1d',
353
+ sampleType: filterable ? 'float' : 'unfilterable-float',
354
+ },
355
+ });
356
+ entries.push({
357
+ binding: textureBindingIdx++,
358
+ visibility: GPUShaderStage.COMPUTE,
359
+ sampler: { type: filterable ? 'filtering' : 'non-filtering' },
360
+ });
361
+ }
362
+ }
363
+ return this.device.createBindGroupLayout({ label: 'particle-uniforms-bgl', entries });
364
+ }
365
+ _buildBindGroup0(layout, slots) {
366
+ const entries = [
367
+ { binding: 0, resource: { buffer: this._simUniformBuffer } },
368
+ { binding: 1, resource: { buffer: this._framesUniformBuffer } },
369
+ ];
370
+ if (this._moduleUniformBuffer !== null) {
371
+ entries.push({ binding: 2, resource: { buffer: this._moduleUniformBuffer } });
372
+ }
373
+ let textureBindingIdx = this._moduleUniformBuffer !== null ? 3 : 2;
374
+ for (const slot of slots) {
375
+ for (const t of slot.contribution.textures ?? []) {
376
+ const tex = this._moduleTextures.get(`${slot.contribution.key}_${t.name}`);
377
+ const filterable = t.format !== 'r32float';
378
+ const sampler = filterable ? this._samplerFiltering : this._samplerNonFiltering;
379
+ entries.push({ binding: textureBindingIdx++, resource: tex.createView({ dimension: '1d' }) });
380
+ entries.push({ binding: textureBindingIdx++, resource: sampler });
381
+ }
382
+ }
383
+ return this.device.createBindGroup({ label: 'particle-uniforms-bg', layout, entries });
384
+ }
385
+ _buildShader(slots) {
386
+ const sections = [];
387
+ sections.push(`
388
+ struct SimUniforms {
389
+ dt: f32,
390
+ liveCount: u32,
391
+ _pad0: u32,
392
+ _pad1: u32,
393
+ }
394
+
395
+ struct FrameUniforms {
396
+ frames: array<vec4<f32>, ${this._frameCount}>,
397
+ }
398
+
399
+ @group(0) @binding(0) var<uniform> sim: SimUniforms;
400
+ @group(0) @binding(1) var<uniform> frameUv: FrameUniforms;
401
+ `);
402
+ const moduleStructFields = [];
403
+ for (const slot of slots) {
404
+ const c = slot.contribution;
405
+ const fields = c.uniforms ?? [];
406
+ if (fields.length === 0) {
407
+ continue;
408
+ }
409
+ sections.push(this._renderModuleStruct(c.key, fields));
410
+ moduleStructFields.push(`u_${c.key}: ${c.key}Uniforms,`);
411
+ }
412
+ if (moduleStructFields.length > 0) {
413
+ sections.push(`
414
+ struct ModuleUniforms {
415
+ ${moduleStructFields.map((s) => ` ${s}`).join('\n')}
416
+ }
417
+
418
+ @group(0) @binding(2) var<uniform> modules: ModuleUniforms;
419
+ `);
420
+ }
421
+ let textureBindingIdx = moduleStructFields.length > 0 ? 3 : 2;
422
+ for (const slot of slots) {
423
+ for (const t of slot.contribution.textures ?? []) {
424
+ sections.push(`
425
+ @group(0) @binding(${textureBindingIdx++}) var u_${slot.contribution.key}_${t.name}: texture_1d<f32>;
426
+ @group(0) @binding(${textureBindingIdx++}) var u_${slot.contribution.key}_${t.name}_sampler: sampler;
427
+ `);
428
+ }
429
+ }
430
+ sections.push(`
431
+ @group(1) @binding(0) var<storage, read_write> positions: array<vec2<f32>>;
432
+ @group(1) @binding(1) var<storage, read_write> velocities: array<vec2<f32>>;
433
+ @group(1) @binding(2) var<storage, read_write> scales: array<vec2<f32>>;
434
+ @group(1) @binding(3) var<storage, read_write> rotInfo: array<vec2<f32>>;
435
+ @group(1) @binding(4) var<storage, read_write> timing: array<vec2<f32>>;
436
+ @group(1) @binding(5) var<storage, read_write> color: array<u32>;
437
+ @group(1) @binding(6) var<storage, read> textureIndex: array<u32>;
438
+ @group(1) @binding(7) var<storage, read_write> instanceOutput: array<u32>;
439
+ `);
440
+ // Module preludes (helper functions/constants). Concatenated in
441
+ // registration order; modules sharing the same key are emitted only
442
+ // once (the contribution body strings are still inlined per-instance,
443
+ // but the prelude function definitions can't be duplicated).
444
+ const seenPreludeKeys = new Set();
445
+ for (const slot of slots) {
446
+ const prelude = slot.contribution.prelude;
447
+ if (prelude === undefined || prelude.trim() === '')
448
+ continue;
449
+ if (seenPreludeKeys.has(slot.contribution.key))
450
+ continue;
451
+ seenPreludeKeys.add(slot.contribution.key);
452
+ sections.push(prelude);
453
+ }
454
+ const moduleBodies = slots.map((s) => s.contribution.body).join('\n');
455
+ const frameCountConst = this._frameCount;
456
+ sections.push(`
457
+ @compute @workgroup_size(${workgroupSize})
458
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
459
+ let idx = gid.x;
460
+ if (idx >= sim.liveCount) { return; }
461
+
462
+ let dt = sim.dt;
463
+
464
+ // Skip dead particles (lifetime sentinel < 0). Write zero-scale instance
465
+ // so the renderer doesn't accidentally draw them.
466
+ if (timing[idx].y < 0.0) {
467
+ let outBaseDead = idx * 10u;
468
+ for (var k: u32 = 0u; k < 10u; k++) { instanceOutput[outBaseDead + k] = 0u; }
469
+ return;
470
+ }
471
+
472
+ // Integration.
473
+ positions[idx] = positions[idx] + velocities[idx] * dt;
474
+ rotInfo[idx].x = rotInfo[idx].x + rotInfo[idx].y * dt;
475
+ timing[idx].x = timing[idx].x + dt;
476
+
477
+ // Module bodies (in registration order).
478
+ ${moduleBodies}
479
+
480
+ // Resolve frame UVs.
481
+ let frameIndex = min(textureIndex[idx], ${frameCountConst}u - 1u);
482
+ let frameUvBounds = frameUv.frames[frameIndex];
483
+
484
+ // Pack interleaved instance data (10 u32s per particle):
485
+ // x, y, scaleX, scaleY, rotation (f32×5) + color (u32) + uvMin.xy (f32×2) + uvMax.xy (f32×2)
486
+ let outBase = idx * 10u;
487
+ instanceOutput[outBase + 0u] = bitcast<u32>(positions[idx].x);
488
+ instanceOutput[outBase + 1u] = bitcast<u32>(positions[idx].y);
489
+ instanceOutput[outBase + 2u] = bitcast<u32>(scales[idx].x);
490
+ instanceOutput[outBase + 3u] = bitcast<u32>(scales[idx].y);
491
+ instanceOutput[outBase + 4u] = bitcast<u32>(rotInfo[idx].x);
492
+ instanceOutput[outBase + 5u] = color[idx];
493
+ instanceOutput[outBase + 6u] = bitcast<u32>(frameUvBounds.x);
494
+ instanceOutput[outBase + 7u] = bitcast<u32>(frameUvBounds.y);
495
+ instanceOutput[outBase + 8u] = bitcast<u32>(frameUvBounds.z);
496
+ instanceOutput[outBase + 9u] = bitcast<u32>(frameUvBounds.w);
497
+ }
498
+ `);
499
+ return sections.join('\n\n');
500
+ }
501
+ _renderModuleStruct(key, fields) {
502
+ const lines = fields.map((f) => ` ${f.name}: ${f.type},`).join('\n');
503
+ return `struct ${key}Uniforms {\n${lines}\n}`;
504
+ }
505
+ }
506
+
507
+ export { ParticleGpuState };
508
+ //# sourceMappingURL=ParticleGpuState.js.map