sparcoon 0.7.2 → 0.9.0
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/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 +46 -48
- package/dist/emitter/FXEmitter.js +355 -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 +10 -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 +1 -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/FXMeshMaterial.d.ts +40 -0
- package/dist/render/FXMeshMaterial.js +77 -0
- package/dist/world/FXWorld.d.ts +42 -0
- package/dist/world/FXWorld.js +103 -0
- package/package.json +45 -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 -17
- 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 -24
- 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,191 @@
|
|
|
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.registerEmitter(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
|
+
* @internal Builds an emitter into `world` from a precompiled effect. Called by {@link FXEffect};
|
|
114
|
+
* the editor emits the two artifacts as an ESM module and the effect hands them here.
|
|
115
|
+
*/
|
|
116
|
+
static fromArtifacts(render, behavior, options = {}, world = FXWorld.getDefault()) {
|
|
117
|
+
const material = new FXArtifactMaterial(render, options.textures);
|
|
118
|
+
const holder = new FXSimulationHolder(behavior);
|
|
119
|
+
const baseGeometry = resolveGeometrySource(render.geometry, options.geometries ?? {});
|
|
120
|
+
return new FXEmitter(material, holder, options, baseGeometry, world);
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* @internal Ticks this emitter one frame. Driven by {@link FXWorld.update}, which advances the
|
|
124
|
+
* shared clock once and passes it in as `elapsedTime`.
|
|
125
|
+
*/
|
|
126
|
+
onWorldTick(deltaTime, elapsedTime) {
|
|
127
|
+
this.onRendering(deltaTime);
|
|
128
|
+
this.material.setElapsedTime(elapsedTime);
|
|
129
|
+
this.material.setDeltaTime(deltaTime);
|
|
130
|
+
// emitterTransform.velocity/angularVelocity are always length 3 (see its declaration) -
|
|
131
|
+
// computed once per tick in onRendering -> tick.
|
|
132
|
+
this.material.setObjectVelocity(this.emitterTransform.velocity);
|
|
133
|
+
this.material.setObjectAngularVelocity(this.emitterTransform.angularVelocity);
|
|
80
134
|
}
|
|
81
|
-
/** Unregisters
|
|
135
|
+
/** Unregisters, removes from the scene, disposes mesh and material. Idempotent. */
|
|
82
136
|
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?.();
|
|
137
|
+
if (this.destroyed) {
|
|
138
|
+
return;
|
|
92
139
|
}
|
|
93
|
-
this.
|
|
94
|
-
this.
|
|
95
|
-
|
|
96
|
-
this.
|
|
140
|
+
this.destroyed = true;
|
|
141
|
+
this.world.unregisterEmitter(this);
|
|
142
|
+
// Drop scheduled work and the buffer cache so nothing references the mesh's buffers after teardown.
|
|
143
|
+
this.pendingBursts.length = 0;
|
|
144
|
+
this.activePlays.length = 0;
|
|
145
|
+
this.kernelBuffersCache = undefined;
|
|
146
|
+
// The material owns no per-emitter GPU resources; the mesh teardown disposes the instanced
|
|
147
|
+
// geometry and the mounted material.
|
|
148
|
+
this.material.destroy();
|
|
149
|
+
this.mesh.destroy();
|
|
97
150
|
this.removeFromParent();
|
|
98
151
|
}
|
|
99
152
|
/**
|
|
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
|
|
153
|
+
* Spawns `count` particles immediately, or after `options.delay`.
|
|
154
|
+
* @returns Handler for {@link stop} to cancel a pending delayed burst.
|
|
105
155
|
*/
|
|
106
156
|
burst(count, options = {}) {
|
|
107
|
-
|
|
108
|
-
|
|
157
|
+
// Whole particles only: a fractional count sets a fractional `instanceCount`.
|
|
158
|
+
assertValidPositiveInteger(count, "FXEmitter.burst.count");
|
|
159
|
+
this.assertCanSpawn("FXEmitter.burst");
|
|
160
|
+
if (this.destroyed) {
|
|
161
|
+
return DEAD_HANDLER;
|
|
162
|
+
}
|
|
163
|
+
const handler = this.nextHandler++;
|
|
109
164
|
const delay = options.delay ?? 0;
|
|
110
165
|
assertValidNonNegativeNumber(delay, "FXEmitter.burst.options.delay");
|
|
111
166
|
if (delay <= 0) {
|
|
112
|
-
this.
|
|
167
|
+
this.spawnBurst(count);
|
|
113
168
|
}
|
|
114
169
|
else {
|
|
115
|
-
this.
|
|
170
|
+
this.pendingBursts.push({ handler, count, delay });
|
|
116
171
|
}
|
|
117
172
|
return handler;
|
|
118
173
|
}
|
|
119
174
|
/**
|
|
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
|
|
175
|
+
* Starts continuous emission at `rate` particles/second.
|
|
176
|
+
* @returns Handler for {@link stop} to cancel.
|
|
125
177
|
*/
|
|
126
178
|
play(rate, options = {}) {
|
|
127
179
|
assertValidPositiveNumber(rate, "FXEmitter.play.rate");
|
|
128
180
|
if (options.delay !== undefined) {
|
|
129
181
|
assertValidNonNegativeNumber(options.delay, "FXEmitter.play.options.delay");
|
|
130
182
|
}
|
|
131
|
-
|
|
132
|
-
this.
|
|
183
|
+
this.assertCanSpawn("FXEmitter.play");
|
|
184
|
+
if (this.destroyed) {
|
|
185
|
+
return DEAD_HANDLER;
|
|
186
|
+
}
|
|
187
|
+
const handler = this.nextHandler++;
|
|
188
|
+
this.activePlays.push({
|
|
133
189
|
handler,
|
|
134
190
|
rate,
|
|
135
191
|
duration: options.duration ?? Infinity,
|
|
@@ -140,155 +196,268 @@ class FXEmitter extends Object3D {
|
|
|
140
196
|
return handler;
|
|
141
197
|
}
|
|
142
198
|
/**
|
|
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
|
|
199
|
+
* Stops an active play or pending burst by handler; stops all when called with no arguments.
|
|
200
|
+
* @returns `true` if anything was stopped.
|
|
147
201
|
*/
|
|
148
202
|
stop(handler) {
|
|
203
|
+
if (this.destroyed) {
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
149
206
|
if (handler === undefined) {
|
|
150
|
-
const hadAnything = this.
|
|
151
|
-
this.
|
|
152
|
-
this.
|
|
207
|
+
const hadAnything = this.pendingBursts.length > 0 || this.activePlays.length > 0;
|
|
208
|
+
this.pendingBursts.length = 0;
|
|
209
|
+
this.activePlays.length = 0;
|
|
153
210
|
return hadAnything;
|
|
154
211
|
}
|
|
155
|
-
const burstIndex = this.
|
|
212
|
+
const burstIndex = this.pendingBursts.findIndex((b) => b.handler === handler);
|
|
156
213
|
if (burstIndex !== -1) {
|
|
157
|
-
this.
|
|
214
|
+
this.pendingBursts.splice(burstIndex, 1);
|
|
158
215
|
return true;
|
|
159
216
|
}
|
|
160
|
-
const playIndex = this.
|
|
217
|
+
const playIndex = this.activePlays.findIndex((play) => play.handler === handler);
|
|
161
218
|
if (playIndex !== -1) {
|
|
162
|
-
this.
|
|
219
|
+
this.activePlays.splice(playIndex, 1);
|
|
163
220
|
return true;
|
|
164
221
|
}
|
|
165
222
|
return false;
|
|
166
223
|
}
|
|
167
224
|
/**
|
|
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`
|
|
225
|
+
* Simulates forward in time to avoid a cold-start on frame 0.
|
|
226
|
+
* @param stepDuration - Maximum step size in seconds. Defaults to `1/60`.
|
|
172
227
|
*/
|
|
173
228
|
prewarm(duration, stepDuration = EMITTER_DEFAULT_PREWARM_MIN_STEP_DURATION) {
|
|
174
229
|
assertValidPositiveNumber(duration, "FXEmitter.prewarm.duration");
|
|
175
230
|
assertValidPositiveNumber(stepDuration, "FXEmitter.prewarm.stepDuration");
|
|
231
|
+
if (this.destroyed) {
|
|
232
|
+
// Ticking a destroyed emitter would fire pending bursts and mutate disposed geometry.
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
176
235
|
const stepCount = Math.min(duration / Math.max(stepDuration, EMITTER_DEFAULT_PREWARM_MIN_STEP_DURATION), EMITTER_DEFAULT_PREWARM_MAX_STEP_COUNT);
|
|
177
236
|
const step = duration / stepCount;
|
|
178
237
|
let remaining = duration;
|
|
179
238
|
while (remaining > 0) {
|
|
180
239
|
const deltaTime = Math.min(remaining, step);
|
|
181
|
-
this.
|
|
240
|
+
this.tick(deltaTime);
|
|
182
241
|
remaining -= deltaTime;
|
|
183
242
|
}
|
|
184
243
|
}
|
|
185
|
-
/** Kills all live particles and cancels all active plays and pending bursts */
|
|
244
|
+
/** Kills all live particles and cancels all active plays and pending bursts. */
|
|
186
245
|
reset() {
|
|
187
|
-
this.
|
|
188
|
-
|
|
189
|
-
|
|
246
|
+
if (this.destroyed) {
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
this.pendingBursts.length = 0;
|
|
250
|
+
this.activePlays.length = 0;
|
|
251
|
+
this.mesh.drop();
|
|
252
|
+
// A restarted effect should not report a teleport spike from wherever the emitter was last
|
|
253
|
+
// posed - drop the velocity/angular-velocity baseline along with the particles.
|
|
254
|
+
this.motionTracker.reset();
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Scrubs live parameter values into uniform/binding slots by name, picked up next frame - the
|
|
258
|
+
* runtime's value channel for a non-structural edit. Unknown name = safe no-op; never recompiles.
|
|
259
|
+
* A structural edit is instead a fresh {@link fromArtifacts} (the editor decides which).
|
|
260
|
+
*/
|
|
261
|
+
applyValues(values) {
|
|
262
|
+
if (this.destroyed) {
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
if (values.uniforms !== undefined) {
|
|
266
|
+
this.material.applyUniformValues(values.uniforms);
|
|
267
|
+
}
|
|
268
|
+
if (values.bindings !== undefined) {
|
|
269
|
+
this.simulation?.applyBindingValues(values.bindings);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Merged attribute set (name -> component count): union of the behavior's writes and the render's
|
|
274
|
+
* reads. Throws on a cross-artifact width disagreement rather than sizing a buffer wrong.
|
|
275
|
+
*/
|
|
276
|
+
attributeComponents() {
|
|
277
|
+
const components = new Map();
|
|
278
|
+
const add = (name, count) => {
|
|
279
|
+
const existing = components.get(name);
|
|
280
|
+
if (existing !== undefined && existing !== count) {
|
|
281
|
+
throw new Error(`FXEmitter: attribute "${name}" is written and read with conflicting widths ` +
|
|
282
|
+
`(${existing.toString()} vs ${count.toString()}); the behavior attributeWrites and ` +
|
|
283
|
+
`the render attributeReads must agree`);
|
|
284
|
+
}
|
|
285
|
+
components.set(name, count);
|
|
286
|
+
};
|
|
287
|
+
for (const write of this.simulation?.attributeWrites ?? []) {
|
|
288
|
+
add(write.name, write.components);
|
|
289
|
+
}
|
|
290
|
+
for (const read of this.material.attributeReads) {
|
|
291
|
+
add(read.name, read.components);
|
|
292
|
+
}
|
|
293
|
+
return components;
|
|
294
|
+
}
|
|
295
|
+
buildCollectedProperties(components) {
|
|
296
|
+
const properties = {
|
|
297
|
+
[FX_CORE_POSITION]: CORE_POSITION_TYPE,
|
|
298
|
+
[FX_CORE_LIFECYCLE]: CORE_LIFECYCLE_TYPE,
|
|
299
|
+
};
|
|
300
|
+
for (const [name, count] of components) {
|
|
301
|
+
properties[`${ATTRIBUTE_BUFFER_PREFIX}${name}`] = {
|
|
302
|
+
glslTypeName: GLSL_TYPE_BY_COMPONENTS[count],
|
|
303
|
+
bufferSize: count,
|
|
304
|
+
instantiable: true,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
return properties;
|
|
308
|
+
}
|
|
309
|
+
setSortFraction(value, context) {
|
|
310
|
+
assertValidNonNegativeNumber(value, context);
|
|
311
|
+
// Clamp to [0, 1]: a value > 1 would grow `sortingAccumulator` without bound.
|
|
312
|
+
this.sortFraction = Math.min(value, 1);
|
|
313
|
+
}
|
|
314
|
+
// Throws when the behavior artifact cannot seed births. Checked at the scheduling points, NOT in
|
|
315
|
+
// the frame tick - a throw from the world tick would take the host's render loop down.
|
|
316
|
+
assertCanSpawn(context) {
|
|
317
|
+
if (this.simulation !== undefined && !this.simulation.canSpawn) {
|
|
318
|
+
throw new Error(`${context}: this behavior artifact cannot seed births (update-only)`);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
// Maps each kernel state-buffer name to its backing Float32Array. Cached across ticks/bursts and
|
|
322
|
+
// rebuilt only when the mesh's buffer set changes (`bufferVersion` - capacity growth).
|
|
323
|
+
kernelBuffers() {
|
|
324
|
+
const version = this.mesh.bufferVersion;
|
|
325
|
+
if (this.kernelBuffersCache !== undefined && this.kernelBuffersCacheVersion === version) {
|
|
326
|
+
return this.kernelBuffersCache;
|
|
327
|
+
}
|
|
328
|
+
const buffers = {
|
|
329
|
+
[FX_CORE_POSITION]: this.mesh.propertyBuffers[FX_CORE_POSITION].array,
|
|
330
|
+
[FX_CORE_LIFECYCLE]: this.mesh.propertyBuffers[FX_CORE_LIFECYCLE].array,
|
|
331
|
+
};
|
|
332
|
+
for (const buffer of this.simulation?.attributeBuffers ?? []) {
|
|
333
|
+
const attribute = this.mesh.propertyBuffers[`${ATTRIBUTE_BUFFER_PREFIX}${buffer.name}`];
|
|
334
|
+
if (attribute !== undefined) {
|
|
335
|
+
buffers[buffer.name] = attribute.array;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
this.kernelBuffersCache = buffers;
|
|
339
|
+
this.kernelBuffersCacheVersion = version;
|
|
340
|
+
return buffers;
|
|
341
|
+
}
|
|
342
|
+
// The emitter's world matrix as the reused snapshot the kernel reads. It is the mesh's
|
|
343
|
+
// `matrixWorld` (same matrix the shader and depth sort see), so a world-space graph stays
|
|
344
|
+
// consistent under any transform. `Matrix4.elements` is already column-major.
|
|
345
|
+
worldTransform() {
|
|
346
|
+
this.mesh.updateWorldMatrix(true, false);
|
|
347
|
+
const world = this.mesh.matrixWorld.elements;
|
|
348
|
+
const transform = this.emitterTransform;
|
|
349
|
+
for (let i = 0; i < 16; i += 1) {
|
|
350
|
+
transform.worldMatrix[i] = world[i];
|
|
351
|
+
}
|
|
352
|
+
return transform;
|
|
190
353
|
}
|
|
191
|
-
|
|
192
|
-
|
|
354
|
+
markBuffersNeedUpdate(names) {
|
|
355
|
+
for (const name of names) {
|
|
356
|
+
const isCore = name === FX_CORE_POSITION || name === FX_CORE_LIFECYCLE;
|
|
357
|
+
const key = isCore ? name : `${ATTRIBUTE_BUFFER_PREFIX}${name}`;
|
|
358
|
+
const attribute = this.mesh.propertyBuffers[key];
|
|
359
|
+
if (attribute !== undefined) {
|
|
360
|
+
attribute.needsUpdate = true;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
tick(deltaTime) {
|
|
365
|
+
// World-space velocity/angular velocity since the previous tick, computed once here and not
|
|
366
|
+
// inside worldTransform() - the burst/play loops below and the update phase can each call
|
|
367
|
+
// worldTransform() several times this tick, and diffing per-call instead of per-tick would
|
|
368
|
+
// corrupt the value on a frame with more than one of those (each call would diff against a
|
|
369
|
+
// baseline the previous call already advanced).
|
|
370
|
+
this.mesh.updateWorldMatrix(true, false);
|
|
371
|
+
this.mesh.matrixWorld.decompose(this.motionPositionScratch, this.motionQuaternionScratch, this.motionScaleScratch);
|
|
372
|
+
const motion = this.motionTracker.sample({
|
|
373
|
+
position: [
|
|
374
|
+
this.motionPositionScratch.x,
|
|
375
|
+
this.motionPositionScratch.y,
|
|
376
|
+
this.motionPositionScratch.z,
|
|
377
|
+
],
|
|
378
|
+
quaternion: [
|
|
379
|
+
this.motionQuaternionScratch.x,
|
|
380
|
+
this.motionQuaternionScratch.y,
|
|
381
|
+
this.motionQuaternionScratch.z,
|
|
382
|
+
this.motionQuaternionScratch.w,
|
|
383
|
+
],
|
|
384
|
+
}, deltaTime);
|
|
385
|
+
this.emitterTransform.velocity[0] = motion.velocity[0];
|
|
386
|
+
this.emitterTransform.velocity[1] = motion.velocity[1];
|
|
387
|
+
this.emitterTransform.velocity[2] = motion.velocity[2];
|
|
388
|
+
this.emitterTransform.angularVelocity[0] = motion.angularVelocity[0];
|
|
389
|
+
this.emitterTransform.angularVelocity[1] = motion.angularVelocity[1];
|
|
390
|
+
this.emitterTransform.angularVelocity[2] = motion.angularVelocity[2];
|
|
391
|
+
// Age increment + cull. Age is owned by this fixed loop, not the graph - same for every effect.
|
|
193
392
|
{
|
|
194
|
-
const
|
|
195
|
-
const { array, itemSize } =
|
|
196
|
-
|
|
197
|
-
|
|
393
|
+
const lifecycle = this.mesh.propertyBuffers[FX_CORE_LIFECYCLE];
|
|
394
|
+
const { array, itemSize } = lifecycle;
|
|
395
|
+
const instanceCount = this.mesh.instanceCount;
|
|
396
|
+
for (let i = 0; i < instanceCount; i++) {
|
|
397
|
+
array[i * itemSize + FX_AGE] += deltaTime;
|
|
198
398
|
}
|
|
199
399
|
}
|
|
200
|
-
this.
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
const pending = this._private_pendingBursts[i];
|
|
400
|
+
this.mesh.removeDeadParticles();
|
|
401
|
+
for (let i = this.pendingBursts.length - 1; i >= 0; i--) {
|
|
402
|
+
const pending = this.pendingBursts[i];
|
|
204
403
|
pending.delay -= deltaTime;
|
|
205
404
|
if (pending.delay <= 0) {
|
|
206
|
-
this.
|
|
207
|
-
this.
|
|
405
|
+
this.pendingBursts.splice(i, 1);
|
|
406
|
+
this.spawnBurst(pending.count);
|
|
208
407
|
}
|
|
209
408
|
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
const play = this._private_activePlays[i];
|
|
409
|
+
for (let i = this.activePlays.length - 1; i >= 0; i--) {
|
|
410
|
+
const play = this.activePlays[i];
|
|
213
411
|
let effectiveDeltaTime = deltaTime;
|
|
214
412
|
if (play.delay > 0) {
|
|
215
413
|
play.delay -= deltaTime;
|
|
216
414
|
if (play.delay > 0) {
|
|
217
415
|
continue;
|
|
218
416
|
}
|
|
219
|
-
// Delay just expired - use the overshoot as effective dt this tick
|
|
417
|
+
// Delay just expired - use the overshoot as effective dt this tick.
|
|
220
418
|
effectiveDeltaTime = -play.delay;
|
|
221
419
|
play.delay = 0;
|
|
222
420
|
}
|
|
421
|
+
const previousElapsed = play.elapsed;
|
|
223
422
|
play.elapsed += effectiveDeltaTime;
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
423
|
+
const expired = play.elapsed >= play.duration;
|
|
424
|
+
// On the final (partial) tick, emit only for the slice inside the play's duration, so the last
|
|
425
|
+
// fraction and the accumulator remainder still spawn instead of being dropped on removal.
|
|
426
|
+
const emitDeltaTime = expired
|
|
427
|
+
? Math.max(0, play.duration - previousElapsed)
|
|
428
|
+
: effectiveDeltaTime;
|
|
429
|
+
play.accumulator += play.rate * emitDeltaTime;
|
|
229
430
|
const particlesToSpawn = Math.floor(play.accumulator);
|
|
230
431
|
if (particlesToSpawn > 0) {
|
|
231
|
-
this.
|
|
432
|
+
this.spawnBurst(particlesToSpawn);
|
|
232
433
|
play.accumulator -= particlesToSpawn;
|
|
233
434
|
}
|
|
435
|
+
if (expired) {
|
|
436
|
+
this.activePlays.splice(i, 1);
|
|
437
|
+
}
|
|
234
438
|
}
|
|
235
|
-
if (this.
|
|
439
|
+
if (this.mesh.instanceCount === 0) {
|
|
236
440
|
return;
|
|
237
441
|
}
|
|
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;
|
|
442
|
+
if (this.simulation !== undefined) {
|
|
443
|
+
// Motion integration lives in the graph (an `integrate-motion` node writes
|
|
444
|
+
// `position += velocity * dt`), not in this host loop.
|
|
445
|
+
this.simulation.update(this.kernelBuffers(), this.mesh.instanceCount, deltaTime, this.worldTransform());
|
|
446
|
+
this.markBuffersNeedUpdate(this.simulation.updateWrittenBuffers);
|
|
261
447
|
}
|
|
448
|
+
// Age (written above) must reach the GPU this frame; the update phase's buffers were flagged already.
|
|
449
|
+
this.mesh.propertyBuffers[FX_CORE_LIFECYCLE].needsUpdate = true;
|
|
262
450
|
}
|
|
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);
|
|
451
|
+
spawnBurst(count) {
|
|
452
|
+
const instanceBegin = this.mesh.instanceCount;
|
|
453
|
+
this.mesh.createInstances(count);
|
|
454
|
+
const instanceEnd = this.mesh.instanceCount;
|
|
455
|
+
// Re-checked (defensive): no spawn function would leave the freshly zeroed rows born dead.
|
|
456
|
+
if (this.simulation?.canSpawn === true) {
|
|
457
|
+
this.simulation.spawn(this.kernelBuffers(), instanceBegin, instanceEnd - instanceBegin, this.worldTransform());
|
|
458
|
+
this.markBuffersNeedUpdate(this.simulation.spawnWrittenBuffers);
|
|
280
459
|
}
|
|
281
460
|
}
|
|
282
461
|
}
|
|
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 };
|
|
462
|
+
// Returned by post-destroy `burst`/`play` - never matches a scheduled handler.
|
|
463
|
+
const DEAD_HANDLER = -1;
|