sparcoon 0.7.3 → 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.
Files changed (155) hide show
  1. package/README.md +124 -76
  2. package/dist/artifact/FXArtifact.d.ts +120 -0
  3. package/dist/artifact/FXArtifact.js +1 -0
  4. package/dist/coreLayout.d.ts +22 -0
  5. package/dist/coreLayout.js +32 -0
  6. package/dist/effect/FXEffect.d.ts +77 -0
  7. package/dist/effect/FXEffect.js +454 -0
  8. package/dist/effect/FXEffectSampling.Internal.d.ts +24 -0
  9. package/dist/effect/FXEffectSampling.Internal.js +165 -0
  10. package/dist/effect/FXEffectSpec.d.ts +115 -0
  11. package/dist/effect/FXEffectSpec.js +1 -0
  12. package/dist/emitter/FXEmitter.Internal.d.ts +23 -58
  13. package/dist/emitter/FXEmitter.Internal.js +13 -29
  14. package/dist/emitter/FXEmitter.d.ts +46 -48
  15. package/dist/emitter/FXEmitter.js +355 -186
  16. package/dist/emitter/FXFromArtifactsOptions.d.ts +13 -0
  17. package/dist/emitter/FXFromArtifactsOptions.js +1 -0
  18. package/dist/emitter/FXObjectMotion.Internal.d.ts +33 -0
  19. package/dist/emitter/FXObjectMotion.Internal.js +87 -0
  20. package/dist/emitter/FXSimulationHolder.d.ts +1 -0
  21. package/dist/emitter/FXSimulationHolder.js +43 -0
  22. package/dist/index.d.ts +10 -39
  23. package/dist/index.js +13 -36
  24. package/dist/instancedParticle/FXInstancedParticle.d.ts +1 -22
  25. package/dist/instancedParticle/FXInstancedParticle.js +145 -85
  26. package/dist/instancedParticle/glTypeInfo.d.ts +5 -0
  27. package/dist/instancedParticle/glTypeInfo.js +3 -0
  28. package/dist/instancedParticle/primitiveGeometry.d.ts +1 -0
  29. package/dist/instancedParticle/primitiveGeometry.js +30 -0
  30. package/dist/miscellaneous/asserts.d.ts +1 -18
  31. package/dist/miscellaneous/asserts.js +21 -28
  32. package/dist/miscellaneous/fxMath.d.ts +26 -0
  33. package/dist/miscellaneous/fxMath.js +261 -0
  34. package/dist/miscellaneous/math.d.ts +2 -7
  35. package/dist/miscellaneous/math.js +8 -9
  36. package/dist/miscellaneous/texture/fxDataTexture.d.ts +26 -0
  37. package/dist/miscellaneous/texture/fxDataTexture.js +20 -0
  38. package/dist/render/FXArtifactMaterial.Internal.d.ts +16 -0
  39. package/dist/render/FXArtifactMaterial.Internal.js +944 -0
  40. package/dist/render/FXArtifactMaterial.d.ts +1 -0
  41. package/dist/render/FXArtifactMaterial.js +79 -0
  42. package/dist/render/FXMaterialOptions.d.ts +27 -0
  43. package/dist/render/FXMaterialOptions.js +5 -0
  44. package/dist/render/FXMeshMaterial.d.ts +40 -0
  45. package/dist/render/FXMeshMaterial.js +77 -0
  46. package/dist/world/FXWorld.d.ts +42 -0
  47. package/dist/world/FXWorld.js +103 -0
  48. package/package.json +45 -29
  49. package/dist/_virtual/2d.js +0 -3
  50. package/dist/_virtual/3d.js +0 -3
  51. package/dist/_virtual/4d.js +0 -3
  52. package/dist/_virtual/mod.js +0 -5
  53. package/dist/_virtual/mod2.js +0 -3
  54. package/dist/emitter/behavior/FXBehavior.d.ts +0 -24
  55. package/dist/emitter/behavior/FXBehavior.js +0 -10
  56. package/dist/emitter/behavior/FXBehaviorDirectionalForce.d.ts +0 -23
  57. package/dist/emitter/behavior/FXBehaviorDirectionalForce.js +0 -72
  58. package/dist/emitter/behavior/FXBehaviorPointForce.d.ts +0 -36
  59. package/dist/emitter/behavior/FXBehaviorPointForce.js +0 -94
  60. package/dist/emitter/behavior/FXBehaviorScaleOverLife.d.ts +0 -24
  61. package/dist/emitter/behavior/FXBehaviorScaleOverLife.js +0 -55
  62. package/dist/emitter/behavior/FXBehaviorTorqueDamping.d.ts +0 -20
  63. package/dist/emitter/behavior/FXBehaviorTorqueDamping.js +0 -48
  64. package/dist/emitter/behavior/FXBehaviorTorqueNoise.d.ts +0 -25
  65. package/dist/emitter/behavior/FXBehaviorTorqueNoise.js +0 -67
  66. package/dist/emitter/behavior/FXBehaviorTorqueOverLife.d.ts +0 -19
  67. package/dist/emitter/behavior/FXBehaviorTorqueOverLife.js +0 -41
  68. package/dist/emitter/behavior/FXBehaviorVelocityDamping.d.ts +0 -20
  69. package/dist/emitter/behavior/FXBehaviorVelocityDamping.js +0 -50
  70. package/dist/emitter/behavior/FXBehaviorVelocityNoise.d.ts +0 -25
  71. package/dist/emitter/behavior/FXBehaviorVelocityNoise.js +0 -76
  72. package/dist/emitter/behavior/FXBehaviorVelocityOverLife.d.ts +0 -22
  73. package/dist/emitter/behavior/FXBehaviorVelocityOverLife.js +0 -55
  74. package/dist/emitter/spawn/FXSpawn.d.ts +0 -11
  75. package/dist/emitter/spawn/FXSpawn.js +0 -10
  76. package/dist/emitter/spawn/FXSpawnBox.d.ts +0 -18
  77. package/dist/emitter/spawn/FXSpawnBox.js +0 -50
  78. package/dist/emitter/spawn/FXSpawnLifetime.d.ts +0 -17
  79. package/dist/emitter/spawn/FXSpawnLifetime.js +0 -44
  80. package/dist/emitter/spawn/FXSpawnOffset.d.ts +0 -21
  81. package/dist/emitter/spawn/FXSpawnOffset.js +0 -49
  82. package/dist/emitter/spawn/FXSpawnPoint.d.ts +0 -18
  83. package/dist/emitter/spawn/FXSpawnPoint.js +0 -46
  84. package/dist/emitter/spawn/FXSpawnRotation.d.ts +0 -17
  85. package/dist/emitter/spawn/FXSpawnRotation.js +0 -42
  86. package/dist/emitter/spawn/FXSpawnScale.d.ts +0 -22
  87. package/dist/emitter/spawn/FXSpawnScale.js +0 -56
  88. package/dist/emitter/spawn/FXSpawnSphere.d.ts +0 -36
  89. package/dist/emitter/spawn/FXSpawnSphere.js +0 -87
  90. package/dist/emitter/spawn/FXSpawnTorque.d.ts +0 -17
  91. package/dist/emitter/spawn/FXSpawnTorque.js +0 -42
  92. package/dist/emitter/spawn/FXSpawnVelocity.d.ts +0 -29
  93. package/dist/emitter/spawn/FXSpawnVelocity.js +0 -91
  94. package/dist/instancedParticle/FXInstancedParticle.Internal.d.ts +0 -2
  95. package/dist/instancedParticle/shared.d.ts +0 -19
  96. package/dist/instancedParticle/shared.js +0 -116
  97. package/dist/materials/FXDiffuseMaterial/FXDiffuseMaterial.Internal.d.ts +0 -24
  98. package/dist/materials/FXDiffuseMaterial/FXDiffuseMaterial.Internal.js +0 -275
  99. package/dist/materials/FXDiffuseMaterial/FXDiffuseMaterial.d.ts +0 -75
  100. package/dist/materials/FXDiffuseMaterial/FXDiffuseMaterial.js +0 -63
  101. package/dist/materials/FXMaterial/FXBlending.Internal.d.ts +0 -24
  102. package/dist/materials/FXMaterial/FXBlending.Internal.js +0 -64
  103. package/dist/materials/FXMaterial/FXDepthMaterial.Internal.d.ts +0 -4
  104. package/dist/materials/FXMaterial/FXDepthMaterial.Internal.js +0 -144
  105. package/dist/materials/FXMaterial/FXDistanceMaterial.Internal.d.ts +0 -4
  106. package/dist/materials/FXMaterial/FXDistanceMaterial.Internal.js +0 -175
  107. package/dist/materials/FXMaterial/FXMaterial.d.ts +0 -70
  108. package/dist/materials/FXMaterial/FXMaterial.js +0 -51
  109. package/dist/materials/FXUnlitMaterial/FXUnlitMaterial.Internal.d.ts +0 -7
  110. package/dist/materials/FXUnlitMaterial/FXUnlitMaterial.Internal.js +0 -103
  111. package/dist/materials/FXUnlitMaterial/FXUnlitMaterial.d.ts +0 -7
  112. package/dist/materials/FXUnlitMaterial/FXUnlitMaterial.js +0 -21
  113. package/dist/miscellaneous/color/FXColor.Internal.d.ts +0 -7
  114. package/dist/miscellaneous/color/FXColor.Internal.js +0 -42
  115. package/dist/miscellaneous/color/FXColor.d.ts +0 -234
  116. package/dist/miscellaneous/color/FXColor.js +0 -636
  117. package/dist/miscellaneous/curve/FXCurve1D.Internal.d.ts +0 -20
  118. package/dist/miscellaneous/curve/FXCurve1D.d.ts +0 -17
  119. package/dist/miscellaneous/curve/FXCurve1D.js +0 -94
  120. package/dist/miscellaneous/miscellaneous.d.ts +0 -34
  121. package/dist/miscellaneous/miscellaneous.js +0 -85
  122. package/dist/miscellaneous/texture/FXTextureView.Internal.d.ts +0 -49
  123. package/dist/miscellaneous/texture/FXTextureView.Internal.js +0 -6
  124. package/dist/miscellaneous/texture/FXTextureView.d.ts +0 -72
  125. package/dist/miscellaneous/texture/FXTextureView.js +0 -234
  126. package/dist/miscellaneous/webglCapabilities.d.ts +0 -1
  127. package/dist/miscellaneous/webglCapabilities.js +0 -45
  128. package/dist/node_modules/fast-simplex-noise/lib/2d.js +0 -93
  129. package/dist/node_modules/fast-simplex-noise/lib/3d.js +0 -142
  130. package/dist/node_modules/fast-simplex-noise/lib/4d.js +0 -196
  131. package/dist/node_modules/fast-simplex-noise/lib/mod.js +0 -24
  132. package/dist/nodes/FXNode.d.ts +0 -5
  133. package/dist/nodes/FXNode.js +0 -10
  134. package/dist/nodes/blending/FXNodeBlending.d.ts +0 -6
  135. package/dist/nodes/blending/FXNodeBlending.js +0 -9
  136. package/dist/nodes/blending/FXNodeLightnessBlendingMask.d.ts +0 -19
  137. package/dist/nodes/blending/FXNodeLightnessBlendingMask.js +0 -65
  138. package/dist/nodes/color/FXNodeColor.d.ts +0 -6
  139. package/dist/nodes/color/FXNodeColor.js +0 -9
  140. package/dist/nodes/color/FXNodeColorOverLife.d.ts +0 -22
  141. package/dist/nodes/color/FXNodeColorOverLife.js +0 -64
  142. package/dist/nodes/color/FXNodeSphericalClip.d.ts +0 -21
  143. package/dist/nodes/color/FXNodeSphericalClip.js +0 -54
  144. package/dist/nodes/normal/FXNodeFlatNormal.d.ts +0 -6
  145. package/dist/nodes/normal/FXNodeFlatNormal.js +0 -16
  146. package/dist/nodes/normal/FXNodeNormal.d.ts +0 -6
  147. package/dist/nodes/normal/FXNodeNormal.js +0 -9
  148. package/dist/nodes/normal/FXNodeSphericalNormal.d.ts +0 -6
  149. package/dist/nodes/normal/FXNodeSphericalNormal.js +0 -23
  150. package/dist/nodes/texture/FXNodeAnimatedTexture.d.ts +0 -52
  151. package/dist/nodes/texture/FXNodeAnimatedTexture.js +0 -151
  152. package/dist/nodes/texture/FXNodeStaticTexture.d.ts +0 -20
  153. package/dist/nodes/texture/FXNodeStaticTexture.js +0 -67
  154. package/dist/nodes/texture/FXNodeTexture.d.ts +0 -6
  155. package/dist/nodes/texture/FXNodeTexture.js +0 -9
