sparcoon 0.9.1 → 0.9.3

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 (36) hide show
  1. package/README.md +176 -76
  2. package/dist/artifact/FXArtifact.d.ts +51 -3
  3. package/dist/behaviorTransformFeedbackLayout.d.ts +24 -0
  4. package/dist/behaviorTransformFeedbackLayout.js +24 -0
  5. package/dist/coreLayout.d.ts +9 -5
  6. package/dist/coreLayout.js +17 -4
  7. package/dist/editor/index.d.ts +1 -0
  8. package/dist/editor/index.js +1 -1
  9. package/dist/effect/FXEffect.d.ts +4 -0
  10. package/dist/effect/FXEffect.js +47 -14
  11. package/dist/effect/FXEffectSpec.d.ts +21 -4
  12. package/dist/emitter/FXApplyRenderArtifactOptions.d.ts +15 -0
  13. package/dist/emitter/FXApplyRenderArtifactOptions.js +1 -0
  14. package/dist/emitter/FXEmitter.d.ts +44 -14
  15. package/dist/emitter/FXEmitter.js +132 -156
  16. package/dist/emitter/FXEmitterDriver.Internal.d.ts +26 -0
  17. package/dist/emitter/FXEmitterDriver.Internal.js +70 -0
  18. package/dist/emitter/FXFromArtifactsOptions.d.ts +12 -1
  19. package/dist/emitter/FXGPUEmitterDriver.Internal.d.ts +1 -0
  20. package/dist/emitter/FXGPUEmitterDriver.Internal.js +137 -0
  21. package/dist/emitter/FXJSEmitterDriver.Internal.d.ts +1 -0
  22. package/dist/emitter/FXJSEmitterDriver.Internal.js +133 -0
  23. package/dist/emitter/FXSimulationHolder.js +7 -5
  24. package/dist/emitter/FXTransformFeedbackSimulationHolder.d.ts +1 -0
  25. package/dist/emitter/FXTransformFeedbackSimulationHolder.js +249 -0
  26. package/dist/index.d.ts +4 -3
  27. package/dist/index.js +3 -2
  28. package/dist/instancedParticle/FXInstancedParticle.js +28 -1
  29. package/dist/instancedParticle/FXTransformFeedbackParticle.d.ts +1 -0
  30. package/dist/instancedParticle/FXTransformFeedbackParticle.js +169 -0
  31. package/dist/miscellaneous/fxMath.d.ts +18 -11
  32. package/dist/miscellaneous/fxMath.js +89 -209
  33. package/dist/render/FXArtifactMaterial.Internal.js +66 -40
  34. package/dist/render/FXArtifactMaterial.js +1 -1
  35. package/dist/render/FXMesh.js +2 -4
  36. package/package.json +11 -3
package/README.md CHANGED
@@ -1,133 +1,233 @@
1
- # Sparcoon
1
+ # <img src="https://raw.githubusercontent.com/jango-git/sparcoon/experimental/assets/logo.svg" width="30" height="30" alt="" align="top" /> Sparcoon
2
2
 
