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
|
@@ -1,37 +1,28 @@
|
|
|
1
1
|
import { Object3D, Quaternion, Vector3 } from "three";
|
|
2
2
|
import { FXWorld } from "../world/FXWorld.js";
|
|
3
3
|
import { FXObjectMotionTracker } from "./FXObjectMotion.Internal.js";
|
|
4
|
-
import { FX_AGE, FX_CORE_LIFECYCLE, FX_CORE_LIFECYCLE_STRIDE, FX_CORE_POSITION, FX_CORE_POSITION_STRIDE, } from "../coreLayout.js";
|
|
5
|
-
import { FXInstancedParticle } from "../instancedParticle/FXInstancedParticle.js";
|
|
6
4
|
import { resolveGeometrySource } from "../instancedParticle/primitiveGeometry.js";
|
|
7
5
|
import { assertValidNonNegativeNumber, assertValidPositiveInteger, assertValidPositiveNumber, } from "../miscellaneous/asserts.js";
|
|
8
6
|
import { FXArtifactMaterial } from "../render/FXArtifactMaterial.js";
|
|
9
|
-
import {
|
|
7
|
+
import { FXGPUEmitterDriver } from "./FXGPUEmitterDriver.Internal.js";
|
|
8
|
+
import { FXJSEmitterDriver } from "./FXJSEmitterDriver.Internal.js";
|
|
10
9
|
import { EMITTER_DEFAULT_CAPACITY_STEP, EMITTER_DEFAULT_CAST_SHADOW, EMITTER_DEFAULT_EXPECTED_CAPACITY, EMITTER_DEFAULT_PREWARM_MAX_STEP_COUNT, EMITTER_DEFAULT_PREWARM_MIN_STEP_DURATION, EMITTER_DEFAULT_RECEIVE_SHADOW, EMITTER_DEFAULT_SORT_FRACTION, } from "./FXEmitter.Internal.js";
|
|
11
|
-
// User attribute `name` -> mesh property-buffer key `fx_<name>`.
|
|
12
|
-
const ATTRIBUTE_BUFFER_PREFIX = "fx_";
|
|
13
|
-
// GLSL type of a float attribute, indexed by component count.
|
|
14
|
-
const GLSL_TYPE_BY_COMPONENTS = ["", "float", "vec2", "vec3", "vec4"];
|
|
15
|
-
const CORE_POSITION_TYPE = {
|
|
16
|
-
glslTypeName: "vec3",
|
|
17
|
-
bufferSize: FX_CORE_POSITION_STRIDE,
|
|
18
|
-
instantiable: true,
|
|
19
|
-
};
|
|
20
|
-
const CORE_LIFECYCLE_TYPE = {
|
|
21
|
-
glslTypeName: "vec2",
|
|
22
|
-
bufferSize: FX_CORE_LIFECYCLE_STRIDE,
|
|
23
|
-
instantiable: true,
|
|
24
|
-
};
|
|
25
10
|
/**
|
|
26
11
|
* Particle emitter - one instanced billboard/mesh draw call, per-particle state driven by a
|
|
27
12
|
* precompiled behavior artifact and appearance by a precompiled render artifact spliced into a
|
|
28
13
|
* Three material. Built and ticked by {@link FXEffect} through an {@link FXWorld}. Not exported from
|
|
29
14
|
* the package (the runtime path is running artifacts, not hand-building emitters); reached only as
|
|
30
15
|
* the live handle returned by {@link FXEffect.getEmitter}.
|
|
16
|
+
*
|
|
17
|
+
* A thin facade over one {@link FXEmitterDriver} (JS or GPU - see `FXEmitterDriver.Internal.ts`),
|
|
18
|
+
* chosen once by {@link fromArtifacts} and never switched mid-playback. This class itself owns only
|
|
19
|
+
* what is genuinely backend-independent: play/burst/stop/prewarm scheduling, sort-fraction timing,
|
|
20
|
+
* object motion tracking, and the render material - the material itself CAN be replaced in place,
|
|
21
|
+
* by {@link applyRenderArtifact}, without touching the driver or its particle state.
|
|
31
22
|
*/
|
|
32
23
|
export class FXEmitter extends Object3D {
|
|
33
24
|
/** @internal Use {@link FXEmitter.fromArtifacts}. */
|
|
34
|
-
constructor(
|
|
25
|
+
constructor(driver, material, options, world) {
|
|
35
26
|
super();
|
|
36
27
|
this.nextHandler = 0;
|
|
37
28
|
this.pendingBursts = [];
|
|
@@ -52,38 +43,22 @@ export class FXEmitter extends Object3D {
|
|
|
52
43
|
this.motionScaleScratch = new Vector3();
|
|
53
44
|
this.motionTracker = new FXObjectMotionTracker();
|
|
54
45
|
this.destroyed = false;
|
|
55
|
-
this.kernelBuffersCacheVersion = -1;
|
|
56
46
|
this.onRendering = (deltaTime) => {
|
|
57
47
|
this.tick(deltaTime);
|
|
58
|
-
if (this.sortCamera
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
48
|
+
if (this.sortCamera === undefined) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
// `sortFraction` is public, so clamp here too: a direct assignment above 1 would grow the accumulator unbounded.
|
|
52
|
+
this.sortingAccumulator += Math.min(Math.max(this.sortFraction, 0), 1);
|
|
53
|
+
if (this.sortingAccumulator >= 1) {
|
|
54
|
+
this.sortingAccumulator -= 1;
|
|
55
|
+
this.sortCamera.getWorldPosition(this.sortingCameraWorldPosition);
|
|
56
|
+
this.driver.sortByDistance(this.sortingCameraWorldPosition);
|
|
68
57
|
}
|
|
69
58
|
};
|
|
70
59
|
this.world = world;
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
assertValidPositiveInteger(options.expectedCapacity, "FXEmitter.constructor.options.expectedCapacity");
|
|
74
|
-
}
|
|
75
|
-
if (options.capacityStep !== undefined) {
|
|
76
|
-
assertValidPositiveInteger(options.capacityStep, "FXEmitter.constructor.options.capacityStep");
|
|
77
|
-
}
|
|
78
|
-
this.material = material;
|
|
79
|
-
this.simulation = simulation;
|
|
80
|
-
// Property set = the two core buffers plus one `fx_<name>` per attribute, known before the mesh
|
|
81
|
-
// and Three material are built so `a_<name>`/`p_<name>` are declared up front. A cross-artifact
|
|
82
|
-
// width disagreement fails fast here.
|
|
83
|
-
const components = this.attributeComponents();
|
|
84
|
-
this.collectedProperties = this.buildCollectedProperties(components);
|
|
85
|
-
const threeMaterial = material.buildThreeMaterial(this.collectedProperties);
|
|
86
|
-
this.mesh = new FXInstancedParticle(this.collectedProperties, options.expectedCapacity ?? EMITTER_DEFAULT_EXPECTED_CAPACITY, options.capacityStep ?? EMITTER_DEFAULT_CAPACITY_STEP, threeMaterial, baseGeometry);
|
|
60
|
+
this.driver = driver;
|
|
61
|
+
this.artifactMaterial = material;
|
|
87
62
|
if (options.receiveShadow ?? EMITTER_DEFAULT_RECEIVE_SHADOW) {
|
|
88
63
|
this.mesh.receiveShadow = true;
|
|
89
64
|
}
|
|
@@ -92,7 +67,7 @@ export class FXEmitter extends Object3D {
|
|
|
92
67
|
// false on the mesh, so the shadow pass's frustum check is a no-op just like the color pass.
|
|
93
68
|
if (options.castShadow ?? EMITTER_DEFAULT_CAST_SHADOW) {
|
|
94
69
|
this.mesh.castShadow = true;
|
|
95
|
-
this.mesh.customDepthMaterial = material.buildThreeDepthMaterial(this.
|
|
70
|
+
this.mesh.customDepthMaterial = material.buildThreeDepthMaterial(this.driver.varyings);
|
|
96
71
|
}
|
|
97
72
|
this.sortCamera = options.sortCamera;
|
|
98
73
|
this.sortFraction = EMITTER_DEFAULT_SORT_FRACTION;
|
|
@@ -103,11 +78,20 @@ export class FXEmitter extends Object3D {
|
|
|
103
78
|
this.add(this.mesh);
|
|
104
79
|
this.world.registerObject(this);
|
|
105
80
|
}
|
|
81
|
+
/** Exact live count for a JS-driven emitter; for a GPU-driven one, there is no CPU-visible live
|
|
82
|
+
* count to report - `capacity` is the honest upper bound reported instead. */
|
|
106
83
|
get particleCount() {
|
|
107
|
-
return this.
|
|
84
|
+
return this.driver.particleCount;
|
|
108
85
|
}
|
|
86
|
+
/** The buffer's current allocated size - grows over time for a JS-driven emitter, fixed at
|
|
87
|
+
* construction for a GPU-driven one (no capacityStep growth path there). */
|
|
109
88
|
get particleCapacity() {
|
|
110
|
-
return this.
|
|
89
|
+
return this.driver.particleCapacity;
|
|
90
|
+
}
|
|
91
|
+
// The active driver's own mesh - a thin passthrough, not a second source of truth (this.driver.mesh
|
|
92
|
+
// is authoritative; nothing caches or duplicates it here).
|
|
93
|
+
get mesh() {
|
|
94
|
+
return this.driver.mesh;
|
|
111
95
|
}
|
|
112
96
|
/**
|
|
113
97
|
* Builds an emitter into `world` from a precompiled effect - the low-level building block behind
|
|
@@ -116,10 +100,34 @@ export class FXEmitter extends Object3D {
|
|
|
116
100
|
* runtime path is running whole projects through `FXEffect`.
|
|
117
101
|
*/
|
|
118
102
|
static fromArtifacts(render, behavior, options = {}, world = FXWorld.getDefault()) {
|
|
103
|
+
if (options.expectedCapacity !== undefined) {
|
|
104
|
+
// Sizes the Float32Array buffers - a fractional length is a RangeError. Doubles as the
|
|
105
|
+
// fixed GPU capacity when a GPU kernel is present (no capacityStep growth for that path).
|
|
106
|
+
assertValidPositiveInteger(options.expectedCapacity, "FXEmitter.constructor.options.expectedCapacity");
|
|
107
|
+
}
|
|
108
|
+
if (options.capacityStep !== undefined) {
|
|
109
|
+
assertValidPositiveInteger(options.capacityStep, "FXEmitter.constructor.options.capacityStep");
|
|
110
|
+
}
|
|
119
111
|
const material = new FXArtifactMaterial(render, options.textures);
|
|
120
|
-
const holder = new FXSimulationHolder(behavior);
|
|
121
112
|
const baseGeometry = resolveGeometrySource(render.geometry, options.geometries ?? {});
|
|
122
|
-
|
|
113
|
+
const capacity = options.expectedCapacity ?? EMITTER_DEFAULT_EXPECTED_CAPACITY;
|
|
114
|
+
// The one place this emitter's backend is chosen, ever (never switched mid-playback). A real
|
|
115
|
+
// driver-level failure (program link, buffer/VAO setup) despite "Try GPU simulation" having
|
|
116
|
+
// compiled and the live render tier already being "standard" falls back to the mandatory JS
|
|
117
|
+
// artifact in place, no reload loop, matching the editor's sceneCoordinator.ts own forced-
|
|
118
|
+
// WebGL1-creation-failure fallback shape on the render side.
|
|
119
|
+
let driver;
|
|
120
|
+
if (options.gpuKernel !== undefined && options.renderer !== undefined) {
|
|
121
|
+
try {
|
|
122
|
+
driver = new FXGPUEmitterDriver(options.gpuKernel, options.renderer, material, capacity, baseGeometry);
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
console.warn("FXEmitter: GPU (transform-feedback) behavior setup failed at runtime; falling back " +
|
|
126
|
+
"to the JS behavior artifact for this emitter.", error);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
driver ?? (driver = new FXJSEmitterDriver(behavior, material, capacity, options.capacityStep ?? EMITTER_DEFAULT_CAPACITY_STEP, baseGeometry));
|
|
130
|
+
return new FXEmitter(driver, material, options, world);
|
|
123
131
|
}
|
|
124
132
|
/**
|
|
125
133
|
* @internal Ticks this emitter one frame. Driven by {@link FXWorld.update}, which advances the
|
|
@@ -127,12 +135,12 @@ export class FXEmitter extends Object3D {
|
|
|
127
135
|
*/
|
|
128
136
|
onWorldTick(deltaTime, elapsedTime) {
|
|
129
137
|
this.onRendering(deltaTime);
|
|
130
|
-
this.
|
|
131
|
-
this.
|
|
138
|
+
this.artifactMaterial.setElapsedTime(elapsedTime);
|
|
139
|
+
this.artifactMaterial.setDeltaTime(deltaTime);
|
|
132
140
|
// emitterTransform.velocity/angularVelocity are always length 3 (see its declaration) -
|
|
133
141
|
// computed once per tick in onRendering -> tick.
|
|
134
|
-
this.
|
|
135
|
-
this.
|
|
142
|
+
this.artifactMaterial.setObjectVelocity(this.emitterTransform.velocity);
|
|
143
|
+
this.artifactMaterial.setObjectAngularVelocity(this.emitterTransform.angularVelocity);
|
|
136
144
|
}
|
|
137
145
|
/** Unregisters, removes from the scene, disposes mesh and material. Idempotent. */
|
|
138
146
|
destroy() {
|
|
@@ -141,18 +149,20 @@ export class FXEmitter extends Object3D {
|
|
|
141
149
|
}
|
|
142
150
|
this.destroyed = true;
|
|
143
151
|
this.world.unregisterObject(this);
|
|
144
|
-
// Drop scheduled work
|
|
152
|
+
// Drop scheduled work so nothing references the driver's buffers after teardown.
|
|
145
153
|
this.pendingBursts.length = 0;
|
|
146
154
|
this.activePlays.length = 0;
|
|
147
|
-
|
|
148
|
-
//
|
|
149
|
-
|
|
150
|
-
this.
|
|
151
|
-
this.mesh.destroy();
|
|
155
|
+
// The material owns no per-emitter GPU resources; the driver teardown disposes the instanced
|
|
156
|
+
// geometry and the mounted material (plus, for a GPU-driven emitter, the compiled program+VAO).
|
|
157
|
+
this.artifactMaterial.destroy();
|
|
158
|
+
this.driver.destroy();
|
|
152
159
|
this.removeFromParent();
|
|
153
160
|
}
|
|
154
161
|
/**
|
|
155
|
-
* Spawns `count` particles immediately, or after `options.delay`.
|
|
162
|
+
* Spawns `count` particles immediately, or after `options.delay`. An immediate burst seeds its
|
|
163
|
+
* particles from this emitter's pose and parameter values at the moment of the call, on either
|
|
164
|
+
* backend - so moving the emitter between two bursts of one frame spawns them in two places.
|
|
165
|
+
* A delayed burst spawns from the pose and values of the tick it expires on.
|
|
156
166
|
* @returns Handler for {@link stop} to cancel a pending delayed burst.
|
|
157
167
|
*/
|
|
158
168
|
burst(count, options = {}) {
|
|
@@ -243,18 +253,38 @@ export class FXEmitter extends Object3D {
|
|
|
243
253
|
remaining -= deltaTime;
|
|
244
254
|
}
|
|
245
255
|
}
|
|
246
|
-
/**
|
|
256
|
+
/**
|
|
257
|
+
* Kills all live particles and cancels all active plays and pending bursts. For a GPU-driven
|
|
258
|
+
* emitter this is a known-narrower reset (see the mesh's own `drop()` doc comment): there is no
|
|
259
|
+
* CPU-visible per-particle state to instantly zero, so already-alive particles keep aging out
|
|
260
|
+
* naturally rather than vanishing on this call's own frame - only the spawn cursor and pending
|
|
261
|
+
* schedule reset immediately.
|
|
262
|
+
*/
|
|
247
263
|
reset() {
|
|
248
264
|
if (this.destroyed) {
|
|
249
265
|
return;
|
|
250
266
|
}
|
|
251
267
|
this.pendingBursts.length = 0;
|
|
252
268
|
this.activePlays.length = 0;
|
|
253
|
-
this.
|
|
269
|
+
this.driver.reset();
|
|
254
270
|
// A restarted effect should not report a teleport spike from wherever the emitter was last
|
|
255
271
|
// posed - drop the velocity/angular-velocity baseline along with the particles.
|
|
256
272
|
this.motionTracker.reset();
|
|
257
273
|
}
|
|
274
|
+
/**
|
|
275
|
+
* For a timeline that loops back to its start - {@link FXEffect}'s own, or a tool's that drives
|
|
276
|
+
* the emitter itself: cancels the active plays and pending bursts, as {@link stop} does, and
|
|
277
|
+
* forgets the motion baseline - the pose jumps from the lap's end to its start, which is no
|
|
278
|
+
* motion - but leaves live particles to finish their lives, so the loop runs on seamlessly.
|
|
279
|
+
* Unlike {@link reset}, which clears them.
|
|
280
|
+
*/
|
|
281
|
+
loopTimeline() {
|
|
282
|
+
if (this.destroyed) {
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
this.stop();
|
|
286
|
+
this.motionTracker.reset();
|
|
287
|
+
}
|
|
258
288
|
/**
|
|
259
289
|
* Scrubs live parameter values into uniform/binding slots by name, picked up next frame - the
|
|
260
290
|
* runtime's value channel for a non-structural edit. Unknown name = safe no-op; never recompiles.
|
|
@@ -265,48 +295,45 @@ export class FXEmitter extends Object3D {
|
|
|
265
295
|
return;
|
|
266
296
|
}
|
|
267
297
|
if (values.uniforms !== undefined) {
|
|
268
|
-
this.
|
|
298
|
+
this.artifactMaterial.applyUniformValues(values.uniforms);
|
|
269
299
|
}
|
|
270
300
|
if (values.bindings !== undefined) {
|
|
271
|
-
this.
|
|
301
|
+
this.driver.applyBindingValues(values.bindings);
|
|
272
302
|
}
|
|
273
303
|
}
|
|
274
304
|
/**
|
|
275
|
-
*
|
|
276
|
-
*
|
|
305
|
+
* Swaps only this emitter's render half in place - a new shader, geometry primitive/mesh, or
|
|
306
|
+
* shadow flags - leaving the behavior driver, particle buffers, instance count, and playback
|
|
307
|
+
* schedule completely untouched (unlike a structural edit that reaches {@link fromArtifacts},
|
|
308
|
+
* this never resets a running effect). The editor calls this instead of rebuilding whenever a
|
|
309
|
+
* graph edit only changed the render half.
|
|
310
|
+
*
|
|
311
|
+
* @throws If the new render artifact's attribute reads disagree with this emitter's already-
|
|
312
|
+
* allocated buffers (a genuine layout change - see {@link FXEmitterDriver.applyRenderArtifact}).
|
|
313
|
+
* This emitter is left exactly as it was before the call in that case; the caller must fall back
|
|
314
|
+
* to a fresh {@link fromArtifacts} instead, since only a new driver can safely resize buffers.
|
|
277
315
|
*/
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
const existing = components.get(name);
|
|
282
|
-
if (existing !== undefined && existing !== count) {
|
|
283
|
-
throw new Error(`FXEmitter: attribute "${name}" is written and read with conflicting widths ` +
|
|
284
|
-
`(${existing.toString()} vs ${count.toString()}); the behavior attributeWrites and ` +
|
|
285
|
-
`the render attributeReads must agree`);
|
|
286
|
-
}
|
|
287
|
-
components.set(name, count);
|
|
288
|
-
};
|
|
289
|
-
for (const write of this.simulation?.attributeWrites ?? []) {
|
|
290
|
-
add(write.name, write.components);
|
|
316
|
+
applyRenderArtifact(render, options = {}) {
|
|
317
|
+
if (this.destroyed) {
|
|
318
|
+
return;
|
|
291
319
|
}
|
|
292
|
-
|
|
293
|
-
|
|
320
|
+
const material = new FXArtifactMaterial(render, options.textures);
|
|
321
|
+
const baseGeometry = resolveGeometrySource(render.geometry, options.geometries ?? {});
|
|
322
|
+
// Throws (leaving this emitter untouched) before anything below mutates it - see this
|
|
323
|
+
// method's own @throws doc.
|
|
324
|
+
this.driver.applyRenderArtifact(material, baseGeometry);
|
|
325
|
+
this.artifactMaterial = material;
|
|
326
|
+
this.mesh.receiveShadow = options.receiveShadow ?? EMITTER_DEFAULT_RECEIVE_SHADOW;
|
|
327
|
+
const previousDepthMaterial = this.mesh.customDepthMaterial;
|
|
328
|
+
if (options.castShadow ?? EMITTER_DEFAULT_CAST_SHADOW) {
|
|
329
|
+
this.mesh.castShadow = true;
|
|
330
|
+
this.mesh.customDepthMaterial = material.buildThreeDepthMaterial(this.driver.varyings);
|
|
294
331
|
}
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
const properties = {
|
|
299
|
-
[FX_CORE_POSITION]: CORE_POSITION_TYPE,
|
|
300
|
-
[FX_CORE_LIFECYCLE]: CORE_LIFECYCLE_TYPE,
|
|
301
|
-
};
|
|
302
|
-
for (const [name, count] of components) {
|
|
303
|
-
properties[`${ATTRIBUTE_BUFFER_PREFIX}${name}`] = {
|
|
304
|
-
glslTypeName: GLSL_TYPE_BY_COMPONENTS[count],
|
|
305
|
-
bufferSize: count,
|
|
306
|
-
instantiable: true,
|
|
307
|
-
};
|
|
332
|
+
else {
|
|
333
|
+
this.mesh.castShadow = false;
|
|
334
|
+
this.mesh.customDepthMaterial = undefined;
|
|
308
335
|
}
|
|
309
|
-
|
|
336
|
+
previousDepthMaterial?.dispose();
|
|
310
337
|
}
|
|
311
338
|
setSortFraction(value, context) {
|
|
312
339
|
assertValidNonNegativeNumber(value, context);
|
|
@@ -316,31 +343,10 @@ export class FXEmitter extends Object3D {
|
|
|
316
343
|
// Throws when the behavior artifact cannot seed births. Checked at the scheduling points, NOT in
|
|
317
344
|
// the frame tick - a throw from the world tick would take the host's render loop down.
|
|
318
345
|
assertCanSpawn(context) {
|
|
319
|
-
if (
|
|
346
|
+
if (!this.driver.canSpawn) {
|
|
320
347
|
throw new Error(`${context}: this behavior artifact cannot seed births (update-only)`);
|
|
321
348
|
}
|
|
322
349
|
}
|
|
323
|
-
// Maps each kernel state-buffer name to its backing Float32Array. Cached across ticks/bursts and
|
|
324
|
-
// rebuilt only when the mesh's buffer set changes (`bufferVersion` - capacity growth).
|
|
325
|
-
kernelBuffers() {
|
|
326
|
-
const version = this.mesh.bufferVersion;
|
|
327
|
-
if (this.kernelBuffersCache !== undefined && this.kernelBuffersCacheVersion === version) {
|
|
328
|
-
return this.kernelBuffersCache;
|
|
329
|
-
}
|
|
330
|
-
const buffers = {
|
|
331
|
-
[FX_CORE_POSITION]: this.mesh.propertyBuffers[FX_CORE_POSITION].array,
|
|
332
|
-
[FX_CORE_LIFECYCLE]: this.mesh.propertyBuffers[FX_CORE_LIFECYCLE].array,
|
|
333
|
-
};
|
|
334
|
-
for (const buffer of this.simulation?.attributeBuffers ?? []) {
|
|
335
|
-
const attribute = this.mesh.propertyBuffers[`${ATTRIBUTE_BUFFER_PREFIX}${buffer.name}`];
|
|
336
|
-
if (attribute !== undefined) {
|
|
337
|
-
buffers[buffer.name] = attribute.array;
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
this.kernelBuffersCache = buffers;
|
|
341
|
-
this.kernelBuffersCacheVersion = version;
|
|
342
|
-
return buffers;
|
|
343
|
-
}
|
|
344
350
|
// The emitter's world matrix as the reused snapshot the kernel reads. It is the mesh's
|
|
345
351
|
// `matrixWorld` (same matrix the shader and depth sort see), so a world-space graph stays
|
|
346
352
|
// consistent under any transform. `Matrix4.elements` is already column-major.
|
|
@@ -353,16 +359,6 @@ export class FXEmitter extends Object3D {
|
|
|
353
359
|
}
|
|
354
360
|
return transform;
|
|
355
361
|
}
|
|
356
|
-
markBuffersNeedUpdate(names) {
|
|
357
|
-
for (const name of names) {
|
|
358
|
-
const isCore = name === FX_CORE_POSITION || name === FX_CORE_LIFECYCLE;
|
|
359
|
-
const key = isCore ? name : `${ATTRIBUTE_BUFFER_PREFIX}${name}`;
|
|
360
|
-
const attribute = this.mesh.propertyBuffers[key];
|
|
361
|
-
if (attribute !== undefined) {
|
|
362
|
-
attribute.needsUpdate = true;
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
362
|
tick(deltaTime) {
|
|
367
363
|
// World-space velocity/angular velocity since the previous tick, computed once here and not
|
|
368
364
|
// inside worldTransform() - the burst/play loops below and the update phase can each call
|
|
@@ -390,16 +386,13 @@ export class FXEmitter extends Object3D {
|
|
|
390
386
|
this.emitterTransform.angularVelocity[0] = motion.angularVelocity[0];
|
|
391
387
|
this.emitterTransform.angularVelocity[1] = motion.angularVelocity[1];
|
|
392
388
|
this.emitterTransform.angularVelocity[2] = motion.angularVelocity[2];
|
|
393
|
-
//
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
for (let i = 0; i < instanceCount; i++) {
|
|
399
|
-
array[i * itemSize + FX_AGE] += deltaTime;
|
|
400
|
-
}
|
|
389
|
+
// Paused (dt == 0): skip the simulation step entirely - no JS age/kernel update, no GPU
|
|
390
|
+
// transform-feedback dispatch, no schedule advance - the emitter keeps rendering whatever
|
|
391
|
+
// particle state the last active tick left behind.
|
|
392
|
+
if (deltaTime <= 0) {
|
|
393
|
+
return;
|
|
401
394
|
}
|
|
402
|
-
this.
|
|
395
|
+
this.driver.beginTick(deltaTime);
|
|
403
396
|
for (let i = this.pendingBursts.length - 1; i >= 0; i--) {
|
|
404
397
|
const pending = this.pendingBursts[i];
|
|
405
398
|
pending.delay -= deltaTime;
|
|
@@ -438,27 +431,10 @@ export class FXEmitter extends Object3D {
|
|
|
438
431
|
this.activePlays.splice(i, 1);
|
|
439
432
|
}
|
|
440
433
|
}
|
|
441
|
-
|
|
442
|
-
return;
|
|
443
|
-
}
|
|
444
|
-
if (this.simulation !== undefined) {
|
|
445
|
-
// Motion integration lives in the graph (an `integrate-motion` node writes
|
|
446
|
-
// `position += velocity * dt`), not in this host loop.
|
|
447
|
-
this.simulation.update(this.kernelBuffers(), this.mesh.instanceCount, deltaTime, this.worldTransform());
|
|
448
|
-
this.markBuffersNeedUpdate(this.simulation.updateWrittenBuffers);
|
|
449
|
-
}
|
|
450
|
-
// Age (written above) must reach the GPU this frame; the update phase's buffers were flagged already.
|
|
451
|
-
this.mesh.propertyBuffers[FX_CORE_LIFECYCLE].needsUpdate = true;
|
|
434
|
+
this.driver.endTick(deltaTime, this.worldTransform());
|
|
452
435
|
}
|
|
453
436
|
spawnBurst(count) {
|
|
454
|
-
|
|
455
|
-
this.mesh.createInstances(count);
|
|
456
|
-
const instanceEnd = this.mesh.instanceCount;
|
|
457
|
-
// Re-checked (defensive): no spawn function would leave the freshly zeroed rows born dead.
|
|
458
|
-
if (this.simulation?.canSpawn === true) {
|
|
459
|
-
this.simulation.spawn(this.kernelBuffers(), instanceBegin, instanceEnd - instanceBegin, this.worldTransform());
|
|
460
|
-
this.markBuffersNeedUpdate(this.simulation.spawnWrittenBuffers);
|
|
461
|
-
}
|
|
437
|
+
this.driver.spawn(count, this.worldTransform());
|
|
462
438
|
}
|
|
463
439
|
}
|
|
464
440
|
// Returned by post-destroy `burst`/`play` - never matches a scheduled handler.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { GLTypeInfo } from "../instancedParticle/glTypeInfo.js";
|
|
2
|
+
/** A named width declaration - shape shared by `FXAttributeDecl` (`components`) and a mapped
|
|
3
|
+
* `FXBufferLayout` (`stride`), the two concrete lists {@link mergeAttributeWidths} merges. */
|
|
4
|
+
interface FXNamedWidth {
|
|
5
|
+
readonly name: string;
|
|
6
|
+
readonly components: number;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Merges width-declaring attribute lists (a driver's own state buffers, the render artifact's
|
|
10
|
+
* `attributeReads`) into one name -> component-count map, throwing on a genuine cross-source
|
|
11
|
+
* disagreement rather than sizing a buffer wrong. Shared by both drivers' constructors so the
|
|
12
|
+
* validation itself never drifts between them, even though each driver's own input list does not.
|
|
13
|
+
*/
|
|
14
|
+
export declare function mergeAttributeWidths(...sources: readonly (readonly FXNamedWidth[])[]): ReadonlyMap<string, number>;
|
|
15
|
+
/** Builds a driver's full varyings map (the two core buffers plus one `fx_<name>` per attribute)
|
|
16
|
+
* from a merged width map - the input to both `FXArtifactMaterial.buildThreeMaterial` and either
|
|
17
|
+
* mesh class's constructor. */
|
|
18
|
+
export declare function varyingsFromComponents(components: ReadonlyMap<string, number>): Record<string, GLTypeInfo>;
|
|
19
|
+
/**
|
|
20
|
+
* Whether two varyings maps declare the exact same names at the exact same width - the guard
|
|
21
|
+
* `applyRenderArtifact` needs before reusing already-allocated buffers in place: a new render
|
|
22
|
+
* artifact whose attribute reads disagree with what is already allocated (a new name, or an
|
|
23
|
+
* existing name at a different width) cannot be applied without a full rebuild.
|
|
24
|
+
*/
|
|
25
|
+
export declare function sameVaryings(current: Record<string, GLTypeInfo>, next: Record<string, GLTypeInfo>): boolean;
|
|
26
|
+
export {};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { FX_CORE_LIFECYCLE, FX_CORE_LIFECYCLE_STRIDE, FX_CORE_POSITION, FX_CORE_POSITION_STRIDE, meshPropertyKeyFor, } from "../coreLayout.js";
|
|
2
|
+
const CORE_POSITION_TYPE = {
|
|
3
|
+
glslTypeName: "vec3",
|
|
4
|
+
bufferSize: FX_CORE_POSITION_STRIDE,
|
|
5
|
+
instantiable: true,
|
|
6
|
+
};
|
|
7
|
+
const CORE_LIFECYCLE_TYPE = {
|
|
8
|
+
glslTypeName: "vec3",
|
|
9
|
+
bufferSize: FX_CORE_LIFECYCLE_STRIDE,
|
|
10
|
+
instantiable: true,
|
|
11
|
+
};
|
|
12
|
+
// GLSL type of a float attribute, indexed by component count.
|
|
13
|
+
const GLSL_TYPE_BY_COMPONENTS = ["", "float", "vec2", "vec3", "vec4"];
|
|
14
|
+
/**
|
|
15
|
+
* Merges width-declaring attribute lists (a driver's own state buffers, the render artifact's
|
|
16
|
+
* `attributeReads`) into one name -> component-count map, throwing on a genuine cross-source
|
|
17
|
+
* disagreement rather than sizing a buffer wrong. Shared by both drivers' constructors so the
|
|
18
|
+
* validation itself never drifts between them, even though each driver's own input list does not.
|
|
19
|
+
*/
|
|
20
|
+
export function mergeAttributeWidths(...sources) {
|
|
21
|
+
const components = new Map();
|
|
22
|
+
for (const source of sources) {
|
|
23
|
+
for (const decl of source) {
|
|
24
|
+
const existing = components.get(decl.name);
|
|
25
|
+
if (existing !== undefined && existing !== decl.components) {
|
|
26
|
+
throw new Error(`FXEmitter: attribute "${decl.name}" is declared with conflicting widths ` +
|
|
27
|
+
`(${existing.toString()} vs ${decl.components.toString()}) across its sources`);
|
|
28
|
+
}
|
|
29
|
+
components.set(decl.name, decl.components);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return components;
|
|
33
|
+
}
|
|
34
|
+
/** Builds a driver's full varyings map (the two core buffers plus one `fx_<name>` per attribute)
|
|
35
|
+
* from a merged width map - the input to both `FXArtifactMaterial.buildThreeMaterial` and either
|
|
36
|
+
* mesh class's constructor. */
|
|
37
|
+
export function varyingsFromComponents(components) {
|
|
38
|
+
const varyings = {
|
|
39
|
+
[FX_CORE_POSITION]: CORE_POSITION_TYPE,
|
|
40
|
+
[FX_CORE_LIFECYCLE]: CORE_LIFECYCLE_TYPE,
|
|
41
|
+
};
|
|
42
|
+
for (const [name, count] of components) {
|
|
43
|
+
varyings[meshPropertyKeyFor(name)] = {
|
|
44
|
+
glslTypeName: GLSL_TYPE_BY_COMPONENTS[count],
|
|
45
|
+
bufferSize: count,
|
|
46
|
+
instantiable: true,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
return varyings;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Whether two varyings maps declare the exact same names at the exact same width - the guard
|
|
53
|
+
* `applyRenderArtifact` needs before reusing already-allocated buffers in place: a new render
|
|
54
|
+
* artifact whose attribute reads disagree with what is already allocated (a new name, or an
|
|
55
|
+
* existing name at a different width) cannot be applied without a full rebuild.
|
|
56
|
+
*/
|
|
57
|
+
export function sameVaryings(current, next) {
|
|
58
|
+
const currentNames = Object.keys(current);
|
|
59
|
+
if (currentNames.length !== Object.keys(next).length) {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
return currentNames.every((name) => {
|
|
63
|
+
if (!Object.prototype.hasOwnProperty.call(next, name)) {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
const nextEntry = next[name];
|
|
67
|
+
return (nextEntry.glslTypeName === current[name].glslTypeName &&
|
|
68
|
+
nextEntry.bufferSize === current[name].bufferSize);
|
|
69
|
+
});
|
|
70
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { BufferGeometry, Texture } from "three";
|
|
1
|
+
import type { BufferGeometry, Texture, WebGLRenderer } from "three";
|
|
2
|
+
import type { FXParticleKernelArtifact } from "../artifact/FXArtifact.js";
|
|
2
3
|
import type { FXEmitterOptions } from "./FXEmitter.Internal.js";
|
|
3
4
|
/**
|
|
4
5
|
* Options for {@link FXEmitter.fromArtifacts}: the emitter options plus external textures by slot -
|
|
@@ -10,4 +11,14 @@ import type { FXEmitterOptions } from "./FXEmitter.Internal.js";
|
|
|
10
11
|
export interface FXFromArtifactsOptions extends Partial<FXEmitterOptions> {
|
|
11
12
|
readonly textures?: Record<string, Texture>;
|
|
12
13
|
readonly geometries?: Record<string, BufferGeometry>;
|
|
14
|
+
/**
|
|
15
|
+
* The standard-tier (WebGL2, transform-feedback) behavior artifact, present only when the
|
|
16
|
+
* graph's spawn node had "Try GPU simulation" on and the graph compiled to GLSL. `renderer` is
|
|
17
|
+
* required alongside it - without one, or if GPU program setup itself throws (a real
|
|
18
|
+
* driver-level failure), the emitter falls back to the mandatory JS `behavior` artifact in
|
|
19
|
+
* place, never a hard error. `expectedCapacity` becomes this emitter's fixed GPU particle
|
|
20
|
+
* capacity (no growth); `capacityStep` is meaningless for a GPU-driven emitter and is ignored.
|
|
21
|
+
*/
|
|
22
|
+
readonly gpuKernel?: FXParticleKernelArtifact;
|
|
23
|
+
readonly renderer?: WebGLRenderer;
|
|
13
24
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|