sparcoon 0.7.3 → 0.9.1
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 +124 -76
- package/dist/artifact/FXArtifact.d.ts +120 -0
- package/dist/artifact/FXArtifact.js +1 -0
- package/dist/coreLayout.d.ts +22 -0
- package/dist/coreLayout.js +32 -0
- package/dist/editor/index.d.ts +8 -0
- package/dist/editor/index.js +17 -0
- package/dist/effect/FXEffect.d.ts +77 -0
- package/dist/effect/FXEffect.js +454 -0
- package/dist/effect/FXEffectSampling.Internal.d.ts +24 -0
- package/dist/effect/FXEffectSampling.Internal.js +165 -0
- package/dist/effect/FXEffectSpec.d.ts +115 -0
- package/dist/effect/FXEffectSpec.js +1 -0
- package/dist/emitter/FXEmitter.Internal.d.ts +23 -58
- package/dist/emitter/FXEmitter.Internal.js +13 -29
- package/dist/emitter/FXEmitter.d.ts +54 -46
- package/dist/emitter/FXEmitter.js +357 -186
- package/dist/emitter/FXFromArtifactsOptions.d.ts +13 -0
- package/dist/emitter/FXFromArtifactsOptions.js +1 -0
- package/dist/emitter/FXObjectMotion.Internal.d.ts +33 -0
- package/dist/emitter/FXObjectMotion.Internal.js +87 -0
- package/dist/emitter/FXSimulationHolder.d.ts +1 -0
- package/dist/emitter/FXSimulationHolder.js +43 -0
- package/dist/index.d.ts +11 -39
- package/dist/index.js +13 -36
- package/dist/instancedParticle/FXInstancedParticle.d.ts +1 -22
- package/dist/instancedParticle/FXInstancedParticle.js +145 -85
- package/dist/instancedParticle/glTypeInfo.d.ts +5 -0
- package/dist/instancedParticle/glTypeInfo.js +3 -0
- package/dist/instancedParticle/primitiveGeometry.d.ts +10 -0
- package/dist/instancedParticle/primitiveGeometry.js +30 -0
- package/dist/miscellaneous/asserts.d.ts +1 -18
- package/dist/miscellaneous/asserts.js +21 -28
- package/dist/miscellaneous/fxMath.d.ts +26 -0
- package/dist/miscellaneous/fxMath.js +261 -0
- package/dist/miscellaneous/math.d.ts +2 -7
- package/dist/miscellaneous/math.js +8 -9
- package/dist/miscellaneous/texture/fxDataTexture.d.ts +26 -0
- package/dist/miscellaneous/texture/fxDataTexture.js +20 -0
- package/dist/render/FXArtifactMaterial.Internal.d.ts +16 -0
- package/dist/render/FXArtifactMaterial.Internal.js +944 -0
- package/dist/render/FXArtifactMaterial.d.ts +1 -0
- package/dist/render/FXArtifactMaterial.js +79 -0
- package/dist/render/FXMaterialOptions.d.ts +27 -0
- package/dist/render/FXMaterialOptions.js +5 -0
- package/dist/render/FXMesh.d.ts +47 -0
- package/dist/render/FXMesh.js +99 -0
- package/dist/render/FXMeshMaterial.d.ts +40 -0
- package/dist/render/FXMeshMaterial.js +77 -0
- package/dist/world/FXWorld.d.ts +52 -0
- package/dist/world/FXWorld.js +104 -0
- package/package.json +49 -29
- package/dist/_virtual/2d.js +0 -3
- package/dist/_virtual/3d.js +0 -3
- package/dist/_virtual/4d.js +0 -3
- package/dist/_virtual/mod.js +0 -5
- package/dist/_virtual/mod2.js +0 -3
- package/dist/emitter/behavior/FXBehavior.d.ts +0 -24
- package/dist/emitter/behavior/FXBehavior.js +0 -10
- package/dist/emitter/behavior/FXBehaviorDirectionalForce.d.ts +0 -23
- package/dist/emitter/behavior/FXBehaviorDirectionalForce.js +0 -72
- package/dist/emitter/behavior/FXBehaviorPointForce.d.ts +0 -36
- package/dist/emitter/behavior/FXBehaviorPointForce.js +0 -94
- package/dist/emitter/behavior/FXBehaviorScaleOverLife.d.ts +0 -24
- package/dist/emitter/behavior/FXBehaviorScaleOverLife.js +0 -55
- package/dist/emitter/behavior/FXBehaviorTorqueDamping.d.ts +0 -20
- package/dist/emitter/behavior/FXBehaviorTorqueDamping.js +0 -48
- package/dist/emitter/behavior/FXBehaviorTorqueNoise.d.ts +0 -25
- package/dist/emitter/behavior/FXBehaviorTorqueNoise.js +0 -67
- package/dist/emitter/behavior/FXBehaviorTorqueOverLife.d.ts +0 -19
- package/dist/emitter/behavior/FXBehaviorTorqueOverLife.js +0 -41
- package/dist/emitter/behavior/FXBehaviorVelocityDamping.d.ts +0 -20
- package/dist/emitter/behavior/FXBehaviorVelocityDamping.js +0 -50
- package/dist/emitter/behavior/FXBehaviorVelocityNoise.d.ts +0 -25
- package/dist/emitter/behavior/FXBehaviorVelocityNoise.js +0 -76
- package/dist/emitter/behavior/FXBehaviorVelocityOverLife.d.ts +0 -22
- package/dist/emitter/behavior/FXBehaviorVelocityOverLife.js +0 -55
- package/dist/emitter/spawn/FXSpawn.d.ts +0 -11
- package/dist/emitter/spawn/FXSpawn.js +0 -10
- package/dist/emitter/spawn/FXSpawnBox.d.ts +0 -18
- package/dist/emitter/spawn/FXSpawnBox.js +0 -50
- package/dist/emitter/spawn/FXSpawnLifetime.d.ts +0 -17
- package/dist/emitter/spawn/FXSpawnLifetime.js +0 -44
- package/dist/emitter/spawn/FXSpawnOffset.d.ts +0 -21
- package/dist/emitter/spawn/FXSpawnOffset.js +0 -49
- package/dist/emitter/spawn/FXSpawnPoint.d.ts +0 -18
- package/dist/emitter/spawn/FXSpawnPoint.js +0 -46
- package/dist/emitter/spawn/FXSpawnRotation.d.ts +0 -17
- package/dist/emitter/spawn/FXSpawnRotation.js +0 -42
- package/dist/emitter/spawn/FXSpawnScale.d.ts +0 -22
- package/dist/emitter/spawn/FXSpawnScale.js +0 -56
- package/dist/emitter/spawn/FXSpawnSphere.d.ts +0 -36
- package/dist/emitter/spawn/FXSpawnSphere.js +0 -87
- package/dist/emitter/spawn/FXSpawnTorque.d.ts +0 -17
- package/dist/emitter/spawn/FXSpawnTorque.js +0 -42
- package/dist/emitter/spawn/FXSpawnVelocity.d.ts +0 -29
- package/dist/emitter/spawn/FXSpawnVelocity.js +0 -91
- package/dist/instancedParticle/FXInstancedParticle.Internal.d.ts +0 -2
- package/dist/instancedParticle/shared.d.ts +0 -19
- package/dist/instancedParticle/shared.js +0 -116
- package/dist/materials/FXDiffuseMaterial/FXDiffuseMaterial.Internal.d.ts +0 -24
- package/dist/materials/FXDiffuseMaterial/FXDiffuseMaterial.Internal.js +0 -275
- package/dist/materials/FXDiffuseMaterial/FXDiffuseMaterial.d.ts +0 -75
- package/dist/materials/FXDiffuseMaterial/FXDiffuseMaterial.js +0 -63
- package/dist/materials/FXMaterial/FXBlending.Internal.d.ts +0 -24
- package/dist/materials/FXMaterial/FXBlending.Internal.js +0 -64
- package/dist/materials/FXMaterial/FXDepthMaterial.Internal.d.ts +0 -4
- package/dist/materials/FXMaterial/FXDepthMaterial.Internal.js +0 -144
- package/dist/materials/FXMaterial/FXDistanceMaterial.Internal.d.ts +0 -4
- package/dist/materials/FXMaterial/FXDistanceMaterial.Internal.js +0 -175
- package/dist/materials/FXMaterial/FXMaterial.d.ts +0 -70
- package/dist/materials/FXMaterial/FXMaterial.js +0 -51
- package/dist/materials/FXUnlitMaterial/FXUnlitMaterial.Internal.d.ts +0 -7
- package/dist/materials/FXUnlitMaterial/FXUnlitMaterial.Internal.js +0 -103
- package/dist/materials/FXUnlitMaterial/FXUnlitMaterial.d.ts +0 -7
- package/dist/materials/FXUnlitMaterial/FXUnlitMaterial.js +0 -21
- package/dist/miscellaneous/color/FXColor.Internal.d.ts +0 -7
- package/dist/miscellaneous/color/FXColor.Internal.js +0 -42
- package/dist/miscellaneous/color/FXColor.d.ts +0 -234
- package/dist/miscellaneous/color/FXColor.js +0 -636
- package/dist/miscellaneous/curve/FXCurve1D.Internal.d.ts +0 -20
- package/dist/miscellaneous/curve/FXCurve1D.d.ts +0 -17
- package/dist/miscellaneous/curve/FXCurve1D.js +0 -94
- package/dist/miscellaneous/miscellaneous.d.ts +0 -34
- package/dist/miscellaneous/miscellaneous.js +0 -85
- package/dist/miscellaneous/texture/FXTextureView.Internal.d.ts +0 -49
- package/dist/miscellaneous/texture/FXTextureView.Internal.js +0 -6
- package/dist/miscellaneous/texture/FXTextureView.d.ts +0 -72
- package/dist/miscellaneous/texture/FXTextureView.js +0 -234
- package/dist/miscellaneous/webglCapabilities.d.ts +0 -1
- package/dist/miscellaneous/webglCapabilities.js +0 -45
- package/dist/node_modules/fast-simplex-noise/lib/2d.js +0 -93
- package/dist/node_modules/fast-simplex-noise/lib/3d.js +0 -142
- package/dist/node_modules/fast-simplex-noise/lib/4d.js +0 -196
- package/dist/node_modules/fast-simplex-noise/lib/mod.js +0 -24
- package/dist/nodes/FXNode.d.ts +0 -5
- package/dist/nodes/FXNode.js +0 -10
- package/dist/nodes/blending/FXNodeBlending.d.ts +0 -6
- package/dist/nodes/blending/FXNodeBlending.js +0 -9
- package/dist/nodes/blending/FXNodeLightnessBlendingMask.d.ts +0 -19
- package/dist/nodes/blending/FXNodeLightnessBlendingMask.js +0 -65
- package/dist/nodes/color/FXNodeColor.d.ts +0 -6
- package/dist/nodes/color/FXNodeColor.js +0 -9
- package/dist/nodes/color/FXNodeColorOverLife.d.ts +0 -22
- package/dist/nodes/color/FXNodeColorOverLife.js +0 -64
- package/dist/nodes/color/FXNodeSphericalClip.d.ts +0 -21
- package/dist/nodes/color/FXNodeSphericalClip.js +0 -54
- package/dist/nodes/normal/FXNodeFlatNormal.d.ts +0 -6
- package/dist/nodes/normal/FXNodeFlatNormal.js +0 -16
- package/dist/nodes/normal/FXNodeNormal.d.ts +0 -6
- package/dist/nodes/normal/FXNodeNormal.js +0 -9
- package/dist/nodes/normal/FXNodeSphericalNormal.d.ts +0 -6
- package/dist/nodes/normal/FXNodeSphericalNormal.js +0 -23
- package/dist/nodes/texture/FXNodeAnimatedTexture.d.ts +0 -52
- package/dist/nodes/texture/FXNodeAnimatedTexture.js +0 -151
- package/dist/nodes/texture/FXNodeStaticTexture.d.ts +0 -20
- package/dist/nodes/texture/FXNodeStaticTexture.js +0 -67
- package/dist/nodes/texture/FXNodeTexture.d.ts +0 -6
- package/dist/nodes/texture/FXNodeTexture.js +0 -9
|
@@ -1,135 +1,193 @@
|
|
|
1
|
-
import { Object3D, Vector3 } from
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
|
|
1
|
+
import { Object3D, Quaternion, Vector3 } from "three";
|
|
2
|
+
import { FXWorld } from "../world/FXWorld.js";
|
|
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
|
+
import { resolveGeometrySource } from "../instancedParticle/primitiveGeometry.js";
|
|
7
|
+
import { assertValidNonNegativeNumber, assertValidPositiveInteger, assertValidPositiveNumber, } from "../miscellaneous/asserts.js";
|
|
8
|
+
import { FXArtifactMaterial } from "../render/FXArtifactMaterial.js";
|
|
9
|
+
import { FXSimulationHolder } from "./FXSimulationHolder.js";
|
|
10
|
+
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
|
+
};
|
|
8
25
|
/**
|
|
9
|
-
* Particle emitter -
|
|
10
|
-
*
|
|
11
|
-
* @
|
|
12
|
-
*
|
|
13
|
-
*
|
|
26
|
+
* Particle emitter - one instanced billboard/mesh draw call, per-particle state driven by a
|
|
27
|
+
* precompiled behavior artifact and appearance by a precompiled render artifact spliced into a
|
|
28
|
+
* Three material. Built and ticked by {@link FXEffect} through an {@link FXWorld}. Not exported from
|
|
29
|
+
* the package (the runtime path is running artifacts, not hand-building emitters); reached only as
|
|
30
|
+
* the live handle returned by {@link FXEffect.getEmitter}.
|
|
14
31
|
*/
|
|
15
|
-
class FXEmitter extends Object3D {
|
|
16
|
-
/**
|
|
17
|
-
|
|
18
|
-
* @param behaviorSequence - Modules executed in order every frame for all live particles
|
|
19
|
-
* @param material - Particle material
|
|
20
|
-
* @param options - Emitter configuration
|
|
21
|
-
*/
|
|
22
|
-
constructor(_private_spawnSequence, _private_behaviorSequence, material, options = {}) {
|
|
32
|
+
export class FXEmitter extends Object3D {
|
|
33
|
+
/** @internal Use {@link FXEmitter.fromArtifacts}. */
|
|
34
|
+
constructor(material, simulation, options = {}, baseGeometry, world) {
|
|
23
35
|
super();
|
|
24
|
-
this.
|
|
25
|
-
this.
|
|
26
|
-
this.
|
|
27
|
-
this.
|
|
28
|
-
this.
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
this.nextHandler = 0;
|
|
37
|
+
this.pendingBursts = [];
|
|
38
|
+
this.activePlays = [];
|
|
39
|
+
this.sortingAccumulator = 0;
|
|
40
|
+
this.sortingCameraWorldPosition = new Vector3();
|
|
41
|
+
// The emitter's `matrixWorld` as a column-major mat4, plus its world-space velocity/angular
|
|
42
|
+
// velocity, handed to the behavior kernel each spawn/update. Mutated in place - never
|
|
43
|
+
// reallocated per frame.
|
|
44
|
+
this.emitterTransform = {
|
|
45
|
+
worldMatrix: new Array(16).fill(0),
|
|
46
|
+
velocity: [0, 0, 0],
|
|
47
|
+
angularVelocity: [0, 0, 0],
|
|
48
|
+
};
|
|
49
|
+
// Reused decompose scratch (avoids a per-tick allocation); scale is discarded.
|
|
50
|
+
this.motionPositionScratch = new Vector3();
|
|
51
|
+
this.motionQuaternionScratch = new Quaternion();
|
|
52
|
+
this.motionScaleScratch = new Vector3();
|
|
53
|
+
this.motionTracker = new FXObjectMotionTracker();
|
|
54
|
+
this.destroyed = false;
|
|
55
|
+
this.kernelBuffersCacheVersion = -1;
|
|
56
|
+
this.onRendering = (deltaTime) => {
|
|
57
|
+
this.tick(deltaTime);
|
|
58
|
+
if (this.sortCamera !== undefined && this.mesh.instanceCount > 0) {
|
|
59
|
+
// `sortFraction` is public, so clamp here too: a direct assignment above 1 would grow the accumulator unbounded.
|
|
60
|
+
this.sortingAccumulator += Math.min(Math.max(this.sortFraction, 0), 1);
|
|
61
|
+
if (this.sortingAccumulator >= 1) {
|
|
62
|
+
this.sortingAccumulator -= 1;
|
|
63
|
+
// Sort in world space to match the shader's `p_cameraDistance`, correct under any transform.
|
|
64
|
+
this.sortCamera.getWorldPosition(this.sortingCameraWorldPosition);
|
|
65
|
+
this.mesh.updateWorldMatrix(true, false);
|
|
66
|
+
this.mesh.sortByDistance(this.sortingCameraWorldPosition, this.mesh.matrixWorld);
|
|
39
67
|
}
|
|
40
68
|
}
|
|
41
69
|
};
|
|
70
|
+
this.world = world;
|
|
42
71
|
if (options.expectedCapacity !== undefined) {
|
|
43
|
-
|
|
72
|
+
// Sizes the Float32Array buffers - a fractional length is a RangeError.
|
|
73
|
+
assertValidPositiveInteger(options.expectedCapacity, "FXEmitter.constructor.options.expectedCapacity");
|
|
44
74
|
}
|
|
45
75
|
if (options.capacityStep !== undefined) {
|
|
46
|
-
|
|
76
|
+
assertValidPositiveInteger(options.capacityStep, "FXEmitter.constructor.options.capacityStep");
|
|
47
77
|
}
|
|
48
|
-
|
|
49
|
-
|
|
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);
|
|
87
|
+
if (options.receiveShadow ?? EMITTER_DEFAULT_RECEIVE_SHADOW) {
|
|
88
|
+
this.mesh.receiveShadow = true;
|
|
50
89
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
collectProperties(collectedProperties, _private_spawnSequence, "FXEmitter.constructor.spawnSequence");
|
|
55
|
-
collectProperties(collectedProperties, _private_behaviorSequence, "FXEmitter.constructor.behaviorSequence");
|
|
56
|
-
material.prepare();
|
|
57
|
-
const threeMaterial = material.buildThreeMaterial(collectedProperties);
|
|
58
|
-
this._private_mesh = new FXInstancedParticle(collectedProperties, options.expectedCapacity ?? EMITTER_DEFAULT_EXPECTED_CAPACITY, options.capacityStep ?? EMITTER_DEFAULT_CAPACITY_STEP, threeMaterial);
|
|
90
|
+
// Pay for the second (depth) ShaderMaterial only when a cast shadow is actually requested.
|
|
91
|
+
// `castShadow`/`customDepthMaterial` are inherited Object3D members; `frustumCulled` is already
|
|
92
|
+
// false on the mesh, so the shadow pass's frustum check is a no-op just like the color pass.
|
|
59
93
|
if (options.castShadow ?? EMITTER_DEFAULT_CAST_SHADOW) {
|
|
60
|
-
this.
|
|
61
|
-
this.
|
|
62
|
-
this._private_mesh.customDistanceMaterial = material.buildDistanceMaterial();
|
|
63
|
-
}
|
|
64
|
-
if (options.receiveShadow ?? EMITTER_DEFAULT_RECEIVE_SHADOW) {
|
|
65
|
-
this._private_mesh.receiveShadow = true;
|
|
94
|
+
this.mesh.castShadow = true;
|
|
95
|
+
this.mesh.customDepthMaterial = material.buildThreeDepthMaterial(this.collectedProperties);
|
|
66
96
|
}
|
|
67
97
|
this.sortCamera = options.sortCamera;
|
|
68
|
-
this.sortFraction =
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
98
|
+
this.sortFraction = EMITTER_DEFAULT_SORT_FRACTION;
|
|
99
|
+
if (options.sortFraction !== undefined) {
|
|
100
|
+
this.setSortFraction(options.sortFraction, "FXEmitter.constructor.options.sortFraction");
|
|
101
|
+
}
|
|
102
|
+
this.assertCanSpawn("FXEmitter.constructor");
|
|
103
|
+
this.add(this.mesh);
|
|
104
|
+
this.world.registerObject(this);
|
|
72
105
|
}
|
|
73
|
-
/** Number of currently alive particles */
|
|
74
106
|
get particleCount() {
|
|
75
|
-
return this.
|
|
107
|
+
return this.mesh.instanceCount;
|
|
76
108
|
}
|
|
77
|
-
/** Maximum number of particles that fit in the currently allocated buffers */
|
|
78
109
|
get particleCapacity() {
|
|
79
|
-
return this.
|
|
110
|
+
return this.mesh.instanceCapacity;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Builds an emitter into `world` from a precompiled effect - the low-level building block behind
|
|
114
|
+
* {@link FXEffect}. Exposed for editor tooling through the `sparcoon/editor` entry (live preview
|
|
115
|
+
* rebuilds a single emitter per graph edit); not part of the main package surface, where the
|
|
116
|
+
* runtime path is running whole projects through `FXEffect`.
|
|
117
|
+
*/
|
|
118
|
+
static fromArtifacts(render, behavior, options = {}, world = FXWorld.getDefault()) {
|
|
119
|
+
const material = new FXArtifactMaterial(render, options.textures);
|
|
120
|
+
const holder = new FXSimulationHolder(behavior);
|
|
121
|
+
const baseGeometry = resolveGeometrySource(render.geometry, options.geometries ?? {});
|
|
122
|
+
return new FXEmitter(material, holder, options, baseGeometry, world);
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* @internal Ticks this emitter one frame. Driven by {@link FXWorld.update}, which advances the
|
|
126
|
+
* shared clock once and passes it in as `elapsedTime`.
|
|
127
|
+
*/
|
|
128
|
+
onWorldTick(deltaTime, elapsedTime) {
|
|
129
|
+
this.onRendering(deltaTime);
|
|
130
|
+
this.material.setElapsedTime(elapsedTime);
|
|
131
|
+
this.material.setDeltaTime(deltaTime);
|
|
132
|
+
// emitterTransform.velocity/angularVelocity are always length 3 (see its declaration) -
|
|
133
|
+
// computed once per tick in onRendering -> tick.
|
|
134
|
+
this.material.setObjectVelocity(this.emitterTransform.velocity);
|
|
135
|
+
this.material.setObjectAngularVelocity(this.emitterTransform.angularVelocity);
|
|
80
136
|
}
|
|
81
|
-
/** Unregisters
|
|
137
|
+
/** Unregisters, removes from the scene, disposes mesh and material. Idempotent. */
|
|
82
138
|
destroy() {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
EMITTERS.splice(index, 1);
|
|
86
|
-
}
|
|
87
|
-
for (const module of this._private_spawnSequence) {
|
|
88
|
-
module.destroy?.();
|
|
89
|
-
}
|
|
90
|
-
for (const module of this._private_behaviorSequence) {
|
|
91
|
-
module.destroy?.();
|
|
139
|
+
if (this.destroyed) {
|
|
140
|
+
return;
|
|
92
141
|
}
|
|
93
|
-
this.
|
|
94
|
-
this.
|
|
95
|
-
|
|
96
|
-
this.
|
|
142
|
+
this.destroyed = true;
|
|
143
|
+
this.world.unregisterObject(this);
|
|
144
|
+
// Drop scheduled work and the buffer cache so nothing references the mesh's buffers after teardown.
|
|
145
|
+
this.pendingBursts.length = 0;
|
|
146
|
+
this.activePlays.length = 0;
|
|
147
|
+
this.kernelBuffersCache = undefined;
|
|
148
|
+
// The material owns no per-emitter GPU resources; the mesh teardown disposes the instanced
|
|
149
|
+
// geometry and the mounted material.
|
|
150
|
+
this.material.destroy();
|
|
151
|
+
this.mesh.destroy();
|
|
97
152
|
this.removeFromParent();
|
|
98
153
|
}
|
|
99
154
|
/**
|
|
100
|
-
* Spawns `count` particles immediately, or after
|
|
101
|
-
*
|
|
102
|
-
* @param count - Number of particles to spawn
|
|
103
|
-
* @param options - Burst options
|
|
104
|
-
* @returns Handler that can be passed to {@link stop} to cancel a pending delayed burst
|
|
155
|
+
* Spawns `count` particles immediately, or after `options.delay`.
|
|
156
|
+
* @returns Handler for {@link stop} to cancel a pending delayed burst.
|
|
105
157
|
*/
|
|
106
158
|
burst(count, options = {}) {
|
|
107
|
-
|
|
108
|
-
|
|
159
|
+
// Whole particles only: a fractional count sets a fractional `instanceCount`.
|
|
160
|
+
assertValidPositiveInteger(count, "FXEmitter.burst.count");
|
|
161
|
+
this.assertCanSpawn("FXEmitter.burst");
|
|
162
|
+
if (this.destroyed) {
|
|
163
|
+
return DEAD_HANDLER;
|
|
164
|
+
}
|
|
165
|
+
const handler = this.nextHandler++;
|
|
109
166
|
const delay = options.delay ?? 0;
|
|
110
167
|
assertValidNonNegativeNumber(delay, "FXEmitter.burst.options.delay");
|
|
111
168
|
if (delay <= 0) {
|
|
112
|
-
this.
|
|
169
|
+
this.spawnBurst(count);
|
|
113
170
|
}
|
|
114
171
|
else {
|
|
115
|
-
this.
|
|
172
|
+
this.pendingBursts.push({ handler, count, delay });
|
|
116
173
|
}
|
|
117
174
|
return handler;
|
|
118
175
|
}
|
|
119
176
|
/**
|
|
120
|
-
* Starts continuous emission at
|
|
121
|
-
*
|
|
122
|
-
* @param rate - Particles per second
|
|
123
|
-
* @param options - Play options
|
|
124
|
-
* @returns Handler that can be passed to {@link stop} to cancel
|
|
177
|
+
* Starts continuous emission at `rate` particles/second.
|
|
178
|
+
* @returns Handler for {@link stop} to cancel.
|
|
125
179
|
*/
|
|
126
180
|
play(rate, options = {}) {
|
|
127
181
|
assertValidPositiveNumber(rate, "FXEmitter.play.rate");
|
|
128
182
|
if (options.delay !== undefined) {
|
|
129
183
|
assertValidNonNegativeNumber(options.delay, "FXEmitter.play.options.delay");
|
|
130
184
|
}
|
|
131
|
-
|
|
132
|
-
this.
|
|
185
|
+
this.assertCanSpawn("FXEmitter.play");
|
|
186
|
+
if (this.destroyed) {
|
|
187
|
+
return DEAD_HANDLER;
|
|
188
|
+
}
|
|
189
|
+
const handler = this.nextHandler++;
|
|
190
|
+
this.activePlays.push({
|
|
133
191
|
handler,
|
|
134
192
|
rate,
|
|
135
193
|
duration: options.duration ?? Infinity,
|
|
@@ -140,155 +198,268 @@ class FXEmitter extends Object3D {
|
|
|
140
198
|
return handler;
|
|
141
199
|
}
|
|
142
200
|
/**
|
|
143
|
-
* Stops an active play or pending burst by handler; stops all when called with no arguments
|
|
144
|
-
*
|
|
145
|
-
* @param handler - Handler returned by {@link play} or {@link burst}; omit to stop everything
|
|
146
|
-
* @returns `true` if anything was stopped
|
|
201
|
+
* Stops an active play or pending burst by handler; stops all when called with no arguments.
|
|
202
|
+
* @returns `true` if anything was stopped.
|
|
147
203
|
*/
|
|
148
204
|
stop(handler) {
|
|
205
|
+
if (this.destroyed) {
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
149
208
|
if (handler === undefined) {
|
|
150
|
-
const hadAnything = this.
|
|
151
|
-
this.
|
|
152
|
-
this.
|
|
209
|
+
const hadAnything = this.pendingBursts.length > 0 || this.activePlays.length > 0;
|
|
210
|
+
this.pendingBursts.length = 0;
|
|
211
|
+
this.activePlays.length = 0;
|
|
153
212
|
return hadAnything;
|
|
154
213
|
}
|
|
155
|
-
const burstIndex = this.
|
|
214
|
+
const burstIndex = this.pendingBursts.findIndex((b) => b.handler === handler);
|
|
156
215
|
if (burstIndex !== -1) {
|
|
157
|
-
this.
|
|
216
|
+
this.pendingBursts.splice(burstIndex, 1);
|
|
158
217
|
return true;
|
|
159
218
|
}
|
|
160
|
-
const playIndex = this.
|
|
219
|
+
const playIndex = this.activePlays.findIndex((play) => play.handler === handler);
|
|
161
220
|
if (playIndex !== -1) {
|
|
162
|
-
this.
|
|
221
|
+
this.activePlays.splice(playIndex, 1);
|
|
163
222
|
return true;
|
|
164
223
|
}
|
|
165
224
|
return false;
|
|
166
225
|
}
|
|
167
226
|
/**
|
|
168
|
-
* Simulates
|
|
169
|
-
*
|
|
170
|
-
* @param duration - Total time to simulate in seconds
|
|
171
|
-
* @param stepDuration - Maximum simulation step size in seconds. Defaults to `1/60`
|
|
227
|
+
* Simulates forward in time to avoid a cold-start on frame 0.
|
|
228
|
+
* @param stepDuration - Maximum step size in seconds. Defaults to `1/60`.
|
|
172
229
|
*/
|
|
173
230
|
prewarm(duration, stepDuration = EMITTER_DEFAULT_PREWARM_MIN_STEP_DURATION) {
|
|
174
231
|
assertValidPositiveNumber(duration, "FXEmitter.prewarm.duration");
|
|
175
232
|
assertValidPositiveNumber(stepDuration, "FXEmitter.prewarm.stepDuration");
|
|
233
|
+
if (this.destroyed) {
|
|
234
|
+
// Ticking a destroyed emitter would fire pending bursts and mutate disposed geometry.
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
176
237
|
const stepCount = Math.min(duration / Math.max(stepDuration, EMITTER_DEFAULT_PREWARM_MIN_STEP_DURATION), EMITTER_DEFAULT_PREWARM_MAX_STEP_COUNT);
|
|
177
238
|
const step = duration / stepCount;
|
|
178
239
|
let remaining = duration;
|
|
179
240
|
while (remaining > 0) {
|
|
180
241
|
const deltaTime = Math.min(remaining, step);
|
|
181
|
-
this.
|
|
242
|
+
this.tick(deltaTime);
|
|
182
243
|
remaining -= deltaTime;
|
|
183
244
|
}
|
|
184
245
|
}
|
|
185
|
-
/** Kills all live particles and cancels all active plays and pending bursts */
|
|
246
|
+
/** Kills all live particles and cancels all active plays and pending bursts. */
|
|
186
247
|
reset() {
|
|
187
|
-
this.
|
|
188
|
-
|
|
189
|
-
|
|
248
|
+
if (this.destroyed) {
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
this.pendingBursts.length = 0;
|
|
252
|
+
this.activePlays.length = 0;
|
|
253
|
+
this.mesh.drop();
|
|
254
|
+
// A restarted effect should not report a teleport spike from wherever the emitter was last
|
|
255
|
+
// posed - drop the velocity/angular-velocity baseline along with the particles.
|
|
256
|
+
this.motionTracker.reset();
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Scrubs live parameter values into uniform/binding slots by name, picked up next frame - the
|
|
260
|
+
* runtime's value channel for a non-structural edit. Unknown name = safe no-op; never recompiles.
|
|
261
|
+
* A structural edit is instead a fresh {@link fromArtifacts} (the editor decides which).
|
|
262
|
+
*/
|
|
263
|
+
applyValues(values) {
|
|
264
|
+
if (this.destroyed) {
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
if (values.uniforms !== undefined) {
|
|
268
|
+
this.material.applyUniformValues(values.uniforms);
|
|
269
|
+
}
|
|
270
|
+
if (values.bindings !== undefined) {
|
|
271
|
+
this.simulation?.applyBindingValues(values.bindings);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Merged attribute set (name -> component count): union of the behavior's writes and the render's
|
|
276
|
+
* reads. Throws on a cross-artifact width disagreement rather than sizing a buffer wrong.
|
|
277
|
+
*/
|
|
278
|
+
attributeComponents() {
|
|
279
|
+
const components = new Map();
|
|
280
|
+
const add = (name, count) => {
|
|
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);
|
|
291
|
+
}
|
|
292
|
+
for (const read of this.material.attributeReads) {
|
|
293
|
+
add(read.name, read.components);
|
|
294
|
+
}
|
|
295
|
+
return components;
|
|
296
|
+
}
|
|
297
|
+
buildCollectedProperties(components) {
|
|
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
|
+
};
|
|
308
|
+
}
|
|
309
|
+
return properties;
|
|
310
|
+
}
|
|
311
|
+
setSortFraction(value, context) {
|
|
312
|
+
assertValidNonNegativeNumber(value, context);
|
|
313
|
+
// Clamp to [0, 1]: a value > 1 would grow `sortingAccumulator` without bound.
|
|
314
|
+
this.sortFraction = Math.min(value, 1);
|
|
315
|
+
}
|
|
316
|
+
// Throws when the behavior artifact cannot seed births. Checked at the scheduling points, NOT in
|
|
317
|
+
// the frame tick - a throw from the world tick would take the host's render loop down.
|
|
318
|
+
assertCanSpawn(context) {
|
|
319
|
+
if (this.simulation !== undefined && !this.simulation.canSpawn) {
|
|
320
|
+
throw new Error(`${context}: this behavior artifact cannot seed births (update-only)`);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
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
|
+
// The emitter's world matrix as the reused snapshot the kernel reads. It is the mesh's
|
|
345
|
+
// `matrixWorld` (same matrix the shader and depth sort see), so a world-space graph stays
|
|
346
|
+
// consistent under any transform. `Matrix4.elements` is already column-major.
|
|
347
|
+
worldTransform() {
|
|
348
|
+
this.mesh.updateWorldMatrix(true, false);
|
|
349
|
+
const world = this.mesh.matrixWorld.elements;
|
|
350
|
+
const transform = this.emitterTransform;
|
|
351
|
+
for (let i = 0; i < 16; i += 1) {
|
|
352
|
+
transform.worldMatrix[i] = world[i];
|
|
353
|
+
}
|
|
354
|
+
return transform;
|
|
190
355
|
}
|
|
191
|
-
|
|
192
|
-
|
|
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
|
+
tick(deltaTime) {
|
|
367
|
+
// World-space velocity/angular velocity since the previous tick, computed once here and not
|
|
368
|
+
// inside worldTransform() - the burst/play loops below and the update phase can each call
|
|
369
|
+
// worldTransform() several times this tick, and diffing per-call instead of per-tick would
|
|
370
|
+
// corrupt the value on a frame with more than one of those (each call would diff against a
|
|
371
|
+
// baseline the previous call already advanced).
|
|
372
|
+
this.mesh.updateWorldMatrix(true, false);
|
|
373
|
+
this.mesh.matrixWorld.decompose(this.motionPositionScratch, this.motionQuaternionScratch, this.motionScaleScratch);
|
|
374
|
+
const motion = this.motionTracker.sample({
|
|
375
|
+
position: [
|
|
376
|
+
this.motionPositionScratch.x,
|
|
377
|
+
this.motionPositionScratch.y,
|
|
378
|
+
this.motionPositionScratch.z,
|
|
379
|
+
],
|
|
380
|
+
quaternion: [
|
|
381
|
+
this.motionQuaternionScratch.x,
|
|
382
|
+
this.motionQuaternionScratch.y,
|
|
383
|
+
this.motionQuaternionScratch.z,
|
|
384
|
+
this.motionQuaternionScratch.w,
|
|
385
|
+
],
|
|
386
|
+
}, deltaTime);
|
|
387
|
+
this.emitterTransform.velocity[0] = motion.velocity[0];
|
|
388
|
+
this.emitterTransform.velocity[1] = motion.velocity[1];
|
|
389
|
+
this.emitterTransform.velocity[2] = motion.velocity[2];
|
|
390
|
+
this.emitterTransform.angularVelocity[0] = motion.angularVelocity[0];
|
|
391
|
+
this.emitterTransform.angularVelocity[1] = motion.angularVelocity[1];
|
|
392
|
+
this.emitterTransform.angularVelocity[2] = motion.angularVelocity[2];
|
|
393
|
+
// Age increment + cull. Age is owned by this fixed loop, not the graph - same for every effect.
|
|
193
394
|
{
|
|
194
|
-
const
|
|
195
|
-
const { array, itemSize } =
|
|
196
|
-
|
|
197
|
-
|
|
395
|
+
const lifecycle = this.mesh.propertyBuffers[FX_CORE_LIFECYCLE];
|
|
396
|
+
const { array, itemSize } = lifecycle;
|
|
397
|
+
const instanceCount = this.mesh.instanceCount;
|
|
398
|
+
for (let i = 0; i < instanceCount; i++) {
|
|
399
|
+
array[i * itemSize + FX_AGE] += deltaTime;
|
|
198
400
|
}
|
|
199
401
|
}
|
|
200
|
-
this.
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
const pending = this._private_pendingBursts[i];
|
|
402
|
+
this.mesh.removeDeadParticles();
|
|
403
|
+
for (let i = this.pendingBursts.length - 1; i >= 0; i--) {
|
|
404
|
+
const pending = this.pendingBursts[i];
|
|
204
405
|
pending.delay -= deltaTime;
|
|
205
406
|
if (pending.delay <= 0) {
|
|
206
|
-
this.
|
|
207
|
-
this.
|
|
407
|
+
this.pendingBursts.splice(i, 1);
|
|
408
|
+
this.spawnBurst(pending.count);
|
|
208
409
|
}
|
|
209
410
|
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
const play = this._private_activePlays[i];
|
|
411
|
+
for (let i = this.activePlays.length - 1; i >= 0; i--) {
|
|
412
|
+
const play = this.activePlays[i];
|
|
213
413
|
let effectiveDeltaTime = deltaTime;
|
|
214
414
|
if (play.delay > 0) {
|
|
215
415
|
play.delay -= deltaTime;
|
|
216
416
|
if (play.delay > 0) {
|
|
217
417
|
continue;
|
|
218
418
|
}
|
|
219
|
-
// Delay just expired - use the overshoot as effective dt this tick
|
|
419
|
+
// Delay just expired - use the overshoot as effective dt this tick.
|
|
220
420
|
effectiveDeltaTime = -play.delay;
|
|
221
421
|
play.delay = 0;
|
|
222
422
|
}
|
|
423
|
+
const previousElapsed = play.elapsed;
|
|
223
424
|
play.elapsed += effectiveDeltaTime;
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
425
|
+
const expired = play.elapsed >= play.duration;
|
|
426
|
+
// On the final (partial) tick, emit only for the slice inside the play's duration, so the last
|
|
427
|
+
// fraction and the accumulator remainder still spawn instead of being dropped on removal.
|
|
428
|
+
const emitDeltaTime = expired
|
|
429
|
+
? Math.max(0, play.duration - previousElapsed)
|
|
430
|
+
: effectiveDeltaTime;
|
|
431
|
+
play.accumulator += play.rate * emitDeltaTime;
|
|
229
432
|
const particlesToSpawn = Math.floor(play.accumulator);
|
|
230
433
|
if (particlesToSpawn > 0) {
|
|
231
|
-
this.
|
|
434
|
+
this.spawnBurst(particlesToSpawn);
|
|
232
435
|
play.accumulator -= particlesToSpawn;
|
|
233
436
|
}
|
|
437
|
+
if (expired) {
|
|
438
|
+
this.activePlays.splice(i, 1);
|
|
439
|
+
}
|
|
234
440
|
}
|
|
235
|
-
if (this.
|
|
441
|
+
if (this.mesh.instanceCount === 0) {
|
|
236
442
|
return;
|
|
237
443
|
}
|
|
238
|
-
{
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
}
|
|
244
|
-
{
|
|
245
|
-
const { builtin } = this._private_mesh.propertyBuffers;
|
|
246
|
-
const { array, itemSize } = builtin;
|
|
247
|
-
for (let i = 0; i < this._private_mesh.instanceCount; i++) {
|
|
248
|
-
const offset = i * itemSize;
|
|
249
|
-
// Position += velocity * dt
|
|
250
|
-
array[offset + BUILTIN_OFFSET_POSITION_X] +=
|
|
251
|
-
array[offset + BUILTIN_OFFSET_VELOCITY_X] * deltaTime;
|
|
252
|
-
array[offset + BUILTIN_OFFSET_POSITION_Y] +=
|
|
253
|
-
array[offset + BUILTIN_OFFSET_VELOCITY_Y] * deltaTime;
|
|
254
|
-
array[offset + BUILTIN_OFFSET_POSITION_Z] +=
|
|
255
|
-
array[offset + BUILTIN_OFFSET_VELOCITY_Z] * deltaTime;
|
|
256
|
-
// Rotation += torque * dt
|
|
257
|
-
array[offset + BUILTIN_OFFSET_ROTATION] +=
|
|
258
|
-
array[offset + BUILTIN_OFFSET_TORQUE] * deltaTime;
|
|
259
|
-
}
|
|
260
|
-
builtin.needsUpdate = true;
|
|
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);
|
|
261
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;
|
|
262
452
|
}
|
|
263
|
-
|
|
264
|
-
const instanceBegin = this.
|
|
265
|
-
this.
|
|
266
|
-
const instanceEnd = this.
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
const itemOffset = i * itemSize;
|
|
272
|
-
array[itemOffset + BUILTIN_OFFSET_RANDOM_A] = Math.random();
|
|
273
|
-
array[itemOffset + BUILTIN_OFFSET_RANDOM_B] = Math.random();
|
|
274
|
-
array[itemOffset + BUILTIN_OFFSET_RANDOM_C] = Math.random();
|
|
275
|
-
}
|
|
276
|
-
builtin.needsUpdate = true;
|
|
277
|
-
}
|
|
278
|
-
for (const spawnModule of this._private_spawnSequence) {
|
|
279
|
-
spawnModule.spawn(this._private_mesh.propertyBuffers, instanceBegin, instanceEnd);
|
|
453
|
+
spawnBurst(count) {
|
|
454
|
+
const instanceBegin = this.mesh.instanceCount;
|
|
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);
|
|
280
461
|
}
|
|
281
462
|
}
|
|
282
463
|
}
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
*
|
|
286
|
-
* @param deltaTime - Elapsed time in seconds since the last frame
|
|
287
|
-
*/
|
|
288
|
-
FXEmitter.onWillRender = (deltaTime) => {
|
|
289
|
-
for (const instance of EMITTERS) {
|
|
290
|
-
instance._private_onRendering(deltaTime);
|
|
291
|
-
}
|
|
292
|
-
};
|
|
293
|
-
|
|
294
|
-
export { FXEmitter };
|
|
464
|
+
// Returned by post-destroy `burst`/`play` - never matches a scheduled handler.
|
|
465
|
+
const DEAD_HANDLER = -1;
|