babylon.quarks 0.24.1 → 0.26.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.
@@ -53,7 +53,25 @@ import {BatchedRenderer, VFXBatchSettings} from './BatchedRenderer';
53
53
  import {ensureEnvAtlasFromCube, getCachedEnvAtlas} from './envAtlas';
54
54
  import {ensureTriangleIndices} from './geometryUtil';
55
55
  import {ParticleEmitter} from './ParticleEmitter';
56
- import {MeshLighting, MeshRim, normalizeMeshRim, RenderMode, SortMode} from './VFXBatch';
56
+ import {
57
+ MeshLighting,
58
+ MeshRim,
59
+ normalizeErosion,
60
+ normalizeGradientMap,
61
+ normalizeMapUV,
62
+ normalizeMeshRim,
63
+ normalizeUVDistortion,
64
+ ParticleErosion,
65
+ ParticleErosionOptions,
66
+ ParticleGradientMap,
67
+ ParticleUVDistortion,
68
+ ParticleUVDistortionOptions,
69
+ RenderMode,
70
+ SortMode,
71
+ TextureUV,
72
+ textureUVToJSON,
73
+ UV_DISTORTION_DEFAULT_UV,
74
+ } from './VFXBatch';
57
75
 
58
76
  export interface BurstParameters {
59
77
  time: number;
@@ -81,6 +99,27 @@ export interface BurstParameters {
81
99
  */
82
100
  export type CullingMode = 'always' | 'pause' | 'pauseAndCatchUp';
83
101
 
102
+ /**
103
+ * What the emitter's world scale does to a world-space system (Unity's
104
+ * Scaling Mode, as far as it differs here). A local-space system lives in its
105
+ * emitter's space, so its scale reaches everything either way.
106
+ *
107
+ * - `'shape'` (default): the scale sizes the emission shape and the
108
+ * particles; speeds, forces and speed limits stay in world units, so a
109
+ * scaled-down effect moves as far as the full-size one.
110
+ * - `'hierarchy'`: the scale also multiplies start speeds (per axis),
111
+ * `ForceOverLife` and `LimitSpeedOverLife` (by the mean of the axes), so the
112
+ * whole effect, motion included, scales like a model would.
113
+ */
114
+ export type ScalingMode = 'shape' | 'hierarchy';
115
+
116
+ /**
117
+ * Where a {@link RenderMode.Beam} system's quads end: a point in world space,
118
+ * or anything with a world position, such as a Babylon `TransformNode`, read
119
+ * every frame.
120
+ */
121
+ export type BeamEnd = {x: number; y: number; z: number} | {getAbsolutePosition(): {x: number; y: number; z: number}};
122
+
84
123
  /** Particle backed by a {@link ParticleStore} row, as this system always creates. */
85
124
  type StoreBackedParticle = Particle & {storeIndex: number; setStoreIndex(index: number): void};
86
125
 
@@ -132,8 +171,19 @@ const DEFAULT_POSITIONS = new Float32Array([-0.5, -0.5, 0, 0.5, -0.5, 0, 0.5, 0.
132
171
  const DEFAULT_UVS = new Float32Array([0, 0, 1, 0, 1, 1, 0, 1]);
133
172
 
134
173
  /** Zero UVs sized to a position buffer — missing/mismatched uv attrs → GL_INVALID_OPERATION on iOS. */
174
+ const fallbackUVs = new WeakMap<Float32Array, Float32Array>();
175
+ const fallbackNormals = new WeakMap<Float32Array, Float32Array>();
176
+ /**
177
+ * Zero UVs for a geometry given without any, one array per geometry: batches
178
+ * are keyed by the UV array too, so systems sharing a geometry share it.
179
+ */
135
180
  function fallbackUVsForPositions(positions: Float32Array): Float32Array {
136
- return new Float32Array((positions.length / 3) * 2);
181
+ let uvs = fallbackUVs.get(positions);
182
+ if (uvs === undefined || uvs.length !== (positions.length / 3) * 2) {
183
+ uvs = new Float32Array((positions.length / 3) * 2);
184
+ fallbackUVs.set(positions, uvs);
185
+ }
186
+ return uvs;
137
187
  }
138
188
  const DEFAULT_INDICES = new Uint32Array([0, 1, 2, 0, 2, 3]);
139
189
 
@@ -147,6 +197,24 @@ function trailFlags(settings: TrailSettings | undefined): Partial<TrailSettings>
147
197
  return flags;
148
198
  }
149
199
 
200
+ /** A trail's side axis as JSON `[x, y, z]`, or nothing when unset. */
201
+ function trailSideAxisJSON(settings: TrailSettings | undefined): {sideAxis?: number[]} {
202
+ const axis = settings?.sideAxis;
203
+ return axis ? {sideAxis: [axis.x, axis.y, axis.z]} : {};
204
+ }
205
+
206
+ /** A trail's side axis from JSON `[x, y, z]` (or an object with x, y, z), or nothing. */
207
+ function trailSideAxisFromJSON(value: unknown): {sideAxis?: Vector3} {
208
+ if (Array.isArray(value)) {
209
+ return {sideAxis: new Vector3(Number(value[0]) || 0, Number(value[1]) || 0, Number(value[2]) || 0)};
210
+ }
211
+ if (value && typeof value === 'object') {
212
+ const v = value as {x?: number; y?: number; z?: number};
213
+ return {sideAxis: new Vector3(v.x ?? 0, v.y ?? 0, v.z ?? 0)};
214
+ }
215
+ return {};
216
+ }
217
+
150
218
  export interface ParticleSystemParameters {
151
219
  autoDestroy?: boolean;
152
220
  looping?: boolean;
@@ -170,6 +238,12 @@ export interface ParticleSystemParameters {
170
238
  instancingGeometry?: Float32Array;
171
239
  instancingIndices?: Uint32Array | Uint16Array;
172
240
  instancingUVs?: Float32Array;
241
+ /**
242
+ * A second UV set for the mesh (glTF `TEXCOORD_1`, Unity's `uv2`), what
243
+ * textures on UV channel 1 read on a mesh particle; see `UVChannel`.
244
+ * Default none: channel 1 reads the first set.
245
+ */
246
+ instancingUVs2?: Float32Array;
173
247
  instancingNormals?: Float32Array;
174
248
  renderMode?: RenderMode;
175
249
  rendererEmitterSettings?: RendererEmitterSettings;
@@ -199,10 +273,20 @@ export interface ParticleSystemParameters {
199
273
  lighting?: MeshLighting;
200
274
  /** Rim (Fresnel) term on mesh particles, see {@link ParticleSystem.rim}. Default none. */
201
275
  rim?: Partial<MeshRim>;
276
+ /** The main map's layout, see {@link ParticleSystem.mapUV}. Default as drawn. */
277
+ mapUV?: Partial<TextureUV>;
278
+ /** Colour ramp for the map, see {@link ParticleSystem.gradientMap}. Default none. */
279
+ gradientMap?: Partial<ParticleGradientMap>;
280
+ /** Alpha erosion, see {@link ParticleSystem.erosion}. Default none. */
281
+ erosion?: ParticleErosionOptions;
282
+ /** Map UV distortion, see {@link ParticleSystem.uvDistortion}. Default none. */
283
+ uvDistortion?: ParticleUVDistortionOptions;
202
284
  /** Multiplier on the time the system is advanced by, see {@link ParticleSystem.playbackSpeed}. Default 1. */
203
285
  playbackSpeed?: number;
204
286
  /** What the system does while off screen, see {@link CullingMode}. Default `'always'`. */
205
287
  cullingMode?: CullingMode;
288
+ /** What the emitter's scale does to a world-space system, see {@link ScalingMode}. Default `'shape'`. */
289
+ scalingMode?: ScalingMode;
206
290
  /** Seed for this system's own random generator, see {@link ParticleSystem.seed}. Default none. */
207
291
  seed?: number;
208
292
  /** Draw order within the batch, see {@link ParticleSystem.sortMode}. Default `'none'`. */
@@ -258,6 +342,8 @@ export interface ParticleSystemJSONParameters {
258
342
  playbackSpeed?: number;
259
343
  /** Written only when not `'always'`. */
260
344
  cullingMode?: CullingMode;
345
+ /** Written only when not `'shape'`. */
346
+ scalingMode?: ScalingMode;
261
347
  /** Written only when set. */
262
348
  seed?: number;
263
349
  /** Written only when not `'none'`. */
@@ -587,6 +673,99 @@ export class ParticleSystem implements IParticleSystem {
587
673
  this.neededToUpdateRender = true;
588
674
  }
589
675
 
676
+ /**
677
+ * How the main map is laid over the particle, see {@link TextureUV}: its
678
+ * UV channel, tiling and offset (Unity's Tiling / Offset) and a scroll on
679
+ * the renderer's clock (Unreal's Panner), for flowing energy, lava or a
680
+ * smear running along a mesh. A scrolled or tiled map should repeat. On a
681
+ * sprite sheet (UV channel 0) the layout applies to the sheet's UV, so a
682
+ * scroll or tiling there runs into the neighbouring cells. Undefined (the
683
+ * default), or the identity: the map as drawn. Fields left out take their
684
+ * defaults; assign a new object to change it. Saved with the material in
685
+ * the effect JSON.
686
+ */
687
+ get mapUV(): TextureUV | undefined {
688
+ return this.rendererSettings.mapUV;
689
+ }
690
+
691
+ set mapUV(uv: Partial<TextureUV> | undefined) {
692
+ this.rendererSettings.mapUV = normalizeMapUV(uv);
693
+ this.neededToUpdateRender = true;
694
+ }
695
+
696
+ /**
697
+ * Colour ramp the map is recoloured through (a gradient map), see
698
+ * {@link ParticleGradientMap}: a channel of the map, or its luminance,
699
+ * picks a colour along U of the ramp; the particle colour still tints the
700
+ * result. One greyscale or channel-packed texture then serves every
701
+ * palette. Undefined (the default), or without a ramp: none. Assign a new
702
+ * object to change it. Saved with the material in the effect JSON.
703
+ */
704
+ get gradientMap(): ParticleGradientMap | undefined {
705
+ return this.rendererSettings.gradientMap;
706
+ }
707
+
708
+ set gradientMap(gradientMap: Partial<ParticleGradientMap> | undefined) {
709
+ this.rendererSettings.gradientMap = normalizeGradientMap(gradientMap);
710
+ this.neededToUpdateRender = true;
711
+ }
712
+
713
+ /**
714
+ * Alpha erosion, see {@link ParticleErosion}: the particle's alpha decides
715
+ * how much of the shape is left rather than how transparent it is, so a
716
+ * `ColorOverLife` alpha curve dissolves the shape through a noise mask or
717
+ * wipes it through a gradient one — and an effect told to stop can fade
718
+ * out that way. Undefined (the default): alpha is plain transparency.
719
+ * Fields left out take their defaults; assign a new object to change it.
720
+ * Saved with the material in the effect JSON.
721
+ */
722
+ get erosion(): ParticleErosion | undefined {
723
+ return this.rendererSettings.erosion;
724
+ }
725
+
726
+ set erosion(erosion: ParticleErosionOptions | undefined) {
727
+ this.rendererSettings.erosion = normalizeErosion(erosion);
728
+ this.neededToUpdateRender = true;
729
+ }
730
+
731
+ /**
732
+ * Map UV distortion, see {@link ParticleUVDistortion}: a scrolling noise
733
+ * texture pushes the map's UV about, for heat shimmer and licking,
734
+ * breathing shapes. Undefined (the default), or without a map: none.
735
+ * Fields left out take their defaults; assign a new object to change it.
736
+ * Saved with the material in the effect JSON.
737
+ */
738
+ get uvDistortion(): ParticleUVDistortion | undefined {
739
+ return this.rendererSettings.uvDistortion;
740
+ }
741
+
742
+ set uvDistortion(distortion: ParticleUVDistortionOptions | undefined) {
743
+ this.rendererSettings.uvDistortion = normalizeUVDistortion(distortion);
744
+ this.neededToUpdateRender = true;
745
+ }
746
+
747
+ /**
748
+ * The emitter's world scale per axis, measured with {@link motionScale}
749
+ * before every step; what `'hierarchy'` multiplies start speeds by. Taken
750
+ * from the system's own emitter rather than the emission matrix, whose
751
+ * columns carry a parent particle's velocity for a sub emitter.
752
+ */
753
+ private readonly hierarchyScale = new Vector3(1, 1, 1);
754
+
755
+ /** The mean world scale of the emitter for a world-space `'hierarchy'` system, else 1. */
756
+ private measureMotionScale(): number {
757
+ if (this.scalingMode !== 'hierarchy' || !this.worldSpace) {
758
+ this.hierarchyScale.set(1, 1, 1);
759
+ return 1;
760
+ }
761
+ const e = this.emitter.matrixWorld.elements;
762
+ const sx = Math.sqrt(e[0] * e[0] + e[1] * e[1] + e[2] * e[2]);
763
+ const sy = Math.sqrt(e[4] * e[4] + e[5] * e[5] + e[6] * e[6]);
764
+ const sz = Math.sqrt(e[8] * e[8] + e[9] * e[9] + e[10] * e[10]);
765
+ this.hierarchyScale.set(sx, sy, sz);
766
+ return (sx + sy + sz) / 3;
767
+ }
768
+
590
769
  /** `lighting` if it is one of the modes, else the default. */
591
770
  private static validLighting(lighting: unknown): MeshLighting {
592
771
  return lighting === 'unlit' || lighting === 'scene' ? lighting : 'fixed';
@@ -630,10 +809,53 @@ export class ParticleSystem implements IParticleSystem {
630
809
  return this.rendererSettings.instancingGeometry;
631
810
  }
632
811
 
812
+ /**
813
+ * The mesh particles' positions. A new geometry with a different vertex
814
+ * count drops UV sets and normals that no longer fit it: the first UV set
815
+ * falls back to zeros, the second to none, the normals to +Z. Assign {@link instancingUVs} and
816
+ * {@link instancingUVs2} after it.
817
+ */
633
818
  set instancingGeometry(geometry: Float32Array) {
634
819
  this.restart();
635
820
  this.particles.length = 0;
636
- this.rendererSettings.instancingGeometry = geometry;
821
+ const settings = this.rendererSettings;
822
+ const uvLength = (geometry.length / 3) * 2;
823
+ const previous = settings.instancingGeometry;
824
+ settings.instancingGeometry = geometry;
825
+ // Sets that no longer fit, or that were the old geometry's fallbacks, move to the new one's.
826
+ if (settings.instancingUVs?.length !== uvLength || settings.instancingUVs === fallbackUVs.get(previous)) {
827
+ settings.instancingUVs = fallbackUVsForPositions(geometry);
828
+ }
829
+ if (
830
+ settings.instancingNormals &&
831
+ (settings.instancingNormals.length !== geometry.length ||
832
+ settings.instancingNormals === fallbackNormals.get(previous))
833
+ ) {
834
+ settings.instancingNormals = ParticleSystem.createFallbackNormals(geometry);
835
+ }
836
+ if (settings.instancingUVs2 && settings.instancingUVs2.length !== uvLength) {
837
+ settings.instancingUVs2 = undefined;
838
+ }
839
+ this.neededToUpdateRender = true;
840
+ }
841
+
842
+ /** The mesh particles' first UV set, two values per vertex. */
843
+ get instancingUVs(): Float32Array {
844
+ return this.rendererSettings.instancingUVs ?? fallbackUVsForPositions(this.rendererSettings.instancingGeometry);
845
+ }
846
+
847
+ set instancingUVs(uvs: Float32Array) {
848
+ this.rendererSettings.instancingUVs = uvs;
849
+ this.neededToUpdateRender = true;
850
+ }
851
+
852
+ /** The mesh particles' second UV set, read by textures on UV channel 1; see `UVChannel`. */
853
+ get instancingUVs2(): Float32Array | undefined {
854
+ return this.rendererSettings.instancingUVs2;
855
+ }
856
+
857
+ set instancingUVs2(uvs: Float32Array | undefined) {
858
+ this.rendererSettings.instancingUVs2 = uvs;
637
859
  this.neededToUpdateRender = true;
638
860
  }
639
861
 
@@ -674,6 +896,7 @@ export class ParticleSystem implements IParticleSystem {
674
896
  case RenderMode.BillBoard:
675
897
  case RenderMode.VerticalBillBoard:
676
898
  case RenderMode.HorizontalBillBoard:
899
+ case RenderMode.Beam:
677
900
  this.rendererEmitterSettings = {};
678
901
  break;
679
902
  }
@@ -730,9 +953,28 @@ export class ParticleSystem implements IParticleSystem {
730
953
  */
731
954
  playbackSpeed = 1;
732
955
 
956
+ /**
957
+ * Runtime multiplier on how many particles the system emits: bursts,
958
+ * emission over time and over distance alike. Unlike
959
+ * {@link setQualityFactor}, which only ever thins an effect, it may go
960
+ * above 1 to make a stronger version of the same effect. The two multiply.
961
+ * 1 by default; not saved in the effect JSON.
962
+ */
963
+ emissionMultiplier = 1;
964
+
733
965
  /** What this system does while off screen, see {@link CullingMode}. `'always'` by default. */
734
966
  cullingMode: CullingMode = 'always';
735
967
 
968
+ /** What the emitter's world scale does to a world-space system, see {@link ScalingMode}. `'shape'` by default. */
969
+ scalingMode: ScalingMode = 'shape';
970
+
971
+ /**
972
+ * What world-space forces and speed limits are multiplied by this step:
973
+ * the mean of the emitter's world scale axes in `'hierarchy'` mode, 1
974
+ * otherwise. Updated before every simulation step.
975
+ */
976
+ motionScale = 1;
977
+
736
978
  /**
737
979
  * Whether each particle starts turned along the direction its shape sends
738
980
  * it (Unity's Align To Direction): a mesh particle's +Z is pointed along
@@ -852,6 +1094,7 @@ export class ParticleSystem implements IParticleSystem {
852
1094
  instancingUVs:
853
1095
  parameters.instancingUVs ??
854
1096
  (parameters.instancingGeometry ? fallbackUVsForPositions(parameters.instancingGeometry) : DEFAULT_UVS),
1097
+ instancingUVs2: parameters.instancingUVs2,
855
1098
  instancingNormals: parameters.instancingNormals,
856
1099
  renderMode: parameters.renderMode ?? RenderMode.BillBoard,
857
1100
  renderOrder: parameters.renderOrder ?? 0,
@@ -880,10 +1123,15 @@ export class ParticleSystem implements IParticleSystem {
880
1123
  applyFog: parameters.applyFog ?? true,
881
1124
  lighting: ParticleSystem.validLighting(parameters.lighting),
882
1125
  rim: normalizeMeshRim(parameters.rim),
1126
+ mapUV: normalizeMapUV(parameters.mapUV),
1127
+ gradientMap: normalizeGradientMap(parameters.gradientMap),
1128
+ erosion: normalizeErosion(parameters.erosion),
1129
+ uvDistortion: normalizeUVDistortion(parameters.uvDistortion),
883
1130
  };
884
1131
  this.flip = ParticleSystem.nonZeroFlip(parameters.flip);
885
1132
  this.playbackSpeed = parameters.playbackSpeed ?? 1;
886
1133
  this.cullingMode = parameters.cullingMode ?? 'always';
1134
+ this.scalingMode = parameters.scalingMode === 'hierarchy' ? 'hierarchy' : 'shape';
887
1135
  this.alignToDirection = parameters.alignToDirection === true;
888
1136
  this.seed = parameters.seed;
889
1137
  if (this.rendererSettings.renderMode === RenderMode.Mesh && !this.rendererSettings.instancingNormals) {
@@ -1062,6 +1310,12 @@ export class ParticleSystem implements IParticleSystem {
1062
1310
  this.qualityFactor = Math.max(0.1, Math.min(1, qualityFactor));
1063
1311
  }
1064
1312
 
1313
+ /** Quality factor times {@link emissionMultiplier}: what emission counts are scaled by. */
1314
+ private emissionFactor(): number {
1315
+ const multiplier = this.emissionMultiplier;
1316
+ return multiplier >= 0 ? this.qualityFactor * multiplier : this.qualityFactor;
1317
+ }
1318
+
1065
1319
  /**
1066
1320
  * Appends one particle to the pool, giving it the next row of the store.
1067
1321
  *
@@ -1212,7 +1466,12 @@ export class ParticleSystem implements IParticleSystem {
1212
1466
  particle.position.applyMatrix4(matrix);
1213
1467
  particle.startSize.multiply(scale).abs();
1214
1468
  particle.size.copy(particle.startSize);
1215
- particle.velocity.multiply(scale).applyMatrix3(this.normalMatrix);
1469
+ if (this.scalingMode === 'hierarchy') {
1470
+ // Scaled per axis by the emitter's world scale, then turned: the speed scales with it.
1471
+ particle.velocity.multiply(this.hierarchyScale).applyQuaternion(tempQ);
1472
+ } else {
1473
+ particle.velocity.multiply(scale).applyMatrix3(this.normalMatrix);
1474
+ }
1216
1475
  if (particle.rotation && particle.rotation instanceof Quaternion) {
1217
1476
  particle.rotation.multiplyQuaternions(tempQ, particle.rotation);
1218
1477
  }
@@ -1514,6 +1773,76 @@ export class ParticleSystem implements IParticleSystem {
1514
1773
  * frame behind the display.
1515
1774
  */
1516
1775
  getWorldBounds(min: Vector3, max: Vector3): boolean {
1776
+ if (!this.measureWorldBounds(min, max)) {
1777
+ return false;
1778
+ }
1779
+ if (this.rendererSettings.renderMode === RenderMode.Beam) {
1780
+ // The quads reach the beam's end, which moves on its own. Without
1781
+ // one a beam draws nothing beyond its particles.
1782
+ const end = this.beamEndScratch;
1783
+ if (!this.getBeamEnd(end)) {
1784
+ return true;
1785
+ }
1786
+ const pad = this.boundsInWorld ? this.boundsPadding : this.boundsPadding * this.emitterMaxScale();
1787
+ min.set(Math.min(min.x, end.x - pad), Math.min(min.y, end.y - pad), Math.min(min.z, end.z - pad));
1788
+ max.set(Math.max(max.x, end.x + pad), Math.max(max.y, end.y + pad), Math.max(max.z, end.z + pad));
1789
+ }
1790
+ return true;
1791
+ }
1792
+
1793
+ /**
1794
+ * Where a {@link RenderMode.Beam} system's quads end, see {@link BeamEnd}.
1795
+ * Unset (the default), a beam has no length and draws nothing. Not saved
1796
+ * in the effect JSON: it is where this instance is aimed at runtime.
1797
+ */
1798
+ beamEnd?: BeamEnd;
1799
+
1800
+ /**
1801
+ * Multiplies every particle's colour, alpha included, as it is drawn:
1802
+ * a colour or a fade driven from code for this instance (Unity's custom
1803
+ * per-instance data, Unreal's user parameters). With {@link erosion},
1804
+ * alpha is the erosion level, so this wipes the shape in and out. The
1805
+ * particles' own colours are untouched, so clearing it restores them, and
1806
+ * systems with different tints still batch together. Under additive
1807
+ * (`ALPHA_ONEONE`) or premultiplied blending, which ignore alpha, fade
1808
+ * with the colour instead, or with erosion on. Unset (the default): none.
1809
+ * Not saved in the effect JSON; copied by {@link clone}.
1810
+ */
1811
+ tint?: Vector4;
1812
+
1813
+ private readonly beamEndScratch = new Vector3();
1814
+
1815
+ /** The world position of {@link beamEnd} into `out`; false when it is unset. */
1816
+ getBeamEnd(out: Vector3): boolean {
1817
+ const end = this.beamEnd;
1818
+ if (end === undefined) {
1819
+ return false;
1820
+ }
1821
+ const node = end as {getAbsolutePosition?: () => {x: number; y: number; z: number}};
1822
+ if (typeof node.getAbsolutePosition === 'function') {
1823
+ const position = node.getAbsolutePosition();
1824
+ out.set(position.x, position.y, position.z);
1825
+ } else {
1826
+ // Any point: quarks' Vector3, or a Babylon one passed from plain JavaScript.
1827
+ const point = end as {x: number; y: number; z: number};
1828
+ out.set(point.x, point.y, point.z);
1829
+ }
1830
+ return true;
1831
+ }
1832
+
1833
+ /** The largest axis of the emitter's world scale. */
1834
+ private emitterMaxScale(): number {
1835
+ const e = this.emitter.matrixWorld.elements;
1836
+ return Math.sqrt(
1837
+ Math.max(
1838
+ e[0] * e[0] + e[1] * e[1] + e[2] * e[2],
1839
+ e[4] * e[4] + e[5] * e[5] + e[6] * e[6],
1840
+ e[8] * e[8] + e[9] * e[9] + e[10] * e[10]
1841
+ )
1842
+ );
1843
+ }
1844
+
1845
+ private measureWorldBounds(min: Vector3, max: Vector3): boolean {
1517
1846
  if (!this.boundsValid) {
1518
1847
  return false;
1519
1848
  }
@@ -1689,6 +2018,7 @@ export class ParticleSystem implements IParticleSystem {
1689
2018
  }
1690
2019
 
1691
2020
  private stepOnce(delta: number): void {
2021
+ this.motionScale = this.measureMotionScale();
1692
2022
  if (!this.onlyUsedByOther) {
1693
2023
  this.emit(delta, this.emissionState, this.emitter.matrixWorld);
1694
2024
  }
@@ -1936,6 +2266,10 @@ export class ParticleSystem implements IParticleSystem {
1936
2266
  // A world-space system records world positions already.
1937
2267
  const recordWorldSpace = trailSettings.recordWorldSpace && !this.worldSpace;
1938
2268
  const emitterMatrix = this.emitter.matrixWorld;
2269
+ // A swept trail records its side as the emitter holds it, in the
2270
+ // space its positions are recorded in.
2271
+ const sideAxis = trailSettings.sideAxis;
2272
+ const sidesInWorld = this.worldSpace || Boolean(trailSettings.recordWorldSpace);
1939
2273
  for (let i = 0; i < particleCount; i++) {
1940
2274
  const trail = particles[i] as TrailParticle;
1941
2275
  // Emptying the history lets the removal pass below take the
@@ -1955,6 +2289,25 @@ export class ParticleSystem implements IParticleSystem {
1955
2289
  positions[offset + 1] = tempV.y;
1956
2290
  positions[offset + 2] = tempV.z;
1957
2291
  }
2292
+ if (sideAxis && trail.age <= trail.life) {
2293
+ trail.ensureSideHistory();
2294
+ const offset = ((trail.historyHead + trail.historyCapacity - 1) % trail.historyCapacity) * 3;
2295
+ const sides = trail.historySides;
2296
+ let x = sideAxis.x;
2297
+ let y = sideAxis.y;
2298
+ let z = sideAxis.z;
2299
+ if (sidesInWorld) {
2300
+ const m = (trail.parentMatrix ?? emitterMatrix).elements;
2301
+ const tx = m[0] * x + m[4] * y + m[8] * z;
2302
+ const ty = m[1] * x + m[5] * y + m[9] * z;
2303
+ z = m[2] * x + m[6] * y + m[10] * z;
2304
+ x = tx;
2305
+ y = ty;
2306
+ }
2307
+ sides[offset] = x;
2308
+ sides[offset + 1] = y;
2309
+ sides[offset + 2] = z;
2310
+ }
1958
2311
  }
1959
2312
  }
1960
2313
 
@@ -2060,8 +2413,9 @@ export class ParticleSystem implements IParticleSystem {
2060
2413
  return;
2061
2414
  }
2062
2415
  const rawCount = burst.count.genValue(this.memory, this.time);
2063
- const qualityFactor = this.qualityFactor;
2064
- const count = qualityFactor >= 0.999 ? rawCount : Math.floor(rawCount * qualityFactor);
2416
+ const factor = this.emissionFactor();
2417
+ // Rounded at random, so a fraction is not always lost: three at 1.5 is four or five, on average 4.5.
2418
+ const count = Math.abs(factor - 1) < 0.001 ? rawCount : Math.floor(rawCount * factor + random());
2065
2419
  emissionState.isBursting = true;
2066
2420
  emissionState.burstParticleCount = count;
2067
2421
  this.spawn(count, emissionState, emitterMatrix);
@@ -2126,6 +2480,12 @@ export class ParticleSystem implements IParticleSystem {
2126
2480
  }
2127
2481
 
2128
2482
  emit(delta: number, emissionState: EmissionState, emitterMatrix: Matrix4) {
2483
+ // Ended before this step (endEmit, or a past step's end of duration):
2484
+ // no more bursts either. A looping system would otherwise keep firing
2485
+ // them every loop after endEmit and never finish. Only the system's
2486
+ // own emission: a sub emitter's bursts belong to its parent particles,
2487
+ // which still go off as they die after the effect was told to end.
2488
+ const endedBefore = this.emitEnded && emissionState === this.emissionState;
2129
2489
  if (emissionState.time > this.duration) {
2130
2490
  if (this.looping) {
2131
2491
  emissionState.time -= this.duration;
@@ -2149,9 +2509,9 @@ export class ParticleSystem implements IParticleSystem {
2149
2509
  this.normalMatrix.getNormalMatrix(emitterMatrix);
2150
2510
  }
2151
2511
  const emissionBurstCount = this.emissionBursts.length;
2152
- const qualityFactor = this.qualityFactor;
2512
+ const qualityFactor = this.emissionFactor();
2153
2513
 
2154
- if (emissionBurstCount > 0) {
2514
+ if (emissionBurstCount > 0 && !endedBefore) {
2155
2515
  this.emitBursts(emissionState, emitterMatrix);
2156
2516
  }
2157
2517
 
@@ -2265,6 +2625,7 @@ export class ParticleSystem implements IParticleSystem {
2265
2625
  ...(this.applyFog ? {} : {applyFog: false}),
2266
2626
  ...(this.playbackSpeed !== 1 ? {playbackSpeed: this.playbackSpeed} : {}),
2267
2627
  ...(this.cullingMode !== 'always' ? {cullingMode: this.cullingMode} : {}),
2628
+ ...(this.scalingMode !== 'shape' ? {scalingMode: this.scalingMode} : {}),
2268
2629
  ...(this.seedValue !== undefined ? {seed: this.seedValue} : {}),
2269
2630
  ...(this.sortMode !== 'none' ? {sortMode: this.sortMode} : {}),
2270
2631
  ...(this.alignToDirection ? {alignToDirection: true} : {}),
@@ -2309,6 +2670,7 @@ export class ParticleSystem implements IParticleSystem {
2309
2670
  : new ConstantValue(30),
2310
2671
  followLocalOrigin: trailSettings?.followLocalOrigin ?? false,
2311
2672
  ...trailFlags(trailSettings),
2673
+ ...trailSideAxisFromJSON((trailSettings as {sideAxis?: unknown} | undefined)?.sideAxis),
2312
2674
  };
2313
2675
  } else if (json.renderMode === RenderMode.StretchedBillBoard) {
2314
2676
  rendererEmitterSettings = {...(json.rendererEmitterSettings ?? {})};
@@ -2325,6 +2687,12 @@ export class ParticleSystem implements IParticleSystem {
2325
2687
  typeof materialTextureRef === 'string'
2326
2688
  ? (meta.textures?.[materialTextureRef] ?? null)
2327
2689
  : (materialTextureRef ?? null);
2690
+ // Texture effects reference textures by uuid in a serialised meta, or hold them once a loader resolved them.
2691
+ const resolveTexture = (ref: unknown): BaseTexture | null =>
2692
+ typeof ref === 'string' ? (meta.textures?.[ref] ?? null) : ((ref as BaseTexture | null) ?? null);
2693
+ const erosionMeta = materialMeta?.erosion;
2694
+ const gradientMeta = materialMeta?.gradientMap;
2695
+ const distortionMeta = materialMeta?.uvDistortion;
2328
2696
  const resolvedGeometryEntry =
2329
2697
  typeof json.instancingGeometry === 'string'
2330
2698
  ? meta.geometries?.[json.instancingGeometry]
@@ -2364,6 +2732,7 @@ export class ParticleSystem implements IParticleSystem {
2364
2732
  instancingGeometry: resolvedGeometry.positions,
2365
2733
  instancingIndices: resolvedGeometry.indices,
2366
2734
  instancingUVs: resolvedGeometry.uvs,
2735
+ instancingUVs2: resolvedGeometry.uvs2,
2367
2736
  instancingNormals: resolvedGeometry.normals,
2368
2737
  renderMode: json.renderMode,
2369
2738
  rendererEmitterSettings,
@@ -2372,6 +2741,7 @@ export class ParticleSystem implements IParticleSystem {
2372
2741
  applyFog: json.applyFog !== false,
2373
2742
  playbackSpeed: typeof json.playbackSpeed === 'number' ? json.playbackSpeed : undefined,
2374
2743
  cullingMode: ParticleSystem.validCullingMode(json.cullingMode),
2744
+ scalingMode: json.scalingMode === 'hierarchy' ? 'hierarchy' : 'shape',
2375
2745
  seed: typeof json.seed === 'number' ? json.seed : undefined,
2376
2746
  sortMode: ParticleSystem.validSortMode(json.sortMode),
2377
2747
  alignToDirection: json.alignToDirection === true,
@@ -2401,6 +2771,19 @@ export class ParticleSystem implements IParticleSystem {
2401
2771
  flip: json.flip ? new Vector2(json.flip[0] ?? 0, json.flip[1] ?? 0) : undefined,
2402
2772
  lighting: materialMeta?.lighting,
2403
2773
  rim: materialMeta?.rim,
2774
+ mapUV: materialMeta?.mapUV,
2775
+ gradientMap:
2776
+ gradientMeta && typeof gradientMeta === 'object'
2777
+ ? {...gradientMeta, ramp: resolveTexture(gradientMeta.ramp)}
2778
+ : undefined,
2779
+ erosion:
2780
+ erosionMeta && typeof erosionMeta === 'object'
2781
+ ? {...erosionMeta, mask: resolveTexture(erosionMeta.mask)}
2782
+ : undefined,
2783
+ uvDistortion:
2784
+ distortionMeta && typeof distortionMeta === 'object'
2785
+ ? {...distortionMeta, map: resolveTexture(distortionMeta.map)}
2786
+ : undefined,
2404
2787
  });
2405
2788
  ps.behaviors = (json.behaviors ?? [])
2406
2789
  .map((behaviorJson) => {
@@ -2419,12 +2802,15 @@ export class ParticleSystem implements IParticleSystem {
2419
2802
  return `${prefix}_${ParticleSystem.serializationCounter}`;
2420
2803
  }
2421
2804
 
2805
+ /** Normals facing +Z, one array per geometry so systems sharing it still batch together. */
2422
2806
  private static createFallbackNormals(positions: Float32Array): Float32Array {
2423
- const normals = new Float32Array(positions.length);
2424
- for (let i = 0; i < normals.length; i += 3) {
2425
- normals[i] = 0;
2426
- normals[i + 1] = 0;
2427
- normals[i + 2] = 1;
2807
+ let normals = fallbackNormals.get(positions);
2808
+ if (!normals) {
2809
+ normals = new Float32Array(positions.length);
2810
+ for (let i = 2; i < normals.length; i += 3) {
2811
+ normals[i] = 1;
2812
+ }
2813
+ fallbackNormals.set(positions, normals);
2428
2814
  }
2429
2815
  return normals;
2430
2816
  }
@@ -2435,6 +2821,7 @@ export class ParticleSystem implements IParticleSystem {
2435
2821
  startLength: (this.rendererEmitterSettings as TrailSettings).startLength.toJSON(),
2436
2822
  followLocalOrigin: (this.rendererEmitterSettings as TrailSettings).followLocalOrigin,
2437
2823
  ...trailFlags(this.rendererEmitterSettings as TrailSettings),
2824
+ ...(trailSideAxisJSON(this.rendererEmitterSettings as TrailSettings) as {sideAxis?: any}),
2438
2825
  };
2439
2826
  }
2440
2827
  if (this.renderMode === RenderMode.StretchedBillBoard) {
@@ -2454,6 +2841,7 @@ export class ParticleSystem implements IParticleSystem {
2454
2841
  positions: Array.from(this.rendererSettings.instancingGeometry),
2455
2842
  indices: Array.from(this.rendererSettings.instancingIndices),
2456
2843
  uvs: this.rendererSettings.instancingUVs ? Array.from(this.rendererSettings.instancingUVs) : undefined,
2844
+ ...(this.rendererSettings.instancingUVs2 ? {uvs2: Array.from(this.rendererSettings.instancingUVs2)} : {}),
2457
2845
  normals: this.rendererSettings.instancingNormals
2458
2846
  ? Array.from(this.rendererSettings.instancingNormals)
2459
2847
  : undefined,
@@ -2476,6 +2864,19 @@ export class ParticleSystem implements IParticleSystem {
2476
2864
  meta.textures[reflectionAtlasUUID] = reflectionAtlas;
2477
2865
  }
2478
2866
 
2867
+ const textureRef = (value: BaseTexture | null | undefined): string | undefined => {
2868
+ if (!(value instanceof Texture)) {
2869
+ return undefined;
2870
+ }
2871
+ const uuid = ParticleSystem.nextSerializationId('quarks_texture');
2872
+ meta.textures[uuid] = value;
2873
+ return uuid;
2874
+ };
2875
+ const erosion = this.erosion;
2876
+ const distortion = this.uvDistortion;
2877
+ const gradientMap = this.gradientMap;
2878
+ const optional = <T>(key: string, value: T | undefined) => (value !== undefined ? {[key]: value} : {});
2879
+
2479
2880
  const materialUUID = ParticleSystem.nextSerializationId('quarks_material');
2480
2881
  meta.materials[materialUUID] = {
2481
2882
  uuid: materialUUID,
@@ -2491,6 +2892,35 @@ export class ParticleSystem implements IParticleSystem {
2491
2892
  // Written only when not the default, so existing effects serialise as before.
2492
2893
  ...(this.lighting !== 'fixed' ? {lighting: this.lighting} : {}),
2493
2894
  ...(this.rim ? {rim: {...this.rim, color: [...this.rim.color]}} : {}),
2895
+ ...optional('mapUV', this.mapUV ? textureUVToJSON(this.mapUV) : undefined),
2896
+ ...(gradientMap
2897
+ ? {
2898
+ gradientMap: {
2899
+ ramp: textureRef(gradientMap.ramp),
2900
+ ...(gradientMap.channel !== 'r' ? {channel: gradientMap.channel} : {}),
2901
+ },
2902
+ }
2903
+ : {}),
2904
+ ...(erosion
2905
+ ? {
2906
+ erosion: {
2907
+ mask: textureRef(erosion.mask),
2908
+ ...optional('maskUV', textureUVToJSON(erosion.maskUV)),
2909
+ softness: erosion.softness,
2910
+ edgeWidth: erosion.edgeWidth,
2911
+ edgeColor: [...erosion.edgeColor],
2912
+ },
2913
+ }
2914
+ : {}),
2915
+ ...(distortion
2916
+ ? {
2917
+ uvDistortion: {
2918
+ map: textureRef(distortion.map),
2919
+ ...optional('mapUV', textureUVToJSON(distortion.mapUV, UV_DISTORTION_DEFAULT_UV)),
2920
+ strength: distortion.strength,
2921
+ },
2922
+ }
2923
+ : {}),
2494
2924
  sourceMaterial: this.materialRef ?? undefined,
2495
2925
  };
2496
2926
  return materialUUID;
@@ -2500,6 +2930,7 @@ export class ParticleSystem implements IParticleSystem {
2500
2930
  positions: Float32Array;
2501
2931
  indices: Uint32Array | Uint16Array;
2502
2932
  uvs?: Float32Array;
2933
+ uvs2?: Float32Array;
2503
2934
  normals?: Float32Array;
2504
2935
  } {
2505
2936
  if (entry?.positions) {
@@ -2519,6 +2950,11 @@ export class ParticleSystem implements IParticleSystem {
2519
2950
  ? entry.uvs
2520
2951
  : new Float32Array(entry.uvs)
2521
2952
  : undefined,
2953
+ uvs2: entry.uvs2
2954
+ ? entry.uvs2 instanceof Float32Array
2955
+ ? entry.uvs2
2956
+ : new Float32Array(entry.uvs2)
2957
+ : undefined,
2522
2958
  normals: entry.normals
2523
2959
  ? entry.normals instanceof Float32Array
2524
2960
  ? entry.normals
@@ -2539,6 +2975,10 @@ export class ParticleSystem implements IParticleSystem {
2539
2975
  positions,
2540
2976
  indices: ensureTriangleIndices(positions, indices),
2541
2977
  uvs: data.attributes.uv?.array ? new Float32Array(data.attributes.uv.array) : undefined,
2978
+ // three.js names the second set `uv1` (r151 on), `uv2` before.
2979
+ uvs2: (data.attributes.uv1 ?? data.attributes.uv2)?.array
2980
+ ? new Float32Array((data.attributes.uv1 ?? data.attributes.uv2).array)
2981
+ : undefined,
2542
2982
  normals: data.attributes.normal?.array ? new Float32Array(data.attributes.normal.array) : undefined,
2543
2983
  };
2544
2984
  }
@@ -2601,6 +3041,9 @@ export class ParticleSystem implements IParticleSystem {
2601
3041
  startLength: (this.rendererEmitterSettings as TrailSettings).startLength.clone(),
2602
3042
  followLocalOrigin: (this.rendererEmitterSettings as TrailSettings).followLocalOrigin,
2603
3043
  ...trailFlags(this.rendererEmitterSettings as TrailSettings),
3044
+ ...((this.rendererEmitterSettings as TrailSettings).sideAxis
3045
+ ? {sideAxis: (this.rendererEmitterSettings as TrailSettings).sideAxis!.clone()}
3046
+ : {}),
2604
3047
  };
2605
3048
  } else if (this.renderMode === RenderMode.StretchedBillBoard) {
2606
3049
  rendererEmitterSettings = {
@@ -2631,6 +3074,7 @@ export class ParticleSystem implements IParticleSystem {
2631
3074
  instancingGeometry: this.rendererSettings.instancingGeometry,
2632
3075
  instancingIndices: this.rendererSettings.instancingIndices,
2633
3076
  instancingUVs: this.rendererSettings.instancingUVs,
3077
+ instancingUVs2: this.rendererSettings.instancingUVs2,
2634
3078
  instancingNormals: this.rendererSettings.instancingNormals,
2635
3079
  renderMode: this.renderMode,
2636
3080
  renderOrder: this.renderOrder,
@@ -2638,6 +3082,7 @@ export class ParticleSystem implements IParticleSystem {
2638
3082
  applyFog: this.applyFog,
2639
3083
  playbackSpeed: this.playbackSpeed,
2640
3084
  cullingMode: this.cullingMode,
3085
+ scalingMode: this.scalingMode,
2641
3086
  seed: this.seedValue,
2642
3087
  sortMode: this.sortMode,
2643
3088
  alignToDirection: this.alignToDirection,
@@ -2665,6 +3110,10 @@ export class ParticleSystem implements IParticleSystem {
2665
3110
  flip: this.flip,
2666
3111
  lighting: this.lighting,
2667
3112
  rim: this.rim,
3113
+ mapUV: this.mapUV,
3114
+ gradientMap: this.gradientMap,
3115
+ erosion: this.erosion,
3116
+ uvDistortion: this.uvDistortion,
2668
3117
  scene: this.emitter.getScene(),
2669
3118
  });
2670
3119
  // A sub emitter behavior transforms by its parent system's emitter;
@@ -2674,6 +3123,9 @@ export class ParticleSystem implements IParticleSystem {
2674
3123
  behavior.particleSystem = copy;
2675
3124
  }
2676
3125
  }
3126
+ if (this.tint) {
3127
+ copy.tint = this.tint.clone();
3128
+ }
2677
3129
  return copy;
2678
3130
  }
2679
3131
  }