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.
- package/README.md +176 -76
- package/dist/artifact/FXArtifact.d.ts +51 -3
- package/dist/behaviorTransformFeedbackLayout.d.ts +24 -0
- package/dist/behaviorTransformFeedbackLayout.js +24 -0
- package/dist/coreLayout.d.ts +9 -5
- package/dist/coreLayout.js +17 -4
- package/dist/editor/index.d.ts +1 -0
- package/dist/editor/index.js +1 -1
- package/dist/effect/FXEffect.d.ts +4 -0
- package/dist/effect/FXEffect.js +47 -14
- package/dist/effect/FXEffectSpec.d.ts +21 -4
- package/dist/emitter/FXApplyRenderArtifactOptions.d.ts +15 -0
- package/dist/emitter/FXApplyRenderArtifactOptions.js +1 -0
- package/dist/emitter/FXEmitter.d.ts +44 -14
- package/dist/emitter/FXEmitter.js +132 -156
- package/dist/emitter/FXEmitterDriver.Internal.d.ts +26 -0
- package/dist/emitter/FXEmitterDriver.Internal.js +70 -0
- package/dist/emitter/FXFromArtifactsOptions.d.ts +12 -1
- package/dist/emitter/FXGPUEmitterDriver.Internal.d.ts +1 -0
- package/dist/emitter/FXGPUEmitterDriver.Internal.js +137 -0
- package/dist/emitter/FXJSEmitterDriver.Internal.d.ts +1 -0
- package/dist/emitter/FXJSEmitterDriver.Internal.js +133 -0
- package/dist/emitter/FXSimulationHolder.js +7 -5
- package/dist/emitter/FXTransformFeedbackSimulationHolder.d.ts +1 -0
- package/dist/emitter/FXTransformFeedbackSimulationHolder.js +249 -0
- package/dist/index.d.ts +4 -3
- package/dist/index.js +3 -2
- package/dist/instancedParticle/FXInstancedParticle.js +28 -1
- package/dist/instancedParticle/FXTransformFeedbackParticle.d.ts +1 -0
- package/dist/instancedParticle/FXTransformFeedbackParticle.js +169 -0
- package/dist/miscellaneous/fxMath.d.ts +18 -11
- package/dist/miscellaneous/fxMath.js +89 -209
- package/dist/render/FXArtifactMaterial.Internal.js +66 -40
- package/dist/render/FXArtifactMaterial.js +1 -1
- package/dist/render/FXMesh.js +2 -4
- 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
|
-
|
|
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
|
-
|
|
6
|
-
editor
|
|
7
|
-
|
|
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
|
-
>
|
|
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
|
-
##
|
|
17
|
+
## Who this is for
|
|
14
18
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
-
|
|
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`
|
|
42
|
+
`three` is a peer dependency; the supported range is `>=0.157 <0.180`.
|
|
29
43
|
|
|
30
44
|
## Quick start
|
|
31
45
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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 {
|
|
53
|
+
import { MyEffect, type MyEffectAssets } from "./effects/MyEffect"; // emitted by the editor
|
|
40
54
|
|
|
41
55
|
const loader = new TextureLoader();
|
|
42
|
-
const assets = {
|
|
43
|
-
|
|
44
|
-
fire: loader.load("fire.png"),
|
|
56
|
+
const assets: MyEffectAssets = {
|
|
57
|
+
spark: loader.load("spark.png"),
|
|
45
58
|
};
|
|
46
|
-
|
|
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(
|
|
52
|
-
FXWorld.update(
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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("
|
|
67
|
-
effect.setMeshParam("
|
|
98
|
+
effect.setEmitterParam("Sparks", "intensity", 0.7);
|
|
99
|
+
effect.setMeshParam("Shockwave", "glow", [1, 0.4, 0.1]);
|
|
68
100
|
|
|
69
|
-
effect.getEmitter("
|
|
70
|
-
effect.getMesh("
|
|
101
|
+
effect.getEmitter("Sparks")?.position.set(0, 1, 0);
|
|
102
|
+
effect.getMesh("Shockwave")?.rotateY(Math.PI);
|
|
71
103
|
```
|
|
72
104
|
|
|
73
|
-
|
|
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
|
-
|
|
76
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
|
88
|
-
scene.add(boss); // still
|
|
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(
|
|
91
|
-
FXWorld.update(
|
|
92
|
-
slowmo.update(
|
|
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
|
-
|
|
99
|
-
|
|
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
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
-
|
|
129
|
-
|
|
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
|
|
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)
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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";
|
package/dist/coreLayout.d.ts
CHANGED
|
@@ -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` (
|
|
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
|
|
8
|
-
* cannot be one attribute, whereas vec3 +
|
|
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 =
|
|
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
|
|
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";
|
package/dist/coreLayout.js
CHANGED
|
@@ -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` (
|
|
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
|
|
8
|
-
* cannot be one attribute, whereas vec3 +
|
|
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 =
|
|
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
|
`;
|
package/dist/editor/index.d.ts
CHANGED
|
@@ -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";
|
package/dist/editor/index.js
CHANGED
|
@@ -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
|
|
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
|
/**
|