@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.
- package/CHANGELOG.md +737 -0
- package/dist/esm/core/Application.d.ts +3 -1
- package/dist/esm/core/Application.js +7 -6
- package/dist/esm/core/Application.js.map +1 -1
- package/dist/esm/core/Scene.d.ts +30 -0
- package/dist/esm/core/Scene.js +56 -0
- package/dist/esm/core/Scene.js.map +1 -1
- package/dist/esm/core/SceneManager.js +2 -2
- package/dist/esm/core/SceneManager.js.map +1 -1
- package/dist/esm/debug/DebugOverlay.js +2 -2
- package/dist/esm/debug/DebugOverlay.js.map +1 -1
- package/dist/esm/debug/PointerStackLayer.js +1 -1
- package/dist/esm/debug/PointerStackLayer.js.map +1 -1
- package/dist/esm/index.js +32 -10
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/input/ArcadeStickGamepadMapping.js +18 -19
- package/dist/esm/input/ArcadeStickGamepadMapping.js.map +1 -1
- package/dist/esm/input/Gamepad.d.ts +164 -62
- package/dist/esm/input/Gamepad.js +290 -134
- package/dist/esm/input/Gamepad.js.map +1 -1
- package/dist/esm/input/GamepadAxis.d.ts +120 -0
- package/dist/esm/input/GamepadAxis.js +106 -0
- package/dist/esm/input/GamepadAxis.js.map +1 -0
- package/dist/esm/input/GamepadButton.d.ts +110 -0
- package/dist/esm/input/GamepadButton.js +99 -0
- package/dist/esm/input/GamepadButton.js.map +1 -0
- package/dist/esm/input/GamepadDefinitions.js +4 -0
- package/dist/esm/input/GamepadDefinitions.js.map +1 -1
- package/dist/esm/input/GamepadMapping.d.ts +28 -24
- package/dist/esm/input/GamepadMapping.js +33 -16
- package/dist/esm/input/GamepadMapping.js.map +1 -1
- package/dist/esm/input/GamepadPromptLayouts.d.ts +10 -8
- package/dist/esm/input/GamepadPromptLayouts.js +21 -20
- package/dist/esm/input/GamepadPromptLayouts.js.map +1 -1
- package/dist/esm/input/GenericDualAnalogGamepadMapping.d.ts +6 -3
- package/dist/esm/input/GenericDualAnalogGamepadMapping.js +55 -46
- package/dist/esm/input/GenericDualAnalogGamepadMapping.js.map +1 -1
- package/dist/esm/input/InputBinding.d.ts +74 -0
- package/dist/esm/input/InputBinding.js +100 -0
- package/dist/esm/input/InputBinding.js.map +1 -0
- package/dist/esm/input/InputManager.d.ts +79 -33
- package/dist/esm/input/InputManager.js +229 -104
- package/dist/esm/input/InputManager.js.map +1 -1
- package/dist/esm/input/InteractionManager.d.ts +1 -1
- package/dist/esm/input/InteractionManager.js +13 -13
- package/dist/esm/input/InteractionManager.js.map +1 -1
- package/dist/esm/input/JoyConLeftGamepadMapping.d.ts +14 -9
- package/dist/esm/input/JoyConLeftGamepadMapping.js +39 -9
- package/dist/esm/input/JoyConLeftGamepadMapping.js.map +1 -1
- package/dist/esm/input/JoyConRightGamepadMapping.d.ts +14 -9
- package/dist/esm/input/JoyConRightGamepadMapping.js +35 -9
- package/dist/esm/input/JoyConRightGamepadMapping.js.map +1 -1
- package/dist/esm/input/Pointer.d.ts +84 -71
- package/dist/esm/input/Pointer.js +71 -71
- package/dist/esm/input/Pointer.js.map +1 -1
- package/dist/esm/input/SteamDeckGamepadMapping.d.ts +18 -0
- package/dist/esm/input/SteamDeckGamepadMapping.js +76 -0
- package/dist/esm/input/SteamDeckGamepadMapping.js.map +1 -0
- package/dist/esm/input/index.d.ts +7 -4
- package/dist/esm/input/types.d.ts +0 -76
- package/dist/esm/input/types.js +1 -80
- package/dist/esm/input/types.js.map +1 -1
- package/dist/esm/particles/ParticleSystem.d.ts +180 -83
- package/dist/esm/particles/ParticleSystem.js +446 -133
- package/dist/esm/particles/ParticleSystem.js.map +1 -1
- package/dist/esm/particles/distributions/BoxArea.d.ts +17 -0
- package/dist/esm/particles/distributions/BoxArea.js +48 -0
- package/dist/esm/particles/distributions/BoxArea.js.map +1 -0
- package/dist/esm/particles/distributions/CircleArea.d.ts +19 -0
- package/dist/esm/particles/distributions/CircleArea.js +33 -0
- package/dist/esm/particles/distributions/CircleArea.js.map +1 -0
- package/dist/esm/particles/distributions/ConeDirection.d.ts +28 -0
- package/dist/esm/particles/distributions/ConeDirection.js +44 -0
- package/dist/esm/particles/distributions/ConeDirection.js.map +1 -0
- package/dist/esm/particles/distributions/Constant.d.ts +17 -0
- package/dist/esm/particles/distributions/Constant.js +35 -0
- package/dist/esm/particles/distributions/Constant.js.map +1 -0
- package/dist/esm/particles/distributions/Curve.d.ts +30 -0
- package/dist/esm/particles/distributions/Curve.js +53 -0
- package/dist/esm/particles/distributions/Curve.js.map +1 -0
- package/dist/esm/particles/distributions/Distribution.d.ts +45 -0
- package/dist/esm/particles/distributions/Gradient.d.ts +40 -0
- package/dist/esm/particles/distributions/Gradient.js +72 -0
- package/dist/esm/particles/distributions/Gradient.js.map +1 -0
- package/dist/esm/particles/distributions/LineSegment.d.ts +15 -0
- package/dist/esm/particles/distributions/LineSegment.js +27 -0
- package/dist/esm/particles/distributions/LineSegment.js.map +1 -0
- package/dist/esm/particles/distributions/Range.d.ts +12 -0
- package/dist/esm/particles/distributions/Range.js +19 -0
- package/dist/esm/particles/distributions/Range.js.map +1 -0
- package/dist/esm/particles/distributions/VectorRange.d.ts +20 -0
- package/dist/esm/particles/distributions/VectorRange.js +31 -0
- package/dist/esm/particles/distributions/VectorRange.js.map +1 -0
- package/dist/esm/particles/distributions/index.d.ts +12 -0
- package/dist/esm/particles/gpu/ParticleGpuState.d.ts +57 -0
- package/dist/esm/particles/gpu/ParticleGpuState.js +508 -0
- package/dist/esm/particles/gpu/ParticleGpuState.js.map +1 -0
- package/dist/esm/particles/index.d.ts +2 -10
- package/dist/esm/particles/modules/AlphaFadeOverLifetime.d.ts +24 -0
- package/dist/esm/particles/modules/AlphaFadeOverLifetime.js +60 -0
- package/dist/esm/particles/modules/AlphaFadeOverLifetime.js.map +1 -0
- package/dist/esm/particles/modules/ApplyForce.d.ts +20 -0
- package/dist/esm/particles/modules/ApplyForce.js +48 -0
- package/dist/esm/particles/modules/ApplyForce.js.map +1 -0
- package/dist/esm/particles/modules/AttractToPoint.d.ts +27 -0
- package/dist/esm/particles/modules/AttractToPoint.js +73 -0
- package/dist/esm/particles/modules/AttractToPoint.js.map +1 -0
- package/dist/esm/particles/modules/BurstSpawn.d.ts +53 -0
- package/dist/esm/particles/modules/BurstSpawn.js +94 -0
- package/dist/esm/particles/modules/BurstSpawn.js.map +1 -0
- package/dist/esm/particles/modules/ColorOverLifetime.d.ts +22 -0
- package/dist/esm/particles/modules/ColorOverLifetime.js +65 -0
- package/dist/esm/particles/modules/ColorOverLifetime.js.map +1 -0
- package/dist/esm/particles/modules/ColorOverSpeed.d.ts +27 -0
- package/dist/esm/particles/modules/ColorOverSpeed.js +86 -0
- package/dist/esm/particles/modules/ColorOverSpeed.js.map +1 -0
- package/dist/esm/particles/modules/DeathModule.d.ts +24 -0
- package/dist/esm/particles/modules/DeathModule.js +25 -0
- package/dist/esm/particles/modules/DeathModule.js.map +1 -0
- package/dist/esm/particles/modules/Drag.d.ts +20 -0
- package/dist/esm/particles/modules/Drag.js +45 -0
- package/dist/esm/particles/modules/Drag.js.map +1 -0
- package/dist/esm/particles/modules/OrbitalForce.d.ts +28 -0
- package/dist/esm/particles/modules/OrbitalForce.js +65 -0
- package/dist/esm/particles/modules/OrbitalForce.js.map +1 -0
- package/dist/esm/particles/modules/RateSpawn.d.ts +41 -0
- package/dist/esm/particles/modules/RateSpawn.js +76 -0
- package/dist/esm/particles/modules/RateSpawn.js.map +1 -0
- package/dist/esm/particles/modules/RepelFromPoint.d.ts +24 -0
- package/dist/esm/particles/modules/RepelFromPoint.js +76 -0
- package/dist/esm/particles/modules/RepelFromPoint.js.map +1 -0
- package/dist/esm/particles/modules/RotateOverLifetime.d.ts +20 -0
- package/dist/esm/particles/modules/RotateOverLifetime.js +43 -0
- package/dist/esm/particles/modules/RotateOverLifetime.js.map +1 -0
- package/dist/esm/particles/modules/ScaleOverLifetime.d.ts +26 -0
- package/dist/esm/particles/modules/ScaleOverLifetime.js +59 -0
- package/dist/esm/particles/modules/ScaleOverLifetime.js.map +1 -0
- package/dist/esm/particles/modules/SpawnModule.d.ts +30 -0
- package/dist/esm/particles/modules/SpawnModule.js +31 -0
- package/dist/esm/particles/modules/SpawnModule.js.map +1 -0
- package/dist/esm/particles/modules/SpawnOnDeath.d.ts +24 -0
- package/dist/esm/particles/modules/SpawnOnDeath.js +47 -0
- package/dist/esm/particles/modules/SpawnOnDeath.js.map +1 -0
- package/dist/esm/particles/modules/Turbulence.d.ts +30 -0
- package/dist/esm/particles/modules/Turbulence.js +122 -0
- package/dist/esm/particles/modules/Turbulence.js.map +1 -0
- package/dist/esm/particles/modules/UpdateModule.d.ts +95 -0
- package/dist/esm/particles/modules/UpdateModule.js +66 -0
- package/dist/esm/particles/modules/UpdateModule.js.map +1 -0
- package/dist/esm/particles/modules/VelocityOverLifetime.d.ts +30 -0
- package/dist/esm/particles/modules/VelocityOverLifetime.js +84 -0
- package/dist/esm/particles/modules/VelocityOverLifetime.js.map +1 -0
- package/dist/esm/particles/modules/WgslContribution.d.ts +81 -0
- package/dist/esm/particles/modules/WgslContribution.js +34 -0
- package/dist/esm/particles/modules/WgslContribution.js.map +1 -0
- package/dist/esm/particles/modules/index.d.ts +22 -0
- package/dist/esm/rendering/webgl2/WebGl2ParticleRenderer.d.ts +9 -14
- package/dist/esm/rendering/webgl2/WebGl2ParticleRenderer.js +90 -61
- package/dist/esm/rendering/webgl2/WebGl2ParticleRenderer.js.map +1 -1
- package/dist/esm/rendering/webgl2/glsl/particle.vert.js +1 -1
- package/dist/esm/rendering/webgpu/WebGpuParticleRenderer.d.ts +9 -0
- package/dist/esm/rendering/webgpu/WebGpuParticleRenderer.js +107 -23
- package/dist/esm/rendering/webgpu/WebGpuParticleRenderer.js.map +1 -1
- package/dist/esm/rendering/webgpu/compute/WebGpuComputePipeline.d.ts +52 -0
- package/dist/esm/rendering/webgpu/compute/WebGpuStorageBuffer.d.ts +29 -0
- package/dist/esm/rendering/webgpu/compute/index.d.ts +3 -0
- package/dist/esm/resources/CacheFirstStrategy.d.ts +7 -4
- package/dist/esm/resources/CacheFirstStrategy.js +11 -8
- package/dist/esm/resources/CacheFirstStrategy.js.map +1 -1
- package/dist/esm/resources/CacheStrategy.d.ts +14 -6
- package/dist/esm/resources/Loader.d.ts +8 -3
- package/dist/esm/resources/Loader.js +19 -37
- package/dist/esm/resources/Loader.js.map +1 -1
- package/dist/esm/resources/NetworkOnlyStrategy.d.ts +3 -0
- package/dist/esm/resources/NetworkOnlyStrategy.js +8 -3
- package/dist/esm/resources/NetworkOnlyStrategy.js.map +1 -1
- package/dist/esm/resources/factories/ImageFactory.d.ts +2 -2
- package/dist/esm/resources/factories/ImageFactory.js.map +1 -1
- package/dist/esm/resources/factories/TextureFactory.d.ts +2 -2
- package/dist/esm/resources/factories/TextureFactory.js.map +1 -1
- package/dist/esm/resources/factories/VttFactory.d.ts +3 -3
- package/dist/esm/resources/factories/VttFactory.js +83 -6
- package/dist/esm/resources/factories/VttFactory.js.map +1 -1
- package/dist/exo.esm.js +4028 -1518
- package/dist/exo.esm.js.map +1 -1
- package/package.json +2 -1
- package/dist/esm/input/GamepadChannels.d.ts +0 -47
- package/dist/esm/input/GamepadChannels.js +0 -53
- package/dist/esm/input/GamepadChannels.js.map +0 -1
- package/dist/esm/input/GamepadControl.d.ts +0 -33
- package/dist/esm/input/GamepadControl.js +0 -42
- package/dist/esm/input/GamepadControl.js.map +0 -1
- package/dist/esm/input/Input.d.ts +0 -52
- package/dist/esm/input/Input.js +0 -90
- package/dist/esm/input/Input.js.map +0 -1
- package/dist/esm/particles/Particle.d.ts +0 -77
- package/dist/esm/particles/Particle.js +0 -143
- package/dist/esm/particles/Particle.js.map +0 -1
- package/dist/esm/particles/ParticleProperties.d.ts +0 -29
- package/dist/esm/particles/affectors/ColorAffector.d.ts +0 -30
- package/dist/esm/particles/affectors/ColorAffector.js +0 -55
- package/dist/esm/particles/affectors/ColorAffector.js.map +0 -1
- package/dist/esm/particles/affectors/ForceAffector.d.ts +0 -24
- package/dist/esm/particles/affectors/ForceAffector.js +0 -39
- package/dist/esm/particles/affectors/ForceAffector.js.map +0 -1
- package/dist/esm/particles/affectors/ParticleAffector.d.ts +0 -19
- package/dist/esm/particles/affectors/ScaleAffector.d.ts +0 -23
- package/dist/esm/particles/affectors/ScaleAffector.js +0 -38
- package/dist/esm/particles/affectors/ScaleAffector.js.map +0 -1
- package/dist/esm/particles/affectors/TorqueAffector.d.ts +0 -23
- package/dist/esm/particles/affectors/TorqueAffector.js +0 -37
- package/dist/esm/particles/affectors/TorqueAffector.js.map +0 -1
- package/dist/esm/particles/emitters/ParticleEmitter.d.ts +0 -19
- package/dist/esm/particles/emitters/ParticleOptions.d.ts +0 -62
- package/dist/esm/particles/emitters/ParticleOptions.js +0 -120
- package/dist/esm/particles/emitters/ParticleOptions.js.map +0 -1
- package/dist/esm/particles/emitters/UniversalEmitter.d.ts +0 -40
- package/dist/esm/particles/emitters/UniversalEmitter.js +0 -68
- 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
|