babylon.quarks 0.25.0 → 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.
@@ -57,11 +57,20 @@ import {
57
57
  MeshLighting,
58
58
  MeshRim,
59
59
  normalizeErosion,
60
+ normalizeGradientMap,
61
+ normalizeMapUV,
60
62
  normalizeMeshRim,
61
- normalizeUVScroll,
63
+ normalizeUVDistortion,
62
64
  ParticleErosion,
65
+ ParticleErosionOptions,
66
+ ParticleGradientMap,
67
+ ParticleUVDistortion,
68
+ ParticleUVDistortionOptions,
63
69
  RenderMode,
64
70
  SortMode,
71
+ TextureUV,
72
+ textureUVToJSON,
73
+ UV_DISTORTION_DEFAULT_UV,
65
74
  } from './VFXBatch';
66
75
 
67
76
  export interface BurstParameters {
@@ -162,8 +171,19 @@ const DEFAULT_POSITIONS = new Float32Array([-0.5, -0.5, 0, 0.5, -0.5, 0, 0.5, 0.
162
171
  const DEFAULT_UVS = new Float32Array([0, 0, 1, 0, 1, 1, 0, 1]);
163
172
 
164
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
+ */
165
180
  function fallbackUVsForPositions(positions: Float32Array): Float32Array {
166
- 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;
167
187
  }
168
188
  const DEFAULT_INDICES = new Uint32Array([0, 1, 2, 0, 2, 3]);
169
189
 
@@ -177,6 +197,24 @@ function trailFlags(settings: TrailSettings | undefined): Partial<TrailSettings>
177
197
  return flags;
178
198
  }
179
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
+
180
218
  export interface ParticleSystemParameters {
181
219
  autoDestroy?: boolean;
182
220
  looping?: boolean;
@@ -200,6 +238,12 @@ export interface ParticleSystemParameters {
200
238
  instancingGeometry?: Float32Array;
201
239
  instancingIndices?: Uint32Array | Uint16Array;
202
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;
203
247
  instancingNormals?: Float32Array;
204
248
  renderMode?: RenderMode;
205
249
  rendererEmitterSettings?: RendererEmitterSettings;
@@ -229,12 +273,14 @@ export interface ParticleSystemParameters {
229
273
  lighting?: MeshLighting;
230
274
  /** Rim (Fresnel) term on mesh particles, see {@link ParticleSystem.rim}. Default none. */
231
275
  rim?: Partial<MeshRim>;
232
- /** Map UV scroll in UV units per second, see {@link ParticleSystem.uvScroll}. Default none. */
233
- uvScroll?: [number, number];
276
+ /** The main map's layout, see {@link ParticleSystem.mapUV}. Default as drawn. */
277
+ mapUV?: Partial<TextureUV>;
234
278
  /** Colour ramp for the map, see {@link ParticleSystem.gradientMap}. Default none. */
235
- gradientMap?: BaseTexture | null;
279
+ gradientMap?: Partial<ParticleGradientMap>;
236
280
  /** Alpha erosion, see {@link ParticleSystem.erosion}. Default none. */
237
- erosion?: Partial<ParticleErosion>;
281
+ erosion?: ParticleErosionOptions;
282
+ /** Map UV distortion, see {@link ParticleSystem.uvDistortion}. Default none. */
283
+ uvDistortion?: ParticleUVDistortionOptions;
238
284
  /** Multiplier on the time the system is advanced by, see {@link ParticleSystem.playbackSpeed}. Default 1. */
239
285
  playbackSpeed?: number;
240
286
  /** What the system does while off screen, see {@link CullingMode}. Default `'always'`. */
@@ -628,33 +674,39 @@ export class ParticleSystem implements IParticleSystem {
628
674
  }
629
675
 
630
676
  /**
631
- * Scrolls the map's UV by `[u, v]` UV units per second on the renderer's
632
- * clock: flowing energy, lava, a smear running along a mesh. The texture
633
- * should wrap. Undefined (the default) or zero does not scroll. Assign a
634
- * new array to change it. Saved with the material in the effect JSON.
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.
635
686
  */
636
- get uvScroll(): [number, number] | undefined {
637
- return this.rendererSettings.uvScroll;
687
+ get mapUV(): TextureUV | undefined {
688
+ return this.rendererSettings.mapUV;
638
689
  }
639
690
 
640
- set uvScroll(scroll: [number, number] | undefined) {
641
- this.rendererSettings.uvScroll = normalizeUVScroll(scroll);
691
+ set mapUV(uv: Partial<TextureUV> | undefined) {
692
+ this.rendererSettings.mapUV = normalizeMapUV(uv);
642
693
  this.neededToUpdateRender = true;
643
694
  }
644
695
 
645
696
  /**
646
- * Colour ramp the map is recoloured through (a gradient map): the map's
647
- * red channel picks a colour along U of this texture, whose alpha
648
- * multiplies the map's; the particle colour still tints the result. One
649
- * greyscale texture then serves every palette. Null (the default): none.
650
- * Saved with the material in the effect JSON.
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.
651
703
  */
652
- get gradientMap(): BaseTexture | null {
653
- return this.rendererSettings.gradientMap ?? null;
704
+ get gradientMap(): ParticleGradientMap | undefined {
705
+ return this.rendererSettings.gradientMap;
654
706
  }
655
707
 
656
- set gradientMap(texture: BaseTexture | null) {
657
- this.rendererSettings.gradientMap = texture ?? null;
708
+ set gradientMap(gradientMap: Partial<ParticleGradientMap> | undefined) {
709
+ this.rendererSettings.gradientMap = normalizeGradientMap(gradientMap);
658
710
  this.neededToUpdateRender = true;
659
711
  }
660
712
 
@@ -671,11 +723,27 @@ export class ParticleSystem implements IParticleSystem {
671
723
  return this.rendererSettings.erosion;
672
724
  }
673
725
 
674
- set erosion(erosion: Partial<ParticleErosion> | undefined) {
726
+ set erosion(erosion: ParticleErosionOptions | undefined) {
675
727
  this.rendererSettings.erosion = normalizeErosion(erosion);
676
728
  this.neededToUpdateRender = true;
677
729
  }
678
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
+
679
747
  /**
680
748
  * The emitter's world scale per axis, measured with {@link motionScale}
681
749
  * before every step; what `'hierarchy'` multiplies start speeds by. Taken
@@ -741,10 +809,53 @@ export class ParticleSystem implements IParticleSystem {
741
809
  return this.rendererSettings.instancingGeometry;
742
810
  }
743
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
+ */
744
818
  set instancingGeometry(geometry: Float32Array) {
745
819
  this.restart();
746
820
  this.particles.length = 0;
747
- 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;
748
859
  this.neededToUpdateRender = true;
749
860
  }
750
861
 
@@ -983,6 +1094,7 @@ export class ParticleSystem implements IParticleSystem {
983
1094
  instancingUVs:
984
1095
  parameters.instancingUVs ??
985
1096
  (parameters.instancingGeometry ? fallbackUVsForPositions(parameters.instancingGeometry) : DEFAULT_UVS),
1097
+ instancingUVs2: parameters.instancingUVs2,
986
1098
  instancingNormals: parameters.instancingNormals,
987
1099
  renderMode: parameters.renderMode ?? RenderMode.BillBoard,
988
1100
  renderOrder: parameters.renderOrder ?? 0,
@@ -1011,9 +1123,10 @@ export class ParticleSystem implements IParticleSystem {
1011
1123
  applyFog: parameters.applyFog ?? true,
1012
1124
  lighting: ParticleSystem.validLighting(parameters.lighting),
1013
1125
  rim: normalizeMeshRim(parameters.rim),
1014
- uvScroll: normalizeUVScroll(parameters.uvScroll),
1015
- gradientMap: parameters.gradientMap ?? null,
1126
+ mapUV: normalizeMapUV(parameters.mapUV),
1127
+ gradientMap: normalizeGradientMap(parameters.gradientMap),
1016
1128
  erosion: normalizeErosion(parameters.erosion),
1129
+ uvDistortion: normalizeUVDistortion(parameters.uvDistortion),
1017
1130
  };
1018
1131
  this.flip = ParticleSystem.nonZeroFlip(parameters.flip);
1019
1132
  this.playbackSpeed = parameters.playbackSpeed ?? 1;
@@ -1684,6 +1797,19 @@ export class ParticleSystem implements IParticleSystem {
1684
1797
  */
1685
1798
  beamEnd?: BeamEnd;
1686
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
+
1687
1813
  private readonly beamEndScratch = new Vector3();
1688
1814
 
1689
1815
  /** The world position of {@link beamEnd} into `out`; false when it is unset. */
@@ -2140,6 +2266,10 @@ export class ParticleSystem implements IParticleSystem {
2140
2266
  // A world-space system records world positions already.
2141
2267
  const recordWorldSpace = trailSettings.recordWorldSpace && !this.worldSpace;
2142
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);
2143
2273
  for (let i = 0; i < particleCount; i++) {
2144
2274
  const trail = particles[i] as TrailParticle;
2145
2275
  // Emptying the history lets the removal pass below take the
@@ -2159,6 +2289,25 @@ export class ParticleSystem implements IParticleSystem {
2159
2289
  positions[offset + 1] = tempV.y;
2160
2290
  positions[offset + 2] = tempV.z;
2161
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
+ }
2162
2311
  }
2163
2312
  }
2164
2313
 
@@ -2521,6 +2670,7 @@ export class ParticleSystem implements IParticleSystem {
2521
2670
  : new ConstantValue(30),
2522
2671
  followLocalOrigin: trailSettings?.followLocalOrigin ?? false,
2523
2672
  ...trailFlags(trailSettings),
2673
+ ...trailSideAxisFromJSON((trailSettings as {sideAxis?: unknown} | undefined)?.sideAxis),
2524
2674
  };
2525
2675
  } else if (json.renderMode === RenderMode.StretchedBillBoard) {
2526
2676
  rendererEmitterSettings = {...(json.rendererEmitterSettings ?? {})};
@@ -2541,6 +2691,8 @@ export class ParticleSystem implements IParticleSystem {
2541
2691
  const resolveTexture = (ref: unknown): BaseTexture | null =>
2542
2692
  typeof ref === 'string' ? (meta.textures?.[ref] ?? null) : ((ref as BaseTexture | null) ?? null);
2543
2693
  const erosionMeta = materialMeta?.erosion;
2694
+ const gradientMeta = materialMeta?.gradientMap;
2695
+ const distortionMeta = materialMeta?.uvDistortion;
2544
2696
  const resolvedGeometryEntry =
2545
2697
  typeof json.instancingGeometry === 'string'
2546
2698
  ? meta.geometries?.[json.instancingGeometry]
@@ -2580,6 +2732,7 @@ export class ParticleSystem implements IParticleSystem {
2580
2732
  instancingGeometry: resolvedGeometry.positions,
2581
2733
  instancingIndices: resolvedGeometry.indices,
2582
2734
  instancingUVs: resolvedGeometry.uvs,
2735
+ instancingUVs2: resolvedGeometry.uvs2,
2583
2736
  instancingNormals: resolvedGeometry.normals,
2584
2737
  renderMode: json.renderMode,
2585
2738
  rendererEmitterSettings,
@@ -2618,12 +2771,19 @@ export class ParticleSystem implements IParticleSystem {
2618
2771
  flip: json.flip ? new Vector2(json.flip[0] ?? 0, json.flip[1] ?? 0) : undefined,
2619
2772
  lighting: materialMeta?.lighting,
2620
2773
  rim: materialMeta?.rim,
2621
- uvScroll: materialMeta?.uvScroll,
2622
- gradientMap: resolveTexture(materialMeta?.gradientMap),
2774
+ mapUV: materialMeta?.mapUV,
2775
+ gradientMap:
2776
+ gradientMeta && typeof gradientMeta === 'object'
2777
+ ? {...gradientMeta, ramp: resolveTexture(gradientMeta.ramp)}
2778
+ : undefined,
2623
2779
  erosion:
2624
2780
  erosionMeta && typeof erosionMeta === 'object'
2625
2781
  ? {...erosionMeta, mask: resolveTexture(erosionMeta.mask)}
2626
2782
  : undefined,
2783
+ uvDistortion:
2784
+ distortionMeta && typeof distortionMeta === 'object'
2785
+ ? {...distortionMeta, map: resolveTexture(distortionMeta.map)}
2786
+ : undefined,
2627
2787
  });
2628
2788
  ps.behaviors = (json.behaviors ?? [])
2629
2789
  .map((behaviorJson) => {
@@ -2642,12 +2802,15 @@ export class ParticleSystem implements IParticleSystem {
2642
2802
  return `${prefix}_${ParticleSystem.serializationCounter}`;
2643
2803
  }
2644
2804
 
2805
+ /** Normals facing +Z, one array per geometry so systems sharing it still batch together. */
2645
2806
  private static createFallbackNormals(positions: Float32Array): Float32Array {
2646
- const normals = new Float32Array(positions.length);
2647
- for (let i = 0; i < normals.length; i += 3) {
2648
- normals[i] = 0;
2649
- normals[i + 1] = 0;
2650
- 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);
2651
2814
  }
2652
2815
  return normals;
2653
2816
  }
@@ -2658,6 +2821,7 @@ export class ParticleSystem implements IParticleSystem {
2658
2821
  startLength: (this.rendererEmitterSettings as TrailSettings).startLength.toJSON(),
2659
2822
  followLocalOrigin: (this.rendererEmitterSettings as TrailSettings).followLocalOrigin,
2660
2823
  ...trailFlags(this.rendererEmitterSettings as TrailSettings),
2824
+ ...(trailSideAxisJSON(this.rendererEmitterSettings as TrailSettings) as {sideAxis?: any}),
2661
2825
  };
2662
2826
  }
2663
2827
  if (this.renderMode === RenderMode.StretchedBillBoard) {
@@ -2677,6 +2841,7 @@ export class ParticleSystem implements IParticleSystem {
2677
2841
  positions: Array.from(this.rendererSettings.instancingGeometry),
2678
2842
  indices: Array.from(this.rendererSettings.instancingIndices),
2679
2843
  uvs: this.rendererSettings.instancingUVs ? Array.from(this.rendererSettings.instancingUVs) : undefined,
2844
+ ...(this.rendererSettings.instancingUVs2 ? {uvs2: Array.from(this.rendererSettings.instancingUVs2)} : {}),
2680
2845
  normals: this.rendererSettings.instancingNormals
2681
2846
  ? Array.from(this.rendererSettings.instancingNormals)
2682
2847
  : undefined,
@@ -2708,6 +2873,9 @@ export class ParticleSystem implements IParticleSystem {
2708
2873
  return uuid;
2709
2874
  };
2710
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} : {});
2711
2879
 
2712
2880
  const materialUUID = ParticleSystem.nextSerializationId('quarks_material');
2713
2881
  meta.materials[materialUUID] = {
@@ -2724,16 +2892,32 @@ export class ParticleSystem implements IParticleSystem {
2724
2892
  // Written only when not the default, so existing effects serialise as before.
2725
2893
  ...(this.lighting !== 'fixed' ? {lighting: this.lighting} : {}),
2726
2894
  ...(this.rim ? {rim: {...this.rim, color: [...this.rim.color]}} : {}),
2727
- ...(this.uvScroll ? {uvScroll: [...this.uvScroll]} : {}),
2728
- ...(this.gradientMap ? {gradientMap: textureRef(this.gradientMap)} : {}),
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
+ : {}),
2729
2904
  ...(erosion
2730
2905
  ? {
2731
2906
  erosion: {
2732
2907
  mask: textureRef(erosion.mask),
2908
+ ...optional('maskUV', textureUVToJSON(erosion.maskUV)),
2733
2909
  softness: erosion.softness,
2734
2910
  edgeWidth: erosion.edgeWidth,
2735
2911
  edgeColor: [...erosion.edgeColor],
2736
- maskScroll: [...erosion.maskScroll],
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,
2737
2921
  },
2738
2922
  }
2739
2923
  : {}),
@@ -2746,6 +2930,7 @@ export class ParticleSystem implements IParticleSystem {
2746
2930
  positions: Float32Array;
2747
2931
  indices: Uint32Array | Uint16Array;
2748
2932
  uvs?: Float32Array;
2933
+ uvs2?: Float32Array;
2749
2934
  normals?: Float32Array;
2750
2935
  } {
2751
2936
  if (entry?.positions) {
@@ -2765,6 +2950,11 @@ export class ParticleSystem implements IParticleSystem {
2765
2950
  ? entry.uvs
2766
2951
  : new Float32Array(entry.uvs)
2767
2952
  : undefined,
2953
+ uvs2: entry.uvs2
2954
+ ? entry.uvs2 instanceof Float32Array
2955
+ ? entry.uvs2
2956
+ : new Float32Array(entry.uvs2)
2957
+ : undefined,
2768
2958
  normals: entry.normals
2769
2959
  ? entry.normals instanceof Float32Array
2770
2960
  ? entry.normals
@@ -2785,6 +2975,10 @@ export class ParticleSystem implements IParticleSystem {
2785
2975
  positions,
2786
2976
  indices: ensureTriangleIndices(positions, indices),
2787
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,
2788
2982
  normals: data.attributes.normal?.array ? new Float32Array(data.attributes.normal.array) : undefined,
2789
2983
  };
2790
2984
  }
@@ -2847,6 +3041,9 @@ export class ParticleSystem implements IParticleSystem {
2847
3041
  startLength: (this.rendererEmitterSettings as TrailSettings).startLength.clone(),
2848
3042
  followLocalOrigin: (this.rendererEmitterSettings as TrailSettings).followLocalOrigin,
2849
3043
  ...trailFlags(this.rendererEmitterSettings as TrailSettings),
3044
+ ...((this.rendererEmitterSettings as TrailSettings).sideAxis
3045
+ ? {sideAxis: (this.rendererEmitterSettings as TrailSettings).sideAxis!.clone()}
3046
+ : {}),
2850
3047
  };
2851
3048
  } else if (this.renderMode === RenderMode.StretchedBillBoard) {
2852
3049
  rendererEmitterSettings = {
@@ -2877,6 +3074,7 @@ export class ParticleSystem implements IParticleSystem {
2877
3074
  instancingGeometry: this.rendererSettings.instancingGeometry,
2878
3075
  instancingIndices: this.rendererSettings.instancingIndices,
2879
3076
  instancingUVs: this.rendererSettings.instancingUVs,
3077
+ instancingUVs2: this.rendererSettings.instancingUVs2,
2880
3078
  instancingNormals: this.rendererSettings.instancingNormals,
2881
3079
  renderMode: this.renderMode,
2882
3080
  renderOrder: this.renderOrder,
@@ -2912,9 +3110,10 @@ export class ParticleSystem implements IParticleSystem {
2912
3110
  flip: this.flip,
2913
3111
  lighting: this.lighting,
2914
3112
  rim: this.rim,
2915
- uvScroll: this.uvScroll,
3113
+ mapUV: this.mapUV,
2916
3114
  gradientMap: this.gradientMap,
2917
3115
  erosion: this.erosion,
3116
+ uvDistortion: this.uvDistortion,
2918
3117
  scene: this.emitter.getScene(),
2919
3118
  });
2920
3119
  // A sub emitter behavior transforms by its parent system's emitter;
@@ -2924,6 +3123,9 @@ export class ParticleSystem implements IParticleSystem {
2924
3123
  behavior.particleSystem = copy;
2925
3124
  }
2926
3125
  }
3126
+ if (this.tint) {
3127
+ copy.tint = this.tint.clone();
3128
+ }
2927
3129
  return copy;
2928
3130
  }
2929
3131
  }
@@ -75,6 +75,7 @@ interface ParsedGeometry {
75
75
  positions: Float32Array;
76
76
  indices: Uint32Array | Uint16Array;
77
77
  uvs?: Float32Array;
78
+ uvs2?: Float32Array;
78
79
  normals?: Float32Array;
79
80
  }
80
81
 
@@ -101,6 +102,7 @@ function geometryKey(geometry: ParsedGeometry): string {
101
102
  geometry.positions,
102
103
  geometry.indices,
103
104
  geometry.uvs,
105
+ geometry.uvs2,
104
106
  geometry.normals,
105
107
  ];
106
108
  let lengths = '';
@@ -313,6 +315,7 @@ export class QuarksLoader {
313
315
  positions: new Float32Array(geom.positions ?? []),
314
316
  indices: geom.indices ? new Uint32Array(geom.indices) : new Uint32Array(0),
315
317
  uvs: geom.uvs ? new Float32Array(geom.uvs) : undefined,
318
+ uvs2: geom.uvs2 ? new Float32Array(geom.uvs2) : undefined,
316
319
  normals: geom.normals ? new Float32Array(geom.normals) : undefined,
317
320
  });
318
321
  }
@@ -427,7 +430,10 @@ export class QuarksLoader {
427
430
  let positions = new Float32Array(0);
428
431
  let indices: Uint32Array | Uint16Array = new Uint32Array(0);
429
432
  let uvs: Float32Array | undefined;
433
+ let uvs2: Float32Array | undefined;
430
434
  let normals: Float32Array | undefined;
435
+ // three.js names the second UV set `uv1` (r151 on), `uv2` before.
436
+ const uv2Attribute = data.attributes?.uv1 ?? data.attributes?.uv2;
431
437
 
432
438
  if (data.interleavedBuffers && data.arrayBuffers) {
433
439
  const buffers: {[uuid: string]: {data: Float32Array; stride: number}} = {};
@@ -453,6 +459,16 @@ export class QuarksLoader {
453
459
  const buf = buffers[attr.data];
454
460
  if (buf) uvs = this.extractInterleavedAttribute(buf.data, buf.stride, attr.offset, attr.itemSize);
455
461
  }
462
+ if (uv2Attribute?.isInterleavedBufferAttribute) {
463
+ const buf = buffers[uv2Attribute.data];
464
+ if (buf)
465
+ uvs2 = this.extractInterleavedAttribute(
466
+ buf.data,
467
+ buf.stride,
468
+ uv2Attribute.offset,
469
+ uv2Attribute.itemSize
470
+ );
471
+ }
456
472
  if (data.attributes.normal?.isInterleavedBufferAttribute) {
457
473
  const attr = data.attributes.normal;
458
474
  const buf = buffers[attr.data];
@@ -467,6 +483,9 @@ export class QuarksLoader {
467
483
  if (data.attributes.uv?.array) {
468
484
  uvs = new Float32Array(data.attributes.uv.array);
469
485
  }
486
+ if (uv2Attribute?.array) {
487
+ uvs2 = new Float32Array(uv2Attribute.array);
488
+ }
470
489
  if (data.attributes.normal?.array) {
471
490
  normals = new Float32Array(data.attributes.normal.array);
472
491
  }
@@ -482,7 +501,7 @@ export class QuarksLoader {
482
501
 
483
502
  indices = ensureTriangleIndices(positions, indices);
484
503
 
485
- return {positions, indices, uvs, normals};
504
+ return {positions, indices, uvs, uvs2, normals};
486
505
  }
487
506
 
488
507
  private extractInterleavedAttribute(buffer: Float32Array, stride: number, offset: number, itemSize: number): any {
@@ -666,7 +685,7 @@ export class QuarksLoader {
666
685
  const data = await loadGeometry(geom.url);
667
686
  // Copy arrays so a shared fetch-cache document is not aliased across
668
687
  // effects — mutating one effect's inlined geometry must not corrupt others.
669
- for (const key of ['positions', 'indices', 'uvs', 'normals']) {
688
+ for (const key of ['positions', 'indices', 'uvs', 'uvs2', 'normals']) {
670
689
  const value = data?.[key];
671
690
  if (value === undefined) {
672
691
  continue;
@@ -824,13 +843,18 @@ export class QuarksLoader {
824
843
  if (matDef.rim && typeof matDef.rim === 'object') {
825
844
  matInfo.rim = matDef.rim;
826
845
  }
827
- // Texture effects (QuarksMaterial): uvScroll [u, v], gradientMap texture uuid,
828
- // erosion {mask (texture uuid), softness, edgeWidth, edgeColor, maskScroll}.
829
- if (Array.isArray(matDef.uvScroll)) {
830
- matInfo.uvScroll = matDef.uvScroll;
846
+ // Texture effects (QuarksMaterial): mapUV {channel, tiling, offset, scroll},
847
+ // gradientMap {ramp (texture uuid), channel},
848
+ // erosion {mask (texture uuid), maskUV {channel, tiling, offset, scroll}, softness, edgeWidth,
849
+ // edgeColor}.
850
+ if (matDef.mapUV && typeof matDef.mapUV === 'object') {
851
+ matInfo.mapUV = matDef.mapUV;
831
852
  }
832
- if (matDef.gradientMap) {
833
- matInfo.gradientMap = meta.textures[matDef.gradientMap] || null;
853
+ if (matDef.gradientMap && typeof matDef.gradientMap === 'object') {
854
+ matInfo.gradientMap = {
855
+ ...matDef.gradientMap,
856
+ ramp: matDef.gradientMap.ramp ? meta.textures[matDef.gradientMap.ramp] || null : null,
857
+ };
834
858
  }
835
859
  if (matDef.erosion && typeof matDef.erosion === 'object') {
836
860
  matInfo.erosion = {
@@ -838,6 +862,13 @@ export class QuarksLoader {
838
862
  mask: matDef.erosion.mask ? meta.textures[matDef.erosion.mask] || null : null,
839
863
  };
840
864
  }
865
+ // uvDistortion {map (texture uuid), mapUV {channel, tiling, offset, scroll}, strength}.
866
+ if (matDef.uvDistortion && typeof matDef.uvDistortion === 'object') {
867
+ matInfo.uvDistortion = {
868
+ ...matDef.uvDistortion,
869
+ map: matDef.uvDistortion.map ? meta.textures[matDef.uvDistortion.map] || null : null,
870
+ };
871
+ }
841
872
 
842
873
  // QuarksMaterial (what ParticleSystem.ensureMaterialMeta emits) writes `alphaMode`
843
874
  // directly as a Babylon alpha-mode constant; three.js materials (MeshBasicMaterial
@@ -887,6 +918,9 @@ export class QuarksLoader {
887
918
  if (geometry.uvs && geometry.uvs.length > 0) {
888
919
  vertexData.uvs = geometry.uvs;
889
920
  }
921
+ if (geometry.uvs2 && geometry.uvs2.length > 0) {
922
+ vertexData.uvs2 = geometry.uvs2;
923
+ }
890
924
  if (geometry.normals && geometry.normals.length > 0) {
891
925
  vertexData.normals = geometry.normals;
892
926
  }