3
- Three.js runtime for VFX authored in the Sparcoon Editor.
3
+ A three.js runtime that plays visual effects authored in the
4
+ [Sparcoon Editor](https://github.com/jango-git/sparcoon-editor). The editor compiles a node graph
5
+ into a plain ES module of precompiled artifacts, and this package executes them: instanced particle
6
+ rendering in one draw call plus a small timeline player. No graph, no compiler and no `eval` in your
7
+ build.
4
8
 
5
- `sparcoon` plays back particle effects designed in the Sparcoon Editor: the
6
- editor compiles a node graph into a plain ES module of precompiled artifacts,
7
- and this thin runtime loads and runs it -- instanced-particle rendering in one
8
- draw call plus a small timeline player. No graph, compiler, or live protocol
9
- ships to your app.
9
+ **[Open the editor](https://jango-git.github.io/sparcoon-editor/)** -
10
+ [Editor source](https://github.com/jango-git/sparcoon-editor) -
11
+ [npm](https://www.npmjs.com/package/sparcoon/v/0.9.3)
10
12
 
11
- > Editor: link coming soon.
13
+ > **Pre-release.** The artifact format is not frozen yet - breaking changes are possible before the
14
+ > first stable release. The package is published under the `experimental` tag: npm's `latest` holds
15
+ > an older release that the current editor does not work with.
12
16
 
13
- ## Features
17
+ ## Who this is for
14
18
 
15
- - Instanced billboard and 3D-mesh particles in a single draw call.
16
- - Per-particle simulation from a precompiled kernel -- no `eval`, no codegen.
17
- - Whole-project playback: an editor module subclasses `FXEffect`.
18
- - One `FXWorld.update` per frame drives every effect.
19
- - Optional depth sorting, shadows, and custom-geometry particles.
20
- - Tree-shakeable ESM with full TypeScript types.
19
+ Someone sent you a `.ts` file exported from the editor and you need to play it in your three.js
20
+ scene. This package is everything you have to add for that.
21
+
22
+ What is worth knowing before you install:
23
+
24
+ - **No code generation.** The exported module is ready GLSL plus ordinary JavaScript functions that
25
+ your bundler compiles like any other source. Neither `eval` nor `new Function` is called anywhere,
26
+ so the effect passes where runtime code generation is forbidden by policy.
27
+ - **No dependencies.** The only peer dependency is `three`.
28
+ - **One draw call per emitter.** Billboards and mesh particles are drawn instanced.
29
+ - **Two GLSL tiers in every artifact.** Standard (WebGL2) and Baseline (WebGL1-compatible); the
30
+ runtime picks by the capabilities of the renderer you pass in.
31
+ - **Optional GPU simulation** through transform feedback, with an automatic fallback to the
32
+ JavaScript kernel.
33
+ - **Tree-shakeable ESM with full types**, unbundled and unminified: your bundler processes the
34
+ package like any other source, exactly the way three.js is consumed.
21
35
 
22
36
  ## Install
23
37
 
24
38
  ```sh
25
- npm install sparcoon three
39
+ npm install sparcoon@experimental three
26
40
  ```
27
41
 
28
- `three` (>= 0.157) is a peer dependency.
42
+ `three` is a peer dependency; the supported range is `>=0.157 <0.180`.
29
43
 
30
44
  ## Quick start
31
45
 
32
- An editor project exports an `FXEffect` subclass plus a typed map of the
33
- textures it needs. Construct it (it joins the default world automatically), add
34
- it to your scene, and call `FXWorld.update(deltaTime)` once per frame.
46
+ A module from the editor exports a named `FXEffect` subclass and a typed interface for its assets.
47
+ The runtime does not load assets: you prepare the textures and geometries and pass them to the
48
+ constructor.
35
49
 
36
50
  ```ts
37
51
  import { TextureLoader } from "three";
38
52
  import { FXWorld } from "sparcoon";
39
- import { VFXDemo0 } from "./effects/VFXDemo_0"; // emitted by the editor
53
+ import { MyEffect, type MyEffectAssets } from "./effects/MyEffect"; // emitted by the editor
40
54
 
41
55
  const loader = new TextureLoader();
42
- const assets = {
43
- heart: loader.load("heart.png"),
44
- fire: loader.load("fire.png"),
56
+ const assets: MyEffectAssets = {
57
+ spark: loader.load("spark.png"),
45
58
  };
46
- const effect = new VFXDemo0(assets);
59
+
60
+ // renderer picks the artifact tier (WebGL2 -> Standard) and enables GPU simulation.
61
+ // camera is needed by emitters that were given a sort interval in the editor.
62
+ const effect = new MyEffect(assets, { renderer, camera });
47
63
 
48
64
  scene.add(effect); // FXEffect is a THREE.Group
49
65
  effect.play();
50
66
 
51
- function frame(deltaTime: number): void {
52
- FXWorld.update(deltaTime); // advances every effect's timeline and its particles
67
+ function frame(deltaSeconds: number): void {
68
+ FXWorld.update(deltaSeconds); // advances the timeline and particles of every effect
53
69
  renderer.render(scene, camera);
54
70
  }
55
71
  ```
56
72
 
57
- The effect is an `Object3D` -- position, rotate, and scale it like any other.
58
- Call `effect.dispose()` to remove it, free its GPU resources, and unsubscribe it
59
- from the world.
73
+ Straight after the constructor the effect sits on frame zero, in the state the artist authored.
74
+ `play()` restarts the timeline from the top; `stop()` halts playback, clears live particles and
75
+ returns the effect to frame zero. The timeline loops, and seamlessly: particles still alive at the
76
+ end of a lap finish their lives in the next one.
77
+
78
+ Without `renderer` the effect takes the Baseline tier unconditionally and runs on the JavaScript
79
+ kernel. That is a working mode if you have no renderer at the point of construction.
60
80
 
61
- ### Live parameters
81
+ `effect.dispose()` unsubscribes the effect from its world, destroys its emitters and frees mesh
82
+ resources. The textures and geometries you passed in are left alone: you own their lifecycle.
62
83
 
63
- Channels the editor marked "live" are driven from your code, not the timeline:
84
+ ## Where the module comes from
85
+
86
+ Effects are built in the [Sparcoon Editor](https://jango-git.github.io/sparcoon-editor/): a
87
+ browser-based node editor with a timeline and a three.js viewport. Each emitter there owns two
88
+ graphs - behavior (per-particle simulation) and render (its material) - and exporting a project puts
89
+ everything into one self-contained TypeScript module. You drop it into your source tree and import
90
+ it, as in the example above.
91
+
92
+ Transform channels and timeline values can be marked by the artist as excluded from the export: the
93
+ timeline does not drive those, your code supplies the values.
94
+
95
+ ## Driving it from code
64
96
 
65
97
  ```ts
66
- effect.setEmitterParam("Hearts", "spawnRate", 64);
67
- effect.setMeshParam("Mesh", "glow", [1, 0.4, 0.1]);
98
+ effect.setEmitterParam("Sparks", "intensity", 0.7);
99
+ effect.setMeshParam("Shockwave", "glow", [1, 0.4, 0.1]);
68
100
 
69
- effect.getEmitter("Hearts"); // the underlying emitter (Object3D)
70
- effect.getMesh("Mesh"); // the underlying THREE.Mesh
101
+ effect.getEmitter("Sparks")?.position.set(0, 1, 0);
102
+ effect.getMesh("Shockwave")?.rotateY(Math.PI);
71
103
  ```
72
104
 
73
- ### Many effects, and separate worlds
105
+ In the module the editor emitted, both setters and both getters are typed against the names the
106
+ project actually declares, so a typo is caught at compile time. An unknown parameter name is always a
107
+ safe no-op: a name drift never corrupts the runtime's data.
74
108
 
75
- Every effect joins the default world on construction, so any number of them are
76
- driven by the same single `FXWorld.update(deltaTime)` -- there is nothing extra
77
- to wire up per effect.
109
+ `getEmitter` returns a live `FXEmitter` (an `Object3D`), which gives you direct control over emission
110
+ outside the timeline:
78
111
 
79
- A world is a tick and clock domain (not a scene graph). Create your own
80
- `FXWorld` when you want an independent clock -- an isolated time scale, or a
81
- group you pause on its own -- and pass it to the effect:
112
+ ```ts
113
+ const sparks = effect.getEmitter("Sparks");
114
+ if (sparks) {
115
+ sparks.burst(64); // one-off release, optionally delayed; returns a handle for stop()
116
+ sparks.play(120); // continuous emission in particles per second; also returns a handle
117
+ sparks.stop(); // with no argument, cancels everything scheduled on this emitter
118
+ sparks.prewarm(2); // simulate 2 seconds forward so playback does not start from an empty screen
119
+
120
+ sparks.sortCamera = camera; // depth sorting; undefined turns it off
121
+ sparks.sortFraction = 0.1; // fraction of frames that re-sort (the default)
122
+ }
123
+ ```
124
+
125
+ An immediate `burst()` spawns from the emitter's pose and parameter values at the moment of the
126
+ call, on the GPU path too - so moving the emitter between two bursts of one frame spawns them in two
127
+ places.
128
+
129
+ For diagnostics there are `particleCount` and `particleCapacity`. A GPU-driven emitter has no exact
130
+ live count, so `particleCount` reports an honest upper bound - the buffer's current capacity.
131
+
132
+ ## Multiple effects and worlds
133
+
134
+ A world is a tick and clock domain, not a scene graph. Every effect subscribes to a world on
135
+ construction, so a single `FXWorld.update(deltaSeconds)` per frame drives them all.
82
136
 
83
137
  ```ts
84
138
  import { FXWorld } from "sparcoon";
85
139
 
86
140
  const slowmo = new FXWorld();
87
- const boss = new VFXDemo0(assets, { world: slowmo });
88
- scene.add(boss); // still added to your scene yourself
141
+ const boss = new MyEffect(assets, { renderer, world: slowmo });
142
+ scene.add(boss); // you still add the object to your scene yourself
89
143
 
90
- function frame(deltaTime: number): void {
91
- FXWorld.update(deltaTime); // the default world
92
- slowmo.update(deltaTime * 0.3); // its own, slower clock
144
+ function frame(deltaSeconds: number): void {
145
+ FXWorld.update(deltaSeconds); // the default world
146
+ slowmo.update(deltaSeconds * 0.3); // its own, slower clock
93
147
  renderer.render(scene, camera);
94
148
  }
95
149
  ```
96
150
 
97
- Within one world every effect shares the clock and one delta per frame, so
98
- `effect.stop()` clears rather than freezes -- to freeze one effect on its own,
99
- give it its own world. `world.dispose()` disposes every effect in it.
151
+ Within one world every effect shares the clock and one delta per frame, so `effect.stop()` clears the
152
+ effect rather than freezing it. To freeze one effect on its own, give it its own world.
153
+ `world.dispose()` disposes every effect in that world.
100
154
 
101
155
  ## How it works
102
156
 
103
- The **Sparcoon Editor** compiles a node graph into a plain ES module exporting
104
- a render artifact (GLSL, uniforms, textures) and a behavior artifact (authored
105
- spawn/update functions). This runtime executes them -- your bundler compiles
106
- the authored functions like any other source, so nothing is evaluated at
107
- runtime. The editor/runtime boundary is a frozen ABI: two core per-particle
108
- buffers, `position` and `lifecycle` (`[age, lifetime]`).
157
+ The editor compiles a graph into artifacts and bakes them into a module together with the project's
158
+ timeline:
159
+
160
+ - **Render artifact** - GLSL, uniforms, texture references.
161
+ - **Behavior artifact** - the authored spawn/update functions, bindings and buffer layout.
162
+ - **GPU kernel** - a fused transform-feedback program, present only when the graph opted into GPU
163
+ simulation and compiled.
164
+
165
+ The runtime executes those artifacts and compiles nothing. The editor/runtime boundary is frozen:
166
+ each particle carries two core buffers, `position` (vec3) and `lifecycle` (vec3 `[age, lifetime,
167
+ id]`), a particle counts as dead once `age >= lifetime`, and everything else lives in ordinary
168
+ `a_fx_<name>` attributes. The package exports the constants of that layout (`FX_AGE`, `FX_LIFETIME`,
169
+ `FX_ID`, `FX_CORE_POSITION` and the rest).
170
+
171
+ The math the generated code calls comes from here too (`fxMix`, `fxSmoothstep`, `fxNoise1` /
172
+ `fxNoise2` / `fxNoise3`, `fxFbm`, `fxFbm3`, `fxFract`, `fxMod`, `fxSampleLut`), along with
173
+ `fxDataTexture` for baked curves and gradients. There is no reason to import them by hand: the effect
174
+ module does it itself.
175
+
176
+ ## Development-build checks
177
+
178
+ Arguments to public methods are sanity-checked (finite numbers, positive durations, integer
179
+ capacities) and throw with the parameter's name when violated. The checks live behind
180
+ `process.env.NODE_ENV !== "production"`, so they drop out of a production build entirely. The access
181
+ to `process` is guarded by a `typeof` check, which keeps the package safe without a bundler - under
182
+ import maps or from a CDN.
109
183
 
110
184
  ## Requirements
111
185
 
112
- - **A bundler** (Vite, webpack, Rollup, esbuild). `sparcoon` ships as
113
- unbundled, unminified ES2020 modules plus types; your bundler tree-shakes and
114
- minifies them, the same way three.js is consumed. Not a drop-in `<script>`.
115
- - **`three` >= 0.157**, as a peer dependency.
116
- - **Old devices.** Output targets ES2020; to run on pre-2015 hardware (for
117
- example a first-generation iPad Pro), downlevel it in your own build
118
- (Babel / SWC / your bundler's `target`). `sparcoon` leaves that final step
119
- to you.
120
-
121
- ## API overview
122
-
123
- - `FXEffect` -- whole-project timeline player (a `Group`); emitted modules
124
- subclass it. `play` / `stop`, `setEmitterParam` / `setMeshParam`,
125
- `getEmitter` / `getMesh`, `dispose`.
126
- - `FXWorld` -- the tick domain. `FXWorld.update(deltaTime)` per frame drives the
127
- default world; `new FXWorld()` is an isolated clock and pool.
128
- - Artifact contract types, the `FX_*` core-layout constants, CPU math helpers
129
- (`fxMix`, `fxSnoise3`, ...), and `fxDataTexture`.
186
+ | | |
187
+ | -------- | ---------------------------------------------------------------------------------- |
188
+ | three.js | `>=0.157 <0.180`, a peer dependency |
189
+ | Bundler | Vite, webpack, Rollup, esbuild - the package ships unminified ESM plus types |
190
+ | JS level | ES2020; for devices older than 2015, transpile it on your side |
191
+ | WebGL | WebGL1 is enough for the Baseline tier; WebGL2 unlocks Standard and the GPU kernel |
192
+
193
+ Dropping the package in through a `<script>` tag is not supported: these are unminified ES modules
194
+ that expect your bundler. The ES2020 target is deliberate, so that you can downlevel it with your own
195
+ Babel or SWC as far as you need, down to first-generation iPad Pro hardware.
196
+
197
+ ## What the runtime does not do
198
+
199
+ - **It does not load assets.** You prepare the textures and geometries and pass them to the effect's
200
+ constructor. Freeing them is yours as well: the runtime never disposes resources it does not own.
201
+ - **It contains no compiler, graph or validator.** All of that stays in the
202
+ [editor](https://github.com/jango-git/sparcoon-editor); only the result arrives here.
203
+ - **It brings no lighting.** A lit graph reads the lights of your scene, so a lit effect needs a
204
+ light probe and a directional light nearby. The preview's own lighting rig is not exported.
205
+ - **It does not decide your scene's draw order.** Depth sorting of particles is enabled per emitter
206
+ and costs frame time.
207
+
208
+ ## The `sparcoon/editor` entry point
209
+
210
+ A separate, unstable entry for tools that build objects from artifacts directly:
211
+ `FXEmitter.fromArtifacts` and `FXMesh.fromArtifact`, targeted value updates, swapping the render half
212
+ without resetting the simulation, and the keyframe-sampling functions shared with the editor. The
213
+ editor's live preview uses it. An application that simply plays a finished effect has no reason to go
214
+ there: the whole path it needs runs through `FXEffect`.
215
+
216
+ ## Local development
217
+
218
+ ```sh
219
+ npm ci
220
+ npm run build # tsc -> dist/, unminified ESM plus .d.ts
221
+ npm test # vitest run
222
+ npm run lint
223
+ ```
224
+
225
+ ## Status
226
+
227
+ Pre-release, under active development. If you hit a bug, the most useful thing is a minimal
228
+ reproduction plus the editor project `.json` it shows up on.
130
229
 
131
230
  ## License
132
231
 
133
- MIT (c) jango
232
+ MIT - use it in commercial work, client projects and ads that ship; keep the copyright notice. See
233
+ [LICENSE](LICENSE).
@@ -3,9 +3,11 @@ import type { FXGraphLambertMaterialOptions, FXGraphUnlitMaterialOptions } from
3
3
  /**
4
4
  * Frozen editor<->runtime boundary. The editor compiles a graph into a plain ESM module that
5
5
  * exports an {@link FXRenderArtifact} (GLSL + uniforms + textures) and an {@link FXBehaviorArtifact}
6
- * (authored spawn/update functions + bindings + buffer layout); the runtime only executes them.
7
- * Runtime-owned and exported; no code generation, no eval (the app's bundler compiles the authored
8
- * functions). Every value scrub must no-op on an unknown name so a name drift never corrupts values.
6
+ * (authored spawn/update functions + bindings + buffer layout), plus an optional
7
+ * {@link FXParticleKernelArtifact} (a fused GPU transform-feedback program) when the graph opts into
8
+ * GPU simulation; the runtime only executes them. Runtime-owned and exported; no code generation, no
9
+ * eval (the app's bundler compiles the authored JS functions, the editor precompiles the GLSL).
10
+ * Every value scrub must no-op on an unknown name so a name drift never corrupts values.
9
11
  */
10
12
  /** One state buffer: name + per-particle stride in floats. */
11
13
  export interface FXBufferLayout {
@@ -98,6 +100,23 @@ export interface FXRenderArtifact {
98
100
  /** Attributes the shader reads (`p_fx_<name>`); the emitter merges these with the sim's write set. */
99
101
  readonly attributeReads: readonly FXAttributeDecl[];
100
102
  }
103
+ /**
104
+ * Which GLSL-family compiler tier produced a compiled {@link FXRenderArtifact}: `"baseline"` is
105
+ * the WebGL1/GLSL-ES-1.00-style compiler; `"standard"` is the WebGL2/GLSL-ES-3.00-only compiler
106
+ * (may use int/ivec and other standard-only capabilities a `"baseline"` graph cannot). Both
107
+ * compile to the same GLSL-text artifact shape and splice through the same material builders
108
+ * unchanged - Three already upgrades any material to `#version 300 es` (with full
109
+ * `attribute`/`varying`/`gl_FragColor` backward-compat macros) on an actual WebGL2 context,
110
+ * regardless of tier, so the runtime needs no declaration-style distinction between the two.
111
+ *
112
+ * Deliberately scoped to the GLSL family in its name: a structurally different family (the future
113
+ * advanced tier - a WGSL/WebGPU compiler, whose artifact has no vertex/fragment GLSL text at all)
114
+ * would need its own sibling type and its own field on the emitter/mesh spec, never a third member
115
+ * of this union or a third key in {@link FXRenderArtifactsByGLSLTier}.
116
+ */
117
+ export type FXGLSLRenderTier = "baseline" | "standard";
118
+ /** One emitter/mesh's render artifact, precompiled once per GLSL tier the editor supports. */
119
+ export type FXRenderArtifactsByGLSLTier = Readonly<Record<FXGLSLRenderTier, FXRenderArtifact>>;
101
120
  /**
102
121
  * Behavior half of an emitted effect: authored spawn/update functions mutating the packed state
103
122
  * buffers, plus buffer layout and live bindings. Math helpers are inlined, so the artifact carries
@@ -118,3 +137,32 @@ export interface FXBehaviorArtifact {
118
137
  /** Advances all live particles `[0, count)`. */
119
138
  update(buffers: FXKernelBuffers, count: number, deltaTime: number, bindings: Record<string, FXValueSlot<number | Float32Array>>, emitter?: FXEmitterTransform): void;
120
139
  }
140
+ /**
141
+ * Standard-tier (WebGL2, transform-feedback) behavior half of an emitted effect - a sibling of
142
+ * {@link FXBehaviorArtifact}, not a variant of it: a compiled GLSL program has no JS function
143
+ * shape to reuse. One fused GLSL ES 3.00 vertex-shader program handles both spawn and update in a
144
+ * single draw, branching per-invocation on the runtime-supplied spawn range (see
145
+ * `behaviorTransformFeedbackLayout.ts`'s uniform-name contract) - never two separate programs,
146
+ * since transform feedback cannot read and write the same buffer in one pass, and splitting the
147
+ * update range around a wrapping spawn cursor compounds badly with GPU-side buffer offsets.
148
+ *
149
+ * Optional per emitter/mesh spec: present only when the graph's spawn node had "Try GPU
150
+ * simulation" on and the graph actually compiled to GLSL; every emitter always has a matching
151
+ * {@link FXBehaviorArtifact} too (the mandatory JS fallback, including for the WebGL2->WebGL1
152
+ * emergency downgrade), so the runtime is never left with only this artifact and no JS twin.
153
+ */
154
+ export interface FXParticleKernelArtifact {
155
+ readonly vertexSource: string;
156
+ readonly fragmentSource: string;
157
+ /** State buffers this program's `in`/`out` pairs cover, in declaration order - a buffer's index
158
+ * here IS its `layout(location = N)` attribute index in {@link vertexSource}; the runtime binds
159
+ * by that index, never by looking an attribute up by name. */
160
+ readonly buffers: readonly FXBufferLayout[];
161
+ /** Varying names for `gl.transformFeedbackVaryings`, one per {@link buffers} entry, same order. */
162
+ readonly transformFeedbackVaryings: readonly string[];
163
+ /** Live-tunable uniform values (graph-authored params only - the fixed contract uniforms in
164
+ * `behaviorTransformFeedbackLayout.ts` are always declared and are the runtime's own
165
+ * responsibility to set every tick, not part of this map). Float32Array is reserved for a
166
+ * future sampler2D-backed (LUT) uniform; no standard-tier node produces one yet. */
167
+ readonly bindings: Record<string, FXValueSlot<number | Float32Array>>;
168
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Frozen transform-feedback (WebGL2, standard-tier behavior) uniform-name contract, shared by the
3
+ * editor's standard-tier behavior assembler (`FXKernelBuildStandard.Internal.ts`) and the
4
+ * runtime's transform-feedback driver. Every fused spawn+update program declares all of these
5
+ * uniforms unconditionally, regardless of whether a given graph actually reads them - fixed,
6
+ * literal names both sides import instead of independently deriving, the same reasoning as
7
+ * `coreLayout.ts`'s `PARTICLE_AGE`-style macros. Dependency-free (the editor imports this too), so
8
+ * keep it three-free and portable.
9
+ *
10
+ * Buffer-backed `in` attributes and `out` (transform-feedback) varyings are NOT listed here: a
11
+ * compiled program binds attributes by explicit `layout(location = N)` index (N = the buffer's
12
+ * position in the artifact's own `buffers` array), and transform-feedback varying names travel
13
+ * with the artifact itself (`FXParticleKernelArtifact.transformFeedbackVaryings`) - neither needs
14
+ * a shared naming scheme, only these truly fixed, graph-independent uniforms do.
15
+ */
16
+ export declare const FX_TRANSFORM_FEEDBACK_SPAWN_RANGE_START_UNIFORM = "u_fxSpawnRangeStart";
17
+ export declare const FX_TRANSFORM_FEEDBACK_SPAWN_RANGE_COUNT_UNIFORM = "u_fxSpawnRangeCount";
18
+ export declare const FX_TRANSFORM_FEEDBACK_SPAWN_ID_BASE_UNIFORM = "u_fxSpawnIdBase";
19
+ export declare const FX_TRANSFORM_FEEDBACK_CAPACITY_UNIFORM = "u_fxCapacity";
20
+ export declare const FX_TRANSFORM_FEEDBACK_RAND_SEED_UNIFORM = "u_fxRandSeed";
21
+ export declare const FX_TRANSFORM_FEEDBACK_DELTA_TIME_UNIFORM = "u_fxDt";
22
+ export declare const FX_TRANSFORM_FEEDBACK_MODEL_MATRIX_UNIFORM = "u_fxModelMatrix";
23
+ export declare const FX_TRANSFORM_FEEDBACK_OBJECT_VELOCITY_UNIFORM = "u_fxObjectVelocity";
24
+ export declare const FX_TRANSFORM_FEEDBACK_OBJECT_ANGULAR_VELOCITY_UNIFORM = "u_fxObjectAngularVelocity";
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Frozen transform-feedback (WebGL2, standard-tier behavior) uniform-name contract, shared by the
3
+ * editor's standard-tier behavior assembler (`FXKernelBuildStandard.Internal.ts`) and the
4
+ * runtime's transform-feedback driver. Every fused spawn+update program declares all of these
5
+ * uniforms unconditionally, regardless of whether a given graph actually reads them - fixed,
6
+ * literal names both sides import instead of independently deriving, the same reasoning as
7
+ * `coreLayout.ts`'s `PARTICLE_AGE`-style macros. Dependency-free (the editor imports this too), so
8
+ * keep it three-free and portable.
9
+ *
10
+ * Buffer-backed `in` attributes and `out` (transform-feedback) varyings are NOT listed here: a
11
+ * compiled program binds attributes by explicit `layout(location = N)` index (N = the buffer's
12
+ * position in the artifact's own `buffers` array), and transform-feedback varying names travel
13
+ * with the artifact itself (`FXParticleKernelArtifact.transformFeedbackVaryings`) - neither needs
14
+ * a shared naming scheme, only these truly fixed, graph-independent uniforms do.
15
+ */
16
+ export const FX_TRANSFORM_FEEDBACK_SPAWN_RANGE_START_UNIFORM = "u_fxSpawnRangeStart";
17
+ export const FX_TRANSFORM_FEEDBACK_SPAWN_RANGE_COUNT_UNIFORM = "u_fxSpawnRangeCount";
18
+ export const FX_TRANSFORM_FEEDBACK_SPAWN_ID_BASE_UNIFORM = "u_fxSpawnIdBase";
19
+ export const FX_TRANSFORM_FEEDBACK_CAPACITY_UNIFORM = "u_fxCapacity";
20
+ export const FX_TRANSFORM_FEEDBACK_RAND_SEED_UNIFORM = "u_fxRandSeed";
21
+ export const FX_TRANSFORM_FEEDBACK_DELTA_TIME_UNIFORM = "u_fxDt";
22
+ export const FX_TRANSFORM_FEEDBACK_MODEL_MATRIX_UNIFORM = "u_fxModelMatrix";
23
+ export const FX_TRANSFORM_FEEDBACK_OBJECT_VELOCITY_UNIFORM = "u_fxObjectVelocity";
24
+ export const FX_TRANSFORM_FEEDBACK_OBJECT_ANGULAR_VELOCITY_UNIFORM = "u_fxObjectAngularVelocity";
@@ -1,22 +1,26 @@
1
1
  /**
2
2
  * Frozen per-particle ABI shared by the runtime loop and every editor-emitted module.
3
- * Two GPU-mirrored core buffers only: `position` (vec3) and `lifecycle` (vec2 [age, lifetime]);
3
+ * Two GPU-mirrored core buffers only: `position` (vec3) and `lifecycle` (vec3 [age, lifetime, id]);
4
4
  * a particle is dead once `age >= lifetime`. Everything else (velocity, scale, ...) is an ordinary
5
5
  * node-declared `a_fx_<name>` attribute, not core.
6
6
  *
7
- * Split into two buffers because a Three instanced attribute is at most a vec4 - a 5-float core
8
- * cannot be one attribute, whereas vec3 + vec2 are two clean varyings. Dependency-free (the editor's
7
+ * Split into two buffers because a Three instanced attribute is at most a vec4 - a 6-float core
8
+ * cannot be one attribute, whereas vec3 + vec3 are two clean varyings. Dependency-free (the editor's
9
9
  * module emitter imports this too), so keep it three-free and portable.
10
10
  */
11
11
  export declare const FX_CORE_POSITION = "position";
12
12
  export declare const FX_CORE_LIFECYCLE = "lifecycle";
13
13
  export declare const FX_CORE_POSITION_STRIDE = 3;
14
- export declare const FX_CORE_LIFECYCLE_STRIDE = 2;
14
+ export declare const FX_CORE_LIFECYCLE_STRIDE = 3;
15
15
  export declare const FX_POSITION_X = 0;
16
16
  export declare const FX_POSITION_Y = 1;
17
17
  export declare const FX_POSITION_Z = 2;
18
18
  export declare const FX_AGE = 0;
19
19
  export declare const FX_LIFETIME = 1;
20
+ export declare const FX_ID = 2;
20
21
  export declare const FX_CORE_POSITION_VARYING = "p_position";
21
22
  export declare const FX_CORE_LIFECYCLE_VARYING = "p_lifecycle";
22
- export declare const FX_CORE_PARTICLE_DEFINES = "\n #define PARTICLE_POSITION_X p_position.x\n #define PARTICLE_POSITION_Y p_position.y\n #define PARTICLE_POSITION_Z p_position.z\n #define PARTICLE_POSITION p_position\n\n #define PARTICLE_AGE p_lifecycle.x\n #define PARTICLE_LIFETIME p_lifecycle.y\n";
23
+ export declare const ATTRIBUTE_BUFFER_PREFIX = "fx_";
24
+ export declare function isCoreBufferName(bufferName: string): boolean;
25
+ export declare function meshPropertyKeyFor(bufferName: string): string;
26
+ export declare const FX_CORE_PARTICLE_DEFINES = "\n #define PARTICLE_POSITION_X p_position.x\n #define PARTICLE_POSITION_Y p_position.y\n #define PARTICLE_POSITION_Z p_position.z\n #define PARTICLE_POSITION p_position\n\n #define PARTICLE_AGE p_lifecycle.x\n #define PARTICLE_LIFETIME p_lifecycle.y\n #define PARTICLE_ID p_lifecycle.z\n";
@@ -1,24 +1,36 @@
1
1
  /**
2
2
  * Frozen per-particle ABI shared by the runtime loop and every editor-emitted module.
3
- * Two GPU-mirrored core buffers only: `position` (vec3) and `lifecycle` (vec2 [age, lifetime]);
3
+ * Two GPU-mirrored core buffers only: `position` (vec3) and `lifecycle` (vec3 [age, lifetime, id]);
4
4
  * a particle is dead once `age >= lifetime`. Everything else (velocity, scale, ...) is an ordinary
5
5
  * node-declared `a_fx_<name>` attribute, not core.
6
6
  *
7
- * Split into two buffers because a Three instanced attribute is at most a vec4 - a 5-float core
8
- * cannot be one attribute, whereas vec3 + vec2 are two clean varyings. Dependency-free (the editor's
7
+ * Split into two buffers because a Three instanced attribute is at most a vec4 - a 6-float core
8
+ * cannot be one attribute, whereas vec3 + vec3 are two clean varyings. Dependency-free (the editor's
9
9
  * module emitter imports this too), so keep it three-free and portable.
10
10
  */
11
11
  export const FX_CORE_POSITION = "position";
12
12
  export const FX_CORE_LIFECYCLE = "lifecycle";
13
13
  export const FX_CORE_POSITION_STRIDE = 3;
14
- export const FX_CORE_LIFECYCLE_STRIDE = 2;
14
+ export const FX_CORE_LIFECYCLE_STRIDE = 3;
15
15
  export const FX_POSITION_X = 0;
16
16
  export const FX_POSITION_Y = 1;
17
17
  export const FX_POSITION_Z = 2;
18
18
  export const FX_AGE = 0;
19
19
  export const FX_LIFETIME = 1;
20
+ export const FX_ID = 2;
20
21
  export const FX_CORE_POSITION_VARYING = "p_position";
21
22
  export const FX_CORE_LIFECYCLE_VARYING = "p_lifecycle";
23
+ // A kernel-compiled buffer name (e.g. "velocity", as authored/compiled - always unprefixed) to its
24
+ // mesh property-buffer key: unchanged for the two core buffers, `fx_<name>` for every attribute.
25
+ // The one place this mapping is defined - every mesh/simulation-holder pairing (JS or GPU) must
26
+ // go through this, not re-derive the prefix locally, or the two sides can silently drift apart.
27
+ export const ATTRIBUTE_BUFFER_PREFIX = "fx_";
28
+ export function isCoreBufferName(bufferName) {
29
+ return bufferName === FX_CORE_POSITION || bufferName === FX_CORE_LIFECYCLE;
30
+ }
31
+ export function meshPropertyKeyFor(bufferName) {
32
+ return isCoreBufferName(bufferName) ? bufferName : `${ATTRIBUTE_BUFFER_PREFIX}${bufferName}`;
33
+ }
22
34
  // Stable macro names so emitted render GLSL reads PARTICLE_POSITION_X / PARTICLE_AGE etc. instead
23
35
  // of hardcoding the varying names this module owns.
24
36
  export const FX_CORE_PARTICLE_DEFINES = `
@@ -29,4 +41,5 @@ export const FX_CORE_PARTICLE_DEFINES = `
29
41
 
30
42
  #define PARTICLE_AGE ${FX_CORE_LIFECYCLE_VARYING}.x
31
43
  #define PARTICLE_LIFETIME ${FX_CORE_LIFECYCLE_VARYING}.y
44
+ #define PARTICLE_ID ${FX_CORE_LIFECYCLE_VARYING}.z
32
45
  `;
@@ -1,5 +1,6 @@
1
1
  export { FXEmitter } from "../emitter/FXEmitter.js";
2
2
  export type { FXFromArtifactsOptions } from "../emitter/FXFromArtifactsOptions.js";
3
+ export type { FXApplyRenderArtifactOptions } from "../emitter/FXApplyRenderArtifactOptions.js";
3
4
  export type { FXApplyValues, FXEmitterOptions, FXEmitterBurstOptions, FXEmitterPlayOptions, } from "../emitter/FXEmitter.Internal.js";
4
5
  export { FXMesh } from "../render/FXMesh.js";
5
6
  export type { FXFromMeshArtifactOptions } from "../render/FXMesh.js";
@@ -13,5 +13,5 @@ export { FXMesh } from "../render/FXMesh.js";
13
13
  export { FXMeshMaterial } from "../render/FXMeshMaterial.js";
14
14
  export { resolveGeometrySource } from "../instancedParticle/primitiveGeometry.js";
15
15
  // Shared keyframe/transform sampling: the editor bakes keyframes and the runtime samples them, so
16
- // both sides must sample identically. See the frozen-contract note in CLAUDE.md.
16
+ // both sides must sample identically - a frozen contract, never a one-sided edit.
17
17
  export { frameOfTime, keyframeSegment, normalizeQuaternion, quaternionFromValue, sampleTrack, sampleTracks, sampleTransform, slerpQuaternion, timeOfFrame, vectorFromValue, } from "../effect/FXEffectSampling.Internal.js";
@@ -18,6 +18,8 @@ export declare abstract class FXEffect extends Group {
18
18
  private readonly specification;
19
19
  private readonly world;
20
20
  private readonly emitters;
21
+ private readonly emitterRenders;
22
+ private readonly meshRenders;
21
23
  private readonly meshMaterials;
22
24
  private readonly meshes;
23
25
  private readonly meshMotionTrackers;
@@ -63,6 +65,8 @@ export declare abstract class FXEffect extends Group {
63
65
  dispose(): void;
64
66
  private resetTransport;
65
67
  private advance;
68
+ /** Poses every entity for the frame `time` falls in, unless it already is. */
69
+ private poseAt;
66
70
  /** Fires burst/play events whose time lies in `[from, to)` on their owning emitter. */
67
71
  private fire;
68
72
  /**