@@ -1,64 +1,95 @@
1
- import { Mesh, InstancedBufferGeometry, BufferAttribute, Float32BufferAttribute, InstancedBufferAttribute, StreamDrawUsage } from 'three';
2
- import { BUILTIN_OFFSET_AGE, BUILTIN_OFFSET_LIFETIME, BUILTIN_OFFSET_POSITION_X, BUILTIN_OFFSET_POSITION_Y, BUILTIN_OFFSET_POSITION_Z } from '../miscellaneous/miscellaneous.js';
3
-
4
- class FXInstancedParticle extends Mesh {
5
- constructor(varyings, expectedCapacity, _private_capacityStep, material) {
1
+ import { InstancedBufferAttribute, InstancedBufferGeometry, Mesh, StreamDrawUsage } from "three";
2
+ import { FX_AGE, FX_CORE_LIFECYCLE, FX_CORE_POSITION, FX_LIFETIME, FX_POSITION_X, FX_POSITION_Y, FX_POSITION_Z, } from "../coreLayout.js";
3
+ /**
4
+ * @internal Instanced mesh backing one {@link FXEmitter}: a base primitive drawn once per particle,
5
+ * plus packed per-particle state. Each field (core `position`/`lifecycle` and each user attribute)
6
+ * is one `InstancedBufferAttribute` in {@link propertyBuffers}, exposed as `a_<name>`. All buffers
7
+ * share one particle index and stay dense: births append, deaths compact survivors down, capacity
8
+ * grows in `capacityStep` blocks - each just iterates every buffer in lockstep.
9
+ */
10
+ export class FXInstancedParticle extends Mesh {
11
+ constructor(varyings, expectedCapacity, capacityStep, material, baseGeometry) {
6
12
  const instancedGeometry = new InstancedBufferGeometry();
7
- const indices = new Uint16Array([0, 2, 1, 2, 3, 1]);
8
- instancedGeometry.setIndex(new BufferAttribute(indices, 1));
9
- const positions = new Float32Array([-0.5, 0.5, 0, 0.5, 0.5, 0, -0.5, -0.5, 0, 0.5, -0.5, 0]);
10
- instancedGeometry.setAttribute("position", new Float32BufferAttribute(positions, 3));
11
- const uvs = new Float32Array([0, 1, 1, 1, 0, 0, 1, 0]);
12
- instancedGeometry.setAttribute("uv", new Float32BufferAttribute(uvs, 2));
13
+ // Copy the base geometry's position/uv/normal + index onto the instanced geometry so the
14
+ // geometry-agnostic vertex epilogue transforms real mesh vertices. `baseGeometry` is caller-
15
+ // owned (a primitive or a custom mesh asset, possibly shared across emitters) - never disposed
16
+ // here.
17
+ const baseIndex = baseGeometry.getIndex();
18
+ if (baseIndex !== null) {
19
+ instancedGeometry.setIndex(baseIndex);
20
+ }
21
+ instancedGeometry.setAttribute("position", baseGeometry.getAttribute("position"));
22
+ instancedGeometry.setAttribute("uv", baseGeometry.getAttribute("uv"));
23
+ instancedGeometry.setAttribute("normal", baseGeometry.getAttribute("normal"));
13
24
  super(instancedGeometry, material);
14
- this._private_capacityStep = _private_capacityStep;
25
+ this.capacityStep = capacityStep;
15
26
  this.propertyBuffers = {};
16
- this._private_sortingIndices = new Int32Array(0);
17
- this._private_sortingSquaredDistances = new Float64Array(0);
18
- this._private_sortingTemporaryBuffer = new Float32Array(0);
27
+ this.bufferVersionInternal = 0;
28
+ this.sortingIndices = new Int32Array(0);
29
+ this.sortingSquaredDistances = new Float64Array(0);
30
+ this.sortingTemporaryBuffer = new Float32Array(0);
19
31
  this.frustumCulled = false;
20
- this._private_instancedGeometry = instancedGeometry;
21
- this._private_instancedGeometry.instanceCount = 0;
22
- this._private_particleMaterial = material;
23
- this._private_capacity = Math.max(Math.ceil(expectedCapacity / this._private_capacityStep) * this._private_capacityStep, this._private_capacityStep);
32
+ this.instancedGeometry = instancedGeometry;
33
+ this.instancedGeometry.instanceCount = 0;
34
+ this.capacity = Math.max(Math.ceil(expectedCapacity / this.capacityStep) * this.capacityStep, this.capacityStep);
24
35
  for (const name in varyings) {
25
36
  const { bufferSize } = varyings[name];
26
- const attribute = new InstancedBufferAttribute(new Float32Array(this._private_capacity * bufferSize), bufferSize);
37
+ const attribute = new InstancedBufferAttribute(new Float32Array(this.capacity * bufferSize), bufferSize);
27
38
  attribute.setUsage(StreamDrawUsage);
28
- this._private_instancedGeometry.setAttribute(`a_${name}`, attribute);
39
+ this.instancedGeometry.setAttribute(`a_${name}`, attribute);
29
40
  this.propertyBuffers[name] = attribute;
30
41
  }
31
42
  }
32
43
  get instanceCount() {
33
- return this._private_instancedGeometry.instanceCount;
44
+ return this.instancedGeometry.instanceCount;
34
45
  }
35
46
  get instanceCapacity() {
36
- return this._private_capacity;
47
+ return this.capacity;
48
+ }
49
+ // Bumped on every backing-array reallocation (capacity growth), so the emitter can cache its
50
+ // per-tick name -> Float32Array record and rebuild only when this moves.
51
+ get bufferVersion() {
52
+ return this.bufferVersionInternal;
37
53
  }
38
54
  createInstances(count) {
39
- const currentInstanceCount = this._private_instancedGeometry.instanceCount;
40
- this._private_ensureCapacity(currentInstanceCount + count);
41
- this._private_instancedGeometry.instanceCount += count;
55
+ const currentInstanceCount = this.instancedGeometry.instanceCount;
56
+ this.ensureCapacity(currentInstanceCount + count);
57
+ // Rows freed by `removeDeadParticles` (copy-down, no zeroing) still hold the previous occupant's
58
+ // state - most dangerously a stale `age >= lifetime`, which no spawn graph can reset (the spawn
59
+ // target has no `age` slot). Zero the claimed range so births start clean; flag each buffer so
60
+ // the cleared rows reach the GPU even for buffers the spawn kernel does not itself write.
61
+ const rangeBegin = currentInstanceCount;
62
+ const rangeEnd = currentInstanceCount + count;
63
+ for (const attribute of Object.values(this.propertyBuffers)) {
64
+ const { array, itemSize } = attribute;
65
+ array.fill(0, rangeBegin * itemSize, rangeEnd * itemSize);
66
+ attribute.needsUpdate = true;
67
+ }
68
+ this.instancedGeometry.instanceCount += count;
42
69
  }
43
70
  removeDeadParticles() {
44
- const builtinBuffer = this.propertyBuffers.builtin;
45
- if (builtinBuffer === undefined) {
71
+ // Cull against core `lifecycle` (vec2 [age, lifetime]); alive while `age < lifetime`. Rides the
72
+ // same copy-down as every other property buffer.
73
+ const lifecycleBuffer = this.propertyBuffers[FX_CORE_LIFECYCLE];
74
+ if (lifecycleBuffer === undefined) {
46
75
  return;
47
76
  }
48
- const { array: builtinArray, itemSize: builtinItemSize } = builtinBuffer;
49
- const { instanceCount } = this._private_instancedGeometry;
77
+ const { array: lifecycleArray, itemSize: lifecycleItemSize } = lifecycleBuffer;
78
+ const { instanceCount } = this.instancedGeometry;
79
+ // Materialized once per call, not per survivor: a `for...in` inside the copy-down loop would
80
+ // re-enumerate keys for every moved particle.
81
+ const attributes = Object.values(this.propertyBuffers);
50
82
  let writeIndex = 0;
51
83
  let didCompact = false;
52
84
  for (let readIndex = 0; readIndex < instanceCount; readIndex++) {
53
- const offset = readIndex * builtinItemSize;
54
- if (builtinArray[offset + BUILTIN_OFFSET_AGE] < builtinArray[offset + BUILTIN_OFFSET_LIFETIME]) {
85
+ const offset = readIndex * lifecycleItemSize;
86
+ if (lifecycleArray[offset + FX_AGE] < lifecycleArray[offset + FX_LIFETIME]) {
55
87
  if (writeIndex !== readIndex) {
56
- for (const name in this.propertyBuffers) {
57
- const attribute = this.propertyBuffers[name];
88
+ for (const attribute of attributes) {
58
89
  const { itemSize: dataItemSize, array: dataArray } = attribute;
59
- const srcOffset = readIndex * dataItemSize;
60
- const dstOffset = writeIndex * dataItemSize;
61
- dataArray.copyWithin(dstOffset, srcOffset, srcOffset + dataItemSize);
90
+ const sourceOffset = readIndex * dataItemSize;
91
+ const destinationOffset = writeIndex * dataItemSize;
92
+ dataArray.copyWithin(destinationOffset, sourceOffset, sourceOffset + dataItemSize);
62
93
  }
63
94
  didCompact = true;
64
95
  }
@@ -66,36 +97,60 @@ class FXInstancedParticle extends Mesh {
66
97
  }
67
98
  }
68
99
  if (didCompact) {
69
- for (const name in this.propertyBuffers) {
70
- this.propertyBuffers[name].needsUpdate = true;
100
+ for (const attribute of attributes) {
101
+ attribute.needsUpdate = true;
71
102
  }
72
103
  }
73
- this._private_instancedGeometry.instanceCount = writeIndex;
104
+ this.instancedGeometry.instanceCount = writeIndex;
74
105
  }
75
- sortByDistance(cameraWorldPosition) {
76
- const { instanceCount } = this._private_instancedGeometry;
106
+ /**
107
+ * Reorders every per-particle buffer back-to-front for correct alpha blending. Distances are
108
+ * world-space (each center transformed by `meshWorldMatrix`), matching the shader's own
109
+ * `p_cameraDistance` - correct under any transform, including non-uniform scale and shear.
110
+ */
111
+ sortByDistance(cameraWorldPosition, meshWorldMatrix) {
112
+ const { instanceCount } = this.instancedGeometry;
77
113
  if (instanceCount < 2) {
78
114
  return;
79
115
  }
80
- const builtinBuffer = this.propertyBuffers.builtin;
81
- if (builtinBuffer === undefined) {
116
+ const positionBuffer = this.propertyBuffers[FX_CORE_POSITION];
117
+ if (positionBuffer === undefined) {
82
118
  return;
83
119
  }
84
- if (this._private_sortingIndices.length < instanceCount) {
85
- this._private_sortingIndices = new Int32Array(this._private_capacity);
86
- this._private_sortingSquaredDistances = new Float64Array(this._private_capacity);
120
+ if (this.sortingIndices.length < instanceCount) {
121
+ this.sortingIndices = new Int32Array(this.capacity);
122
+ this.sortingSquaredDistances = new Float64Array(this.capacity);
87
123
  }
88
- const { array: builtinArray, itemSize: builtinItemSize } = builtinBuffer;
124
+ const { array: positionArray, itemSize: positionItemSize } = positionBuffer;
125
+ // Column-major elements, indexed directly to avoid a per-particle Vector3.
126
+ const elements = meshWorldMatrix.elements;
89
127
  for (let particleIndex = 0; particleIndex < instanceCount; particleIndex++) {
90
- this._private_sortingIndices[particleIndex] = particleIndex;
91
- const itemOffset = particleIndex * builtinItemSize;
92
- const dx = builtinArray[itemOffset + BUILTIN_OFFSET_POSITION_X] - cameraWorldPosition.x;
93
- const dy = builtinArray[itemOffset + BUILTIN_OFFSET_POSITION_Y] - cameraWorldPosition.y;
94
- const dz = builtinArray[itemOffset + BUILTIN_OFFSET_POSITION_Z] - cameraWorldPosition.z;
95
- this._private_sortingSquaredDistances[particleIndex] = dx * dx + dy * dy + dz * dz;
128
+ this.sortingIndices[particleIndex] = particleIndex;
129
+ const itemOffset = particleIndex * positionItemSize;
130
+ const positionX = positionArray[itemOffset + FX_POSITION_X];
131
+ const positionY = positionArray[itemOffset + FX_POSITION_Y];
132
+ const positionZ = positionArray[itemOffset + FX_POSITION_Z];
133
+ // meshWorldMatrix * vec4(center, 1.0) - bottom row is (0,0,0,1), so no w divide.
134
+ const deltaX = elements[0] * positionX +
135
+ elements[4] * positionY +
136
+ elements[8] * positionZ +
137
+ elements[12] -
138
+ cameraWorldPosition.x;
139
+ const deltaY = elements[1] * positionX +
140
+ elements[5] * positionY +
141
+ elements[9] * positionZ +
142
+ elements[13] -
143
+ cameraWorldPosition.y;
144
+ const deltaZ = elements[2] * positionX +
145
+ elements[6] * positionY +
146
+ elements[10] * positionZ +
147
+ elements[14] -
148
+ cameraWorldPosition.z;
149
+ this.sortingSquaredDistances[particleIndex] =
150
+ deltaX * deltaX + deltaY * deltaY + deltaZ * deltaZ;
96
151
  }
97
- const sortingSquaredDistances = this._private_sortingSquaredDistances;
98
- const sortingIndices = this._private_sortingIndices.subarray(0, instanceCount);
152
+ const sortingSquaredDistances = this.sortingSquaredDistances;
153
+ const sortingIndices = this.sortingIndices.subarray(0, instanceCount);
99
154
  sortingIndices.sort((indexA, indexB) => sortingSquaredDistances[indexB] - sortingSquaredDistances[indexA]);
100
155
  let maximumItemSize = 0;
101
156
  for (const name in this.propertyBuffers) {
@@ -104,8 +159,8 @@ class FXInstancedParticle extends Mesh {
104
159
  }
105
160
  }
106
161
  const requiredTemporaryBufferSize = instanceCount * maximumItemSize;
107
- if (this._private_sortingTemporaryBuffer.length < requiredTemporaryBufferSize) {
108
- this._private_sortingTemporaryBuffer = new Float32Array(this._private_capacity * maximumItemSize);
162
+ if (this.sortingTemporaryBuffer.length < requiredTemporaryBufferSize) {
163
+ this.sortingTemporaryBuffer = new Float32Array(this.capacity * maximumItemSize);
109
164
  }
110
165
  for (const name in this.propertyBuffers) {
111
166
  const attribute = this.propertyBuffers[name];
@@ -115,28 +170,38 @@ class FXInstancedParticle extends Mesh {
115
170
  const sourceOffset = originalIndex * itemSize;
116
171
  const destinationOffset = newIndex * itemSize;
117
172
  for (let componentIndex = 0; componentIndex < itemSize; componentIndex++) {
118
- this._private_sortingTemporaryBuffer[destinationOffset + componentIndex] =
173
+ this.sortingTemporaryBuffer[destinationOffset + componentIndex] =
119
174
  array[sourceOffset + componentIndex];
120
175
  }
121
176
  }
122
177
  for (let i = 0; i < instanceCount * itemSize; i++) {
123
- array[i] = this._private_sortingTemporaryBuffer[i];
178
+ array[i] = this.sortingTemporaryBuffer[i];
124
179
  }
125
180
  attribute.needsUpdate = true;
126
181
  }
127
182
  }
128
183
  drop() {
129
- this._private_instancedGeometry.instanceCount = 0;
184
+ this.instancedGeometry.instanceCount = 0;
130
185
  }
186
+ // Disposes the instanced geometry and the currently mounted material - not the constructor one,
187
+ // which a live material hot-swap may have replaced and disposed.
131
188
  destroy() {
132
- this._private_instancedGeometry.dispose();
133
- this._private_particleMaterial.dispose();
189
+ // position/uv/normal + index were borrowed by reference from the caller-owned base geometry
190
+ // (constructor) - possibly shared with other live consumers of the same custom mesh asset.
191
+ // Detach them first so dispose()'s GPU-buffer cascade only touches the `a_<name>` buffers this
192
+ // instance actually owns, not a shared consumer's GPU cache.
193
+ this.instancedGeometry.deleteAttribute("position");
194
+ this.instancedGeometry.deleteAttribute("uv");
195
+ this.instancedGeometry.deleteAttribute("normal");
196
+ this.instancedGeometry.setIndex(null);
197
+ this.instancedGeometry.dispose();
198
+ this.material?.dispose();
134
199
  }
135
- _private_ensureCapacity(requiredCapacity) {
136
- if (requiredCapacity <= this._private_capacity) {
200
+ ensureCapacity(requiredCapacity) {
201
+ if (requiredCapacity <= this.capacity) {
137
202
  return;
138
203
  }
139
- const newCapacity = Math.ceil(requiredCapacity / this._private_capacityStep) * this._private_capacityStep;
204
+ const newCapacity = Math.ceil(requiredCapacity / this.capacityStep) * this.capacityStep;
140
205
  for (const name in this.propertyBuffers) {
141
206
  const oldAttribute = this.propertyBuffers[name];
142
207
  const { itemSize, array, usage } = oldAttribute;
@@ -144,27 +209,22 @@ class FXInstancedParticle extends Mesh {
144
209
  newArray.set(array);
145
210
  const newAttribute = new InstancedBufferAttribute(newArray, itemSize);
146
211
  newAttribute.setUsage(usage);
147
- this._private_instancedGeometry.setAttribute(`a_${name}`, newAttribute);
212
+ this.instancedGeometry.setAttribute(`a_${name}`, newAttribute);
148
213
  this.propertyBuffers[name] = newAttribute;
149
- // Detach old typed array to help GC release GPU-side memory sooner.
150
- // Three.js does not expose dispose() on BufferAttribute directly,
151
- // but clearing the array reference prevents the old data from lingering.
152
- oldAttribute.array = null;
214
+ // Dropping the CPU array does NOT free the GPU buffer: three's WebGLAttributes holds it in a
215
+ // WeakMap keyed by the (now-replaced) BufferAttribute and only deletes it when that wrapper is
216
+ // GC'd (no dispose() on BufferAttribute across r157-179). Best-effort, not deterministic.
217
+ oldAttribute.array = undefined;
153
218
  }
154
- this._private_capacity = newCapacity;
155
- // Force the renderer to recalculate _maxInstanceCount on the next
156
- // setupVertexAttributes pass. The new InstancedBufferAttributes will
157
- // trigger a VAO rebind, at which point _maxInstanceCount is recomputed
158
- // from the current attribute sizes.
159
- //
160
- // _maxInstanceCount is an internal renderer cache field set by
161
- // WebGLBindingStates.js. It was never promoted to a public API in the
162
- // r157-r180 range. Deleting it is the standard workaround used across
163
- // the Three.js ecosystem (see #19706, #26363, #27205).
164
- //
219
+ this.capacity = newCapacity;
220
+ this.bufferVersionInternal++;
221
+ this.invalidateInstanceCap();
222
+ }
223
+ // Force the renderer to recompute `_maxInstanceCount` on the next setupVertexAttributes pass. It
224
+ // is an internal WebGLBindingStates cache, never promoted to public API across r157-r180; deleting
225
+ // it is the standard ecosystem workaround (three #19706, #26363, #27205).
226
+ invalidateInstanceCap() {
165
227
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
166
- delete this._private_instancedGeometry._maxInstanceCount;
228
+ delete this.instancedGeometry._maxInstanceCount;
167
229
  }
168
230
  }
169
-
170
- export { FXInstancedParticle };
@@ -0,0 +1,5 @@
1
+ export interface GLTypeInfo {
2
+ glslTypeName: string;
3
+ bufferSize: number;
4
+ instantiable: boolean;
5
+ }
@@ -0,0 +1,3 @@
1
+ // GLTypeInfo (GLSL type name + float stride + instantiability). The emitter and material
2
+ // adapters size the per-particle buffers off these strides.
3
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,30 @@
1
+ import { BoxGeometry, PlaneGeometry, SphereGeometry } from "three";
2
+ /**
3
+ * @internal Base (non-instanced) mesh a {@link FXInstancedParticle} instances once per particle.
4
+ * Carries standard `position`/`uv`/`normal` + index so the geometry-agnostic vertex epilogue can
5
+ * transform real mesh vertices. `"plane"` is the unit XY quad (+Z normal) used for billboards.
6
+ * The result is transient - the caller copies its attributes and discards it.
7
+ */
8
+ export function buildPrimitiveGeometry(primitive) {
9
+ switch (primitive) {
10
+ case "plane":
11
+ return new PlaneGeometry(1, 1);
12
+ case "box":
13
+ return new BoxGeometry(1, 1, 1);
14
+ case "sphere":
15
+ return new SphereGeometry(0.5, 24, 16);
16
+ }
17
+ }
18
+ /**
19
+ * @internal Resolves an artifact's {@link FXGeometrySource} to a concrete `BufferGeometry`: a
20
+ * built-in primitive is built fresh; a `"custom"` slot is looked up by name in `customGeometries`
21
+ * (app-supplied, e.g. from the exported module's `assets` or the editor's own live content
22
+ * library) - falling back to the `"plane"` primitive when the name is unbound, the same graceful
23
+ * degradation an unbound external texture slot gets.
24
+ */
25
+ export function resolveGeometrySource(source, customGeometries) {
26
+ if (source === undefined || source.type === "primitive") {
27
+ return buildPrimitiveGeometry(source?.primitive ?? "plane");
28
+ }
29
+ return customGeometries[source.external] ?? buildPrimitiveGeometry("plane");
30
+ }
@@ -1,21 +1,4 @@
1
- /**
2
- * Validates value is finite number within safe range.
3
- * @param value Number to validate
4
- * @param subject Value name for error messages
5
- * @throws If value is not finite or exceeds safe range
6
- */
7
1
  export declare function assertValidNumber(value: number, subject: string): void;
8
- /**
9
- * Validates value is positive (>= EPSILON).
10
- * @param value Number to validate
11
- * @param subject Value name for error messages
12
- * @throws If value is invalid or below EPSILON
13
- */
14
2
  export declare function assertValidPositiveNumber(value: number, subject: string): void;
15
- /**
16
- * Validates value is non-negative (>= 0).
17
- * @param value Number to validate
18
- * @param subject Value name for error messages
19
- * @throws If value is invalid or negative
20
- */
21
3
  export declare function assertValidNonNegativeNumber(value: number, subject: string): void;
4
+ export declare function assertValidPositiveInteger(value: number, subject: string): void;
@@ -1,13 +1,10 @@
1
- import { EPSILON } from './math.js';
2
-
3
- /**
4
- * Validates value is finite number within safe range.
5
- * @param value Number to validate
6
- * @param subject Value name for error messages
7
- * @throws If value is not finite or exceeds safe range
8
- */
9
- function assertValidNumber(value, subject) {
10
- if (process.env.NODE_ENV !== "production") {
1
+ import { EPSILON } from "./math.js";
2
+ // `typeof process` short-circuits before `process.env`, so the published ESM never throws a
3
+ // ReferenceError under import maps / a CDN (no bundler `process` shim). Bundlers still inline
4
+ // `process.env.NODE_ENV`, so DEV validation drops out of production builds.
5
+ const DEV = typeof process === "undefined" || process.env.NODE_ENV !== "production";
6
+ export function assertValidNumber(value, subject) {
7
+ if (DEV) {
11
8
  if (!Number.isFinite(value)) {
12
9
  throw new Error(`${subject}: value must be a finite number`);
13
10
  }
@@ -16,33 +13,29 @@ function assertValidNumber(value, subject) {
16
13
  }
17
14
  }
18
15
  }
19
- /**
20
- * Validates value is positive (>= EPSILON).
21
- * @param value Number to validate
22
- * @param subject Value name for error messages
23
- * @throws If value is invalid or below EPSILON
24
- */
25
- function assertValidPositiveNumber(value, subject) {
26
- if (process.env.NODE_ENV !== "production") {
16
+ export function assertValidPositiveNumber(value, subject) {
17
+ if (DEV) {
27
18
  assertValidNumber(value, subject);
28
19
  if (value < EPSILON) {
29
20
  throw new Error(`${subject}: value must be greater than or equal to ${EPSILON}`);
30
21
  }
31
22
  }
32
23
  }
33
- /**
34
- * Validates value is non-negative (>= 0).
35
- * @param value Number to validate
36
- * @param subject Value name for error messages
37
- * @throws If value is invalid or negative
38
- */
39
- function assertValidNonNegativeNumber(value, subject) {
40
- if (process.env.NODE_ENV !== "production") {
24
+ export function assertValidNonNegativeNumber(value, subject) {
25
+ if (DEV) {
41
26
  assertValidNumber(value, subject);
42
27
  if (value < 0) {
43
28
  throw new Error(`${subject}: value must be greater than or equal to 0`);
44
29
  }
45
30
  }
46
31
  }
47
-
48
- export { assertValidNonNegativeNumber, assertValidNumber, assertValidPositiveNumber };
32
+ // Guards counts/capacities that reach `instanceCount` or a `Float32Array` length, where a
33
+ // fractional value would draw a truncated instance count or throw a `RangeError`.
34
+ export function assertValidPositiveInteger(value, subject) {
35
+ if (DEV) {
36
+ assertValidPositiveNumber(value, subject);
37
+ if (!Number.isInteger(value)) {
38
+ throw new Error(`${subject}: value must be an integer`);
39
+ }
40
+ }
41
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * CPU-side twins of the compiler's math/noise builtins (`fxMix`, `fxSnoise3`, ...). A behavior
3
+ * kernel's authored `spawn`/`update` body calls these by name - the editor's compiler prints those
4
+ * exact calls - so every exported name here is a frozen contract with already-compiled kernels,
5
+ * like the `a_`/`p_` buffer prefixes. Never rename one without updating the compiler side in
6
+ * lockstep.
7
+ */
8
+ export declare function fxFract(x: number): number;
9
+ export declare function fxMod(a: number, b: number): number;
10
+ export declare function fxMix(a: number, b: number, t: number): number;
11
+ export declare function fxSmoothstep(edge0: number, edge1: number, x: number): number;
12
+ export declare function fxHash(n: number): number;
13
+ /** Cheap deterministic 1D value-noise (smoothed hash interpolation), range [-1, 1]. */
14
+ export declare function fxValueNoise(x: number): number;
15
+ /** Fractal Brownian motion over {@link fxValueNoise} (gain 0.5, lacunarity 2). */
16
+ export declare function fxFbm(x: number, octaves: number): number;
17
+ /** Linearly samples a curve LUT at `t` in [0, 1] (clamped), interpolating between entries. */
18
+ export declare function fxSampleLut(lut: ArrayLike<number>, t: number): number;
19
+ /**
20
+ * Ashima/Gustavson "webgl-noise" simplex noise, 2D - the CPU twin of the compiler's GLSL `snoise`
21
+ * helper. Component arguments (rather than a vector) so it mirrors how the scalarized JS backend calls
22
+ * it; the arithmetic tracks the GLSL version op-for-op. Output is roughly `[-1, 1]`.
23
+ */
24
+ export declare function fxSnoise2(vx: number, vy: number): number;
25
+ /** 3D twin of {@link fxSnoise2}, mirroring the GLSL `snoise(vec3)` overload op-for-op. */
26
+ export declare function fxSnoise3(vx: number, vy: number, vz: number): number;