sparcoon 0.9.0 → 0.9.2
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 +11 -6
- 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 +9 -0
- package/dist/editor/index.js +17 -0
- package/dist/effect/FXEffect.d.ts +2 -0
- package/dist/effect/FXEffect.js +30 -10
- 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 +42 -13
- package/dist/emitter/FXEmitter.js +113 -152
- 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 +181 -0
- package/dist/index.d.ts +5 -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/instancedParticle/primitiveGeometry.d.ts +10 -1
- package/dist/instancedParticle/primitiveGeometry.js +1 -1
- 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.d.ts +47 -0
- package/dist/render/FXMesh.js +97 -0
- package/dist/world/FXWorld.d.ts +27 -17
- package/dist/world/FXWorld.js +32 -31
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -13,7 +13,8 @@ ships to your app.
|
|
|
13
13
|
## Features
|
|
14
14
|
|
|
15
15
|
- Instanced billboard and 3D-mesh particles in a single draw call.
|
|
16
|
-
- Per-particle simulation from a precompiled kernel
|
|
16
|
+
- Per-particle simulation from a precompiled kernel, on the CPU or -- on a
|
|
17
|
+
WebGL2 renderer -- the GPU (transform feedback); no `eval`, no codegen either way.
|
|
17
18
|
- Whole-project playback: an editor module subclasses `FXEffect`.
|
|
18
19
|
- One `FXWorld.update` per frame drives every effect.
|
|
19
20
|
- Optional depth sorting, shadows, and custom-geometry particles.
|
|
@@ -102,10 +103,14 @@ give it its own world. `world.dispose()` disposes every effect in it.
|
|
|
102
103
|
|
|
103
104
|
The **Sparcoon Editor** compiles a node graph into a plain ES module exporting
|
|
104
105
|
a render artifact (GLSL, uniforms, textures) and a behavior artifact (authored
|
|
105
|
-
spawn/update functions)
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
106
|
+
spawn/update functions), plus -- when the graph opts into GPU simulation -- a
|
|
107
|
+
fused WebGL2 transform-feedback kernel. This runtime executes them: your bundler
|
|
108
|
+
compiles the authored functions like any other source and the behavior runs on
|
|
109
|
+
the CPU, or, when you pass a WebGL2 `renderer` in the effect options, the
|
|
110
|
+
precompiled kernel runs the simulation on the GPU instead (falling back to the
|
|
111
|
+
CPU behavior if GPU setup fails). Nothing is evaluated at runtime either way.
|
|
112
|
+
The editor/runtime boundary is a frozen ABI: two core per-particle buffers,
|
|
113
|
+
`position` and `lifecycle` (`[age, lifetime, id]`).
|
|
109
114
|
|
|
110
115
|
## Requirements
|
|
111
116
|
|
|
@@ -126,7 +131,7 @@ buffers, `position` and `lifecycle` (`[age, lifetime]`).
|
|
|
126
131
|
- `FXWorld` -- the tick domain. `FXWorld.update(deltaTime)` per frame drives the
|
|
127
132
|
default world; `new FXWorld()` is an isolated clock and pool.
|
|
128
133
|
- Artifact contract types, the `FX_*` core-layout constants, CPU math helpers
|
|
129
|
-
(`fxMix`, `
|
|
134
|
+
(`fxMix`, `fxNoise3`, ...), and `fxDataTexture`.
|
|
130
135
|
|
|
131
136
|
## License
|
|
132
137
|
|
|
@@ -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
|
`;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { FXEmitter } from "../emitter/FXEmitter.js";
|
|
2
|
+
export type { FXFromArtifactsOptions } from "../emitter/FXFromArtifactsOptions.js";
|
|
3
|
+
export type { FXApplyRenderArtifactOptions } from "../emitter/FXApplyRenderArtifactOptions.js";
|
|
4
|
+
export type { FXApplyValues, FXEmitterOptions, FXEmitterBurstOptions, FXEmitterPlayOptions, } from "../emitter/FXEmitter.Internal.js";
|
|
5
|
+
export { FXMesh } from "../render/FXMesh.js";
|
|
6
|
+
export type { FXFromMeshArtifactOptions } from "../render/FXMesh.js";
|
|
7
|
+
export { FXMeshMaterial } from "../render/FXMeshMaterial.js";
|
|
8
|
+
export { resolveGeometrySource } from "../instancedParticle/primitiveGeometry.js";
|
|
9
|
+
export { frameOfTime, keyframeSegment, normalizeQuaternion, quaternionFromValue, sampleTrack, sampleTracks, sampleTransform, slerpQuaternion, timeOfFrame, vectorFromValue, } from "../effect/FXEffectSampling.Internal.js";
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Editor-tooling surface: `import ... from "sparcoon/editor"`.
|
|
2
|
+
//
|
|
3
|
+
// UNSTABLE - no semver guarantees. These are the low-level building blocks the Sparcoon Editor's
|
|
4
|
+
// live preview needs (rebuild a single emitter/mesh from freshly compiled artifacts on every graph
|
|
5
|
+
// edit), plus the keyframe-sampling math the editor and runtime must share so a baked preview and
|
|
6
|
+
// runtime playback never drift. Regular game/scene hosts use the main "sparcoon" entry
|
|
7
|
+
// (FXEffect + FXWorld); they should not reach for anything here.
|
|
8
|
+
export { FXEmitter } from "../emitter/FXEmitter.js";
|
|
9
|
+
// Self-contained VFX mesh (the mesh twin of FXEmitter): build one from a render artifact and let
|
|
10
|
+
// FXWorld.update drive it, including the object velocity/angular velocity a mesh render graph reads.
|
|
11
|
+
export { FXMesh } from "../render/FXMesh.js";
|
|
12
|
+
// Lower-level pieces FXMesh is built from - kept for tooling that assembles a mesh by hand.
|
|
13
|
+
export { FXMeshMaterial } from "../render/FXMeshMaterial.js";
|
|
14
|
+
export { resolveGeometrySource } from "../instancedParticle/primitiveGeometry.js";
|
|
15
|
+
// Shared keyframe/transform sampling: the editor bakes keyframes and the runtime samples them, so
|
|
16
|
+
// both sides must sample identically - a frozen contract, never a one-sided edit.
|
|
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;
|
package/dist/effect/FXEffect.js
CHANGED
|
@@ -29,6 +29,10 @@ function applyTransform(object, transform, liveChannels) {
|
|
|
29
29
|
function materializeValue(value) {
|
|
30
30
|
return typeof value === "number" ? value : [...value];
|
|
31
31
|
}
|
|
32
|
+
/** Picks one emitter/mesh's compiled artifact by render target (see {@link FXEffectOptions.renderer}). */
|
|
33
|
+
function resolveRenderArtifact(render, isWebGL2) {
|
|
34
|
+
return render[isWebGL2 ? "standard" : "baseline"];
|
|
35
|
+
}
|
|
32
36
|
// Duck-typed, not `instanceof`: the app's `three` and this package's peer-dep `three` can be two
|
|
33
37
|
// separate module instances (e.g. hoisted differently across a workspace), which would make
|
|
34
38
|
// `instanceof Texture`/`instanceof BufferGeometry` false for a perfectly good asset. Every `three`
|
|
@@ -115,6 +119,11 @@ export class FXEffect extends Group {
|
|
|
115
119
|
constructor(specification, assets, options = {}) {
|
|
116
120
|
super();
|
|
117
121
|
this.emitters = [];
|
|
122
|
+
// The single artifact resolved (by render target) from each emitter/mesh spec's
|
|
123
|
+
// FXRenderArtifactsByGLSLTier - parallel to `emitters`/`meshes`, read by the parameter-drive path
|
|
124
|
+
// instead of re-resolving the spec's map on every call.
|
|
125
|
+
this.emitterRenders = [];
|
|
126
|
+
this.meshRenders = [];
|
|
118
127
|
this.meshMaterials = [];
|
|
119
128
|
this.meshes = [];
|
|
120
129
|
// World-space velocity/angular-velocity tracker per mesh, parallel to `meshes`.
|
|
@@ -140,14 +149,22 @@ export class FXEffect extends Group {
|
|
|
140
149
|
this.world = options.world ?? FXWorld.getDefault();
|
|
141
150
|
applyTransform(this, specification.transform);
|
|
142
151
|
const geometries = geometriesFromAssets(assets);
|
|
152
|
+
const isWebGL2 = options.renderer?.capabilities.isWebGL2 ?? false;
|
|
143
153
|
for (const emitterSpec of specification.emitters) {
|
|
144
154
|
const textures = texturesForSlots(emitterSpec.externalSlots, assets);
|
|
145
|
-
const
|
|
155
|
+
const render = resolveRenderArtifact(emitterSpec.render, isWebGL2);
|
|
156
|
+
const emitter = FXEmitter.fromArtifacts(render, emitterSpec.behavior, {
|
|
146
157
|
textures,
|
|
147
158
|
geometries,
|
|
148
159
|
expectedCapacity: emitterSpec.expectedCapacity,
|
|
149
160
|
castShadow: emitterSpec.castShadow ?? false,
|
|
150
161
|
receiveShadow: emitterSpec.receiveShadow ?? false,
|
|
162
|
+
// Only offered on an actual WebGL2 negotiation - the same `isWebGL2` signal already
|
|
163
|
+
// picking `render`'s "standard" tier above. FXEmitter itself decides whether GPU setup
|
|
164
|
+
// then actually succeeds, falling back to `emitterSpec.behavior` in place if not - this
|
|
165
|
+
// effect never needs to know which one ends up driving a given emitter.
|
|
166
|
+
gpuKernel: isWebGL2 ? emitterSpec.gpuBehavior : undefined,
|
|
167
|
+
renderer: options.renderer,
|
|
151
168
|
}, this.world);
|
|
152
169
|
if (options.camera !== undefined && emitterSpec.sortInterval > 0) {
|
|
153
170
|
emitter.sortCamera = options.camera;
|
|
@@ -157,11 +174,13 @@ export class FXEffect extends Group {
|
|
|
157
174
|
this.add(emitter);
|
|
158
175
|
this.emitterIndexByName.set(emitterSpec.name, this.emitters.length);
|
|
159
176
|
this.emitters.push(emitter);
|
|
177
|
+
this.emitterRenders.push(render);
|
|
160
178
|
this.emitterDriven.push(new Set());
|
|
161
179
|
}
|
|
162
180
|
for (const meshSpec of specification.meshes) {
|
|
163
181
|
const textures = texturesForSlots(meshSpec.externalSlots, assets);
|
|
164
|
-
const
|
|
182
|
+
const render = resolveRenderArtifact(meshSpec.render, isWebGL2);
|
|
183
|
+
const material = new FXMeshMaterial(render, textures);
|
|
165
184
|
const mesh = new Mesh(resolveGeometrySource(meshSpec.geometry, geometries), material.buildThreeMaterial());
|
|
166
185
|
if (meshSpec.receiveShadow ?? false) {
|
|
167
186
|
mesh.receiveShadow = true;
|
|
@@ -174,6 +193,7 @@ export class FXEffect extends Group {
|
|
|
174
193
|
this.add(mesh);
|
|
175
194
|
this.meshIndexByName.set(meshSpec.name, this.meshes.length);
|
|
176
195
|
this.meshes.push(mesh);
|
|
196
|
+
this.meshRenders.push(render);
|
|
177
197
|
this.meshMaterials.push(material);
|
|
178
198
|
this.meshMotionTrackers.push(new FXObjectMotionTracker());
|
|
179
199
|
this.meshDriven.push(new Set());
|
|
@@ -283,7 +303,7 @@ export class FXEffect extends Group {
|
|
|
283
303
|
const specification = this.specification.emitters[index];
|
|
284
304
|
const uniforms = {};
|
|
285
305
|
const bindings = {};
|
|
286
|
-
writeParameterSlots(
|
|
306
|
+
writeParameterSlots(this.emitterRenders[index], specification.behavior, parameter, materializeValue(value), uniforms, bindings);
|
|
287
307
|
if (Object.keys(uniforms).length > 0 || Object.keys(bindings).length > 0) {
|
|
288
308
|
this.emitters[index].applyValues({ uniforms, bindings });
|
|
289
309
|
}
|
|
@@ -297,10 +317,9 @@ export class FXEffect extends Group {
|
|
|
297
317
|
if (index === undefined) {
|
|
298
318
|
return;
|
|
299
319
|
}
|
|
300
|
-
const specification = this.specification.meshes[index];
|
|
301
320
|
const uniforms = {};
|
|
302
321
|
const bindings = {};
|
|
303
|
-
writeParameterSlots(
|
|
322
|
+
writeParameterSlots(this.meshRenders[index], NO_BEHAVIOR_BINDINGS, parameter, materializeValue(value), uniforms, bindings);
|
|
304
323
|
if (Object.keys(uniforms).length > 0) {
|
|
305
324
|
this.meshMaterials[index].applyUniformValues(uniforms);
|
|
306
325
|
}
|
|
@@ -415,16 +434,17 @@ export class FXEffect extends Group {
|
|
|
415
434
|
}
|
|
416
435
|
driveEmitterValues(index, values) {
|
|
417
436
|
const specification = this.specification.emitters[index];
|
|
437
|
+
const render = this.emitterRenders[index];
|
|
418
438
|
const driven = this.emitterDriven[index];
|
|
419
439
|
const uniforms = {};
|
|
420
440
|
const bindings = {};
|
|
421
441
|
for (const [name, value] of values) {
|
|
422
|
-
writeParameterSlots(
|
|
442
|
+
writeParameterSlots(render, specification.behavior, name, value, uniforms, bindings);
|
|
423
443
|
driven.add(name);
|
|
424
444
|
}
|
|
425
445
|
for (const name of [...driven]) {
|
|
426
446
|
if (!values.has(name)) {
|
|
427
|
-
writeParameterBaseline(
|
|
447
|
+
writeParameterBaseline(render, specification.behavior, name, uniforms, bindings);
|
|
428
448
|
driven.delete(name);
|
|
429
449
|
}
|
|
430
450
|
}
|
|
@@ -433,17 +453,17 @@ export class FXEffect extends Group {
|
|
|
433
453
|
}
|
|
434
454
|
}
|
|
435
455
|
driveMeshValues(index, values) {
|
|
436
|
-
const
|
|
456
|
+
const render = this.meshRenders[index];
|
|
437
457
|
const driven = this.meshDriven[index];
|
|
438
458
|
const uniforms = {};
|
|
439
459
|
const bindings = {};
|
|
440
460
|
for (const [name, value] of values) {
|
|
441
|
-
writeParameterSlots(
|
|
461
|
+
writeParameterSlots(render, NO_BEHAVIOR_BINDINGS, name, value, uniforms, bindings);
|
|
442
462
|
driven.add(name);
|
|
443
463
|
}
|
|
444
464
|
for (const name of [...driven]) {
|
|
445
465
|
if (!values.has(name)) {
|
|
446
|
-
writeParameterBaseline(
|
|
466
|
+
writeParameterBaseline(render, NO_BEHAVIOR_BINDINGS, name, uniforms, bindings);
|
|
447
467
|
driven.delete(name);
|
|
448
468
|
}
|
|
449
469
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { Camera } from "three";
|
|
2
|
-
import type { FXBehaviorArtifact, FXGeometrySource,
|
|
1
|
+
import type { Camera, WebGLRenderer } from "three";
|
|
2
|
+
import type { FXBehaviorArtifact, FXGeometrySource, FXParticleKernelArtifact, FXRenderArtifactsByGLSLTier } from "../artifact/FXArtifact.js";
|
|
3
3
|
import type { FXWorld } from "../world/FXWorld.js";
|
|
4
4
|
/**
|
|
5
5
|
* Data shapes for {@link FXEffect}: a whole exported project (every emitter + VFX mesh, its
|
|
@@ -48,8 +48,19 @@ export type FXEffectEvent = {
|
|
|
48
48
|
export interface FXEffectEmitterSpec {
|
|
49
49
|
/** The editor-authored name, addressing this emitter through {@link FXEffect.getEmitter} etc. */
|
|
50
50
|
readonly name: string;
|
|
51
|
-
readonly render:
|
|
51
|
+
readonly render: FXRenderArtifactsByGLSLTier;
|
|
52
52
|
readonly behavior: FXBehaviorArtifact;
|
|
53
|
+
/**
|
|
54
|
+
* The standard-tier (WebGL2, transform-feedback) behavior artifact - present only when this
|
|
55
|
+
* emitter's spawn node had "Try GPU simulation" on and the graph compiled to GLSL. `behavior`
|
|
56
|
+
* above is always present regardless - the mandatory JS fallback, including for the
|
|
57
|
+
* WebGL2->WebGL1 emergency downgrade - so a spec is never left with only this field and no JS
|
|
58
|
+
* twin. {@link FXEffect} passes this to
|
|
59
|
+
* `FXEmitter.fromArtifacts` only when the live renderer actually negotiated WebGL2 (the same
|
|
60
|
+
* `isWebGL2` signal that already picks `render`'s "standard" tier); the runtime decides for
|
|
61
|
+
* itself whether GPU setup then actually succeeds, falling back to `behavior` in place if not.
|
|
62
|
+
*/
|
|
63
|
+
readonly gpuBehavior?: FXParticleKernelArtifact;
|
|
53
64
|
readonly expectedCapacity: number;
|
|
54
65
|
readonly sortInterval: number;
|
|
55
66
|
/** Casts a shape-aware shadow; omitted (treated as false) by pre-shadow exports. */
|
|
@@ -72,7 +83,7 @@ export interface FXEffectEmitterSpec {
|
|
|
72
83
|
export interface FXEffectMeshSpec {
|
|
73
84
|
/** The editor-authored name, addressing this mesh through {@link FXEffect.getMesh} etc. */
|
|
74
85
|
readonly name: string;
|
|
75
|
-
readonly render:
|
|
86
|
+
readonly render: FXRenderArtifactsByGLSLTier;
|
|
76
87
|
readonly geometry: FXGeometrySource;
|
|
77
88
|
/** See {@link FXEffectEmitterSpec.castShadow}. */
|
|
78
89
|
readonly castShadow?: boolean;
|
|
@@ -112,4 +123,10 @@ export interface FXEffectOptions {
|
|
|
112
123
|
* own clock and particle pool (an independent time scale, or a group you tick separately).
|
|
113
124
|
*/
|
|
114
125
|
readonly world?: FXWorld;
|
|
126
|
+
/**
|
|
127
|
+
* The renderer this effect will draw through: `renderer.capabilities.isWebGL2` picks each
|
|
128
|
+
* emitter/mesh's `"standard"` or `"baseline"` artifact (see {@link FXRenderArtifactsByGLSLTier}).
|
|
129
|
+
* Omit to get the `"baseline"` artifact unconditionally (the pre-split behavior).
|
|
130
|
+
*/
|
|
131
|
+
readonly renderer?: WebGLRenderer;
|
|
115
132
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { BufferGeometry, Texture } from "three";
|
|
2
|
+
/**
|
|
3
|
+
* Options for {@link FXEmitter.applyRenderArtifact}: the render-only subset of
|
|
4
|
+
* {@link FXFromArtifactsOptions} - no `expectedCapacity`/`capacityStep`/`gpuKernel`/`renderer`,
|
|
5
|
+
* since none of those can change without a full {@link FXEmitter.fromArtifacts} rebuild. The app
|
|
6
|
+
* owns `textures`/`geometries`' lifecycle; the runtime never disposes them.
|
|
7
|
+
*/
|
|
8
|
+
export interface FXApplyRenderArtifactOptions {
|
|
9
|
+
readonly textures?: Record<string, Texture>;
|
|
10
|
+
readonly geometries?: Record<string, BufferGeometry>;
|
|
11
|
+
/** default false */
|
|
12
|
+
readonly receiveShadow?: boolean;
|
|
13
|
+
/** Casts a shape-aware shadow (a customDepthMaterial built from the new render artifact). default false */
|
|
14
|
+
readonly castShadow?: boolean;
|
|
15
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import type { Camera } from "three";
|
|
2
2
|
import { Object3D } from "three";
|
|
3
|
+
import type { FXBehaviorArtifact, FXRenderArtifact } from "../artifact/FXArtifact.js";
|
|
4
|
+
import type { FXApplyRenderArtifactOptions } from "./FXApplyRenderArtifactOptions.js";
|
|
5
|
+
import type { FXFromArtifactsOptions } from "./FXFromArtifactsOptions.js";
|
|
6
|
+
import { FXWorld } from "../world/FXWorld.js";
|
|
3
7
|
import type { FXApplyValues, FXEmitterBurstOptions, FXEmitterPlayOptions } from "./FXEmitter.Internal.js";
|
|
4
8
|
/**
|
|
5
9
|
* Particle emitter - one instanced billboard/mesh draw call, per-particle state driven by a
|
|
@@ -7,6 +11,12 @@ import type { FXApplyValues, FXEmitterBurstOptions, FXEmitterPlayOptions } from
|
|
|
7
11
|
* Three material. Built and ticked by {@link FXEffect} through an {@link FXWorld}. Not exported from
|
|
8
12
|
* the package (the runtime path is running artifacts, not hand-building emitters); reached only as
|
|
9
13
|
* the live handle returned by {@link FXEffect.getEmitter}.
|
|
14
|
+
*
|
|
15
|
+
* A thin facade over one {@link FXEmitterDriver} (JS or GPU - see `FXEmitterDriver.Internal.ts`),
|
|
16
|
+
* chosen once by {@link fromArtifacts} and never switched mid-playback. This class itself owns only
|
|
17
|
+
* what is genuinely backend-independent: play/burst/stop/prewarm scheduling, sort-fraction timing,
|
|
18
|
+
* object motion tracking, and the render material - the material itself CAN be replaced in place,
|
|
19
|
+
* by {@link applyRenderArtifact}, without touching the driver or its particle state.
|
|
10
20
|
*/
|
|
11
21
|
export declare class FXEmitter extends Object3D {
|
|
12
22
|
/** Camera for back-to-front depth sorting; `undefined` disables it. Distances are world-space, so any emitter/ancestor transform is handled correctly. */
|
|
@@ -14,10 +24,8 @@ export declare class FXEmitter extends Object3D {
|
|
|
14
24
|
/** Fraction of frames sorting runs on (`1` = every frame, `0.1` = ~every 10th); defaults to `0.1`. */
|
|
15
25
|
sortFraction: number;
|
|
16
26
|
private readonly world;
|
|
17
|
-
private readonly
|
|
18
|
-
private
|
|
19
|
-
private readonly simulation?;
|
|
20
|
-
private readonly collectedProperties;
|
|
27
|
+
private readonly driver;
|
|
28
|
+
private material;
|
|
21
29
|
private nextHandler;
|
|
22
30
|
private readonly pendingBursts;
|
|
23
31
|
private readonly activePlays;
|
|
@@ -29,10 +37,20 @@ export declare class FXEmitter extends Object3D {
|
|
|
29
37
|
private readonly motionScaleScratch;
|
|
30
38
|
private readonly motionTracker;
|
|
31
39
|
private destroyed;
|
|
32
|
-
|
|
33
|
-
|
|
40
|
+
/** Exact live count for a JS-driven emitter; for a GPU-driven one, there is no CPU-visible live
|
|
41
|
+
* count to report - `capacity` is the honest upper bound reported instead. */
|
|
34
42
|
get particleCount(): number;
|
|
43
|
+
/** The buffer's current allocated size - grows over time for a JS-driven emitter, fixed at
|
|
44
|
+
* construction for a GPU-driven one (no capacityStep growth path there). */
|
|
35
45
|
get particleCapacity(): number;
|
|
46
|
+
private get mesh();
|
|
47
|
+
/**
|
|
48
|
+
* Builds an emitter into `world` from a precompiled effect - the low-level building block behind
|
|
49
|
+
* {@link FXEffect}. Exposed for editor tooling through the `sparcoon/editor` entry (live preview
|
|
50
|
+
* rebuilds a single emitter per graph edit); not part of the main package surface, where the
|
|
51
|
+
* runtime path is running whole projects through `FXEffect`.
|
|
52
|
+
*/
|
|
53
|
+
static fromArtifacts(render: FXRenderArtifact, behavior: FXBehaviorArtifact, options?: FXFromArtifactsOptions, world?: FXWorld): FXEmitter;
|
|
36
54
|
/** Unregisters, removes from the scene, disposes mesh and material. Idempotent. */
|
|
37
55
|
destroy(): void;
|
|
38
56
|
/**
|
|
@@ -55,7 +73,13 @@ export declare class FXEmitter extends Object3D {
|
|
|
55
73
|
* @param stepDuration - Maximum step size in seconds. Defaults to `1/60`.
|
|
56
74
|
*/
|
|
57
75
|
prewarm(duration: number, stepDuration?: number): void;
|
|
58
|
-
/**
|
|
76
|
+
/**
|
|
77
|
+
* Kills all live particles and cancels all active plays and pending bursts. For a GPU-driven
|
|
78
|
+
* emitter this is a known-narrower reset (see the mesh's own `drop()` doc comment): there is no
|
|
79
|
+
* CPU-visible per-particle state to instantly zero, so already-alive particles keep aging out
|
|
80
|
+
* naturally rather than vanishing on this call's own frame - only the spawn cursor and pending
|
|
81
|
+
* schedule reset immediately.
|
|
82
|
+
*/
|
|
59
83
|
reset(): void;
|
|
60
84
|
/**
|
|
61
85
|
* Scrubs live parameter values into uniform/binding slots by name, picked up next frame - the
|
|
@@ -64,16 +88,21 @@ export declare class FXEmitter extends Object3D {
|
|
|
64
88
|
*/
|
|
65
89
|
applyValues(values: FXApplyValues): void;
|
|
66
90
|
/**
|
|
67
|
-
*
|
|
68
|
-
*
|
|
91
|
+
* Swaps only this emitter's render half in place - a new shader, geometry primitive/mesh, or
|
|
92
|
+
* shadow flags - leaving the behavior driver, particle buffers, instance count, and playback
|
|
93
|
+
* schedule completely untouched (unlike a structural edit that reaches {@link fromArtifacts},
|
|
94
|
+
* this never resets a running effect). The editor calls this instead of rebuilding whenever a
|
|
95
|
+
* graph edit only changed the render half.
|
|
96
|
+
*
|
|
97
|
+
* @throws If the new render artifact's attribute reads disagree with this emitter's already-
|
|
98
|
+
* allocated buffers (a genuine layout change - see {@link FXEmitterDriver.applyRenderArtifact}).
|
|
99
|
+
* This emitter is left exactly as it was before the call in that case; the caller must fall back
|
|
100
|
+
* to a fresh {@link fromArtifacts} instead, since only a new driver can safely resize buffers.
|
|
69
101
|
*/
|
|
70
|
-
|
|
71
|
-
private buildCollectedProperties;
|
|
102
|
+
applyRenderArtifact(render: FXRenderArtifact, options?: FXApplyRenderArtifactOptions): void;
|
|
72
103
|
private setSortFraction;
|
|
73
104
|
private assertCanSpawn;
|
|
74
|
-
private kernelBuffers;
|
|
75
105
|
private worldTransform;
|
|
76
|
-
private markBuffersNeedUpdate;
|
|
77
106
|
private tick;
|
|
78
107
|
private readonly onRendering;
|
|
79
108
|
private spawnBurst;
|