babylon.quarks 0.27.0 → 0.29.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 (55) hide show
  1. package/CHANGELOG.md +585 -0
  2. package/README.md +3 -1
  3. package/dist/babylon.quarks.cjs +1 -1
  4. package/dist/babylon.quarks.cjs.map +1 -1
  5. package/dist/babylon.quarks.esm.js +1 -1
  6. package/dist/babylon.quarks.esm.js.map +1 -1
  7. package/dist/babylon.quarks.umd.min.js +1 -1
  8. package/dist/babylon.quarks.umd.min.js.map +1 -1
  9. package/dist/types/index.d.cts +4811 -974
  10. package/dist/types/index.d.ts +4811 -974
  11. package/package.json +7 -2
  12. package/src/BatchedRenderer.ts +106 -79
  13. package/src/EffectPool.ts +96 -16
  14. package/src/MeshSurfaceEmitter.ts +24 -13
  15. package/src/ParticleEmitter.ts +119 -26
  16. package/src/ParticleSystem.ts +305 -55
  17. package/src/QuarksLoader.ts +298 -171
  18. package/src/QuarksPrefab.ts +301 -94
  19. package/src/QuarksSerializer.ts +44 -16
  20. package/src/QuarksUtil.ts +88 -11
  21. package/src/SpriteBatch.ts +24 -6
  22. package/src/TrailBatch.ts +20 -7
  23. package/src/VFXBatch.ts +182 -528
  24. package/src/batchKey.ts +6 -2
  25. package/src/batchSettings.ts +548 -0
  26. package/src/effectDocumentJSON.ts +261 -0
  27. package/src/effectResources.ts +21 -24
  28. package/src/envAtlas.ts +8 -2
  29. package/src/geometryUtil.ts +1 -1
  30. package/src/index.ts +41 -4
  31. package/src/nodeId.ts +25 -2
  32. package/src/shaderProgram.ts +213 -16
  33. package/src/shaderVariants.ts +124 -7
  34. package/src/shaders/chunks/blend.ts +39 -0
  35. package/src/shaders/chunks/fog.ts +8 -3
  36. package/src/shaders/chunks/imageProcessing.ts +143 -0
  37. package/src/shaders/chunks/soft.ts +6 -5
  38. package/src/shaders/chunks/surface.ts +2 -2
  39. package/src/shaders/particle_frag.glsl.ts +5 -1
  40. package/src/shaders/particle_frag.wgsl.ts +4 -0
  41. package/src/shaders/particle_physics_frag.glsl.ts +7 -2
  42. package/src/shaders/particle_physics_frag.wgsl.ts +4 -0
  43. package/src/shaders/trail_frag.glsl.ts +6 -1
  44. package/src/shaders/trail_frag.wgsl.ts +4 -0
  45. package/src/system/EmissionController.ts +23 -20
  46. package/src/system/EventDispatcher.ts +26 -10
  47. package/src/system/ParticleSpawner.ts +96 -19
  48. package/src/system/geometry.ts +22 -29
  49. package/src/system/integrate.ts +61 -4
  50. package/src/system/materialResolve.ts +56 -7
  51. package/src/system/normalize.ts +34 -2
  52. package/src/system/serialization.ts +54 -33
  53. package/src/system/settingsSchema.ts +57 -17
  54. package/src/system/types.ts +150 -42
  55. package/src/trailSettings.ts +9 -6
@@ -1,3 +1,4 @@
1
+ import type {BufferGeometryDataJSON, GeometryArrays, GeometryEntry} from '../effectDocumentJSON';
1
2
  import {ensureTriangleIndices} from '../geometryUtil';
2
3
 
3
4
  /** Positions of the unit quad billboards are drawn with, and the default mesh. */
@@ -47,19 +48,19 @@ export function isFallbackNormals(normals: Float32Array | undefined, positions:
47
48
  return normals !== undefined && normals === fallbackNormals.get(positions);
48
49
  }
49
50
 
51
+ /** `values` as a Float32Array: itself when it is one, else a copy. */
52
+ function floats(values: number[] | Float32Array | undefined): Float32Array | undefined {
53
+ return values ? (values instanceof Float32Array ? values : new Float32Array(values)) : undefined;
54
+ }
55
+
50
56
  /**
51
57
  * Typed arrays from a geometry entry of an effect document: quarks' own `{positions, indices, uvs,
52
- * uvs2, normals}`, or a three.js `BufferGeometry` JSON. Anything else gives the default quad.
58
+ * uvs2, normals}`, or a three.js `BufferGeometry` JSON or its `data`. Anything else gives the
59
+ * default quad.
53
60
  */
54
- export function resolveGeometryData(entry: any): {
55
- positions: Float32Array;
56
- indices: Uint32Array | Uint16Array;
57
- uvs?: Float32Array;
58
- uvs2?: Float32Array;
59
- normals?: Float32Array;
60
- } {
61
- if (entry?.positions) {
62
- const positions = entry.positions instanceof Float32Array ? entry.positions : new Float32Array(entry.positions);
61
+ export function resolveGeometryData(entry: GeometryEntry | BufferGeometryDataJSON | undefined): GeometryArrays {
62
+ if (entry && 'positions' in entry && entry.positions) {
63
+ const positions = floats(entry.positions)!;
63
64
  const rawIndices =
64
65
  entry.indices instanceof Uint16Array || entry.indices instanceof Uint32Array
65
66
  ? entry.indices
@@ -69,23 +70,17 @@ export function resolveGeometryData(entry: any): {
69
70
  return {
70
71
  positions,
71
72
  indices: ensureTriangleIndices(positions, rawIndices),
72
- uvs: entry.uvs ? (entry.uvs instanceof Float32Array ? entry.uvs : new Float32Array(entry.uvs)) : undefined,
73
- uvs2: entry.uvs2
74
- ? entry.uvs2 instanceof Float32Array
75
- ? entry.uvs2
76
- : new Float32Array(entry.uvs2)
77
- : undefined,
78
- normals: entry.normals
79
- ? entry.normals instanceof Float32Array
80
- ? entry.normals
81
- : new Float32Array(entry.normals)
82
- : undefined,
73
+ uvs: floats(entry.uvs),
74
+ uvs2: floats(entry.uvs2),
75
+ normals: floats(entry.normals),
83
76
  };
84
77
  }
85
78
 
86
- const data = entry?.data ?? entry;
87
- if (data?.attributes?.position?.array) {
88
- const positions = new Float32Array(data.attributes.position.array);
79
+ const data = entry && 'data' in entry ? entry.data : entry;
80
+ const attributes = data && 'attributes' in data ? data.attributes : undefined;
81
+ const position = attributes?.position?.array;
82
+ if (data && 'attributes' in data && attributes && position) {
83
+ const positions = new Float32Array(position);
89
84
  let indices: Uint32Array | Uint16Array | undefined;
90
85
  if (data.index?.array) {
91
86
  const indexType = data.index.type === 'Uint16Array' ? Uint16Array : Uint32Array;
@@ -94,12 +89,10 @@ export function resolveGeometryData(entry: any): {
94
89
  return {
95
90
  positions,
96
91
  indices: ensureTriangleIndices(positions, indices),
97
- uvs: data.attributes.uv?.array ? new Float32Array(data.attributes.uv.array) : undefined,
92
+ uvs: floats(attributes.uv?.array),
98
93
  // three.js names the second set `uv1` (r151 on), `uv2` before.
99
- uvs2: (data.attributes.uv1 ?? data.attributes.uv2)?.array
100
- ? new Float32Array((data.attributes.uv1 ?? data.attributes.uv2).array)
101
- : undefined,
102
- normals: data.attributes.normal?.array ? new Float32Array(data.attributes.normal.array) : undefined,
94
+ uvs2: floats((attributes.uv1 ?? attributes.uv2)?.array),
95
+ normals: floats(attributes.normal?.array),
103
96
  };
104
97
  }
105
98
 
@@ -129,6 +129,34 @@ function retire(particles: Particle[], store: ParticleStore, i: number, last: nu
129
129
  return particle;
130
130
  }
131
131
 
132
+ /**
133
+ * What a ring buffer does to a particle at the end of its life (see `RingBufferMode`): `pause`
134
+ * holds it frozen at its end, else it goes back to `loopStart` (a fraction of its life) once it
135
+ * reaches `loopEnd`.
136
+ */
137
+ export interface RingBufferStep {
138
+ /** Freeze at the end of life rather than loop. */
139
+ pause: boolean;
140
+ /** Where a looping particle goes back to, a fraction of its life, 0 to 1. */
141
+ loopStart: number;
142
+ /**
143
+ * Where a looping particle goes back from, a fraction of its life, `loopStart` to 1. Equal to
144
+ * `loopStart`, the particle is held there.
145
+ */
146
+ loopEnd: number;
147
+ }
148
+
149
+ /** The least age a held particle keeps, as a fraction of its life: past 0, a newborn's age. */
150
+ const HELD_AGE = 1e-6;
151
+
152
+ /**
153
+ * The age a looping ring particle is held at, or Infinity when it loops: a loop range of no
154
+ * length holds it at that point of its life, as a pause does at the end.
155
+ */
156
+ function holdAge(ring: RingBufferStep, life: number): number {
157
+ return ring.loopEnd > ring.loopStart ? Infinity : Math.max(ring.loopEnd, HELD_AGE) * life;
158
+ }
159
+
132
160
  /**
133
161
  * The sprite path's step after the behaviors, as one pass over the columns that does three
134
162
  * things, none of which touches a particle object until a death. Returns the live count.
@@ -142,7 +170,8 @@ function retire(particles: Particle[], store: ParticleStore, i: number, last: nu
142
170
  * Position is integrated along the velocity.
143
171
  *
144
172
  * A particle whose age reached its life is swapped with the last live one, rows included. The
145
- * survivor moved into the slot has not been visited yet, so the slot is processed again.
173
+ * survivor moved into the slot has not been visited yet, so the slot is processed again. With a
174
+ * `ring` buffer no particle dies: it is frozen at the end of its life, or sent back into it.
146
175
  *
147
176
  * The bounds fall out of the same pass. Measuring every particle on every step costs about as
148
177
  * much as the integration itself, so the full measurement runs every few steps; in between,
@@ -158,9 +187,11 @@ export function integrateSprites(
158
187
  collapseColor: boolean,
159
188
  bounds: BoundsTracker,
160
189
  settings: BoundsSettings,
161
- onDied: DeathListener | undefined
190
+ onDied: DeathListener | undefined,
191
+ ring?: RingBufferStep
162
192
  ): number {
163
193
  const scalars = store.scalars;
194
+ const pause = ring !== undefined && ring.pause;
164
195
  const stride = ParticleStore.SCALAR_STRIDE;
165
196
  const positions = store.position;
166
197
  const velocities = store.velocity;
@@ -198,7 +229,12 @@ export function integrateSprites(
198
229
  prevColor[o4 + 3] = curColor[o4 + 3];
199
230
  }
200
231
  }
201
- const step = delta * scalars[scalar + ParticleStore.SPEED_MODIFIER];
232
+ const life = scalars[scalar + ParticleStore.LIFE];
233
+ // Frozen until a newborn replaces it, no motion and no ageing: at the end of its life when
234
+ // paused, or at a loop range of no length, held a hair past 0 so Birth fires only once.
235
+ const hold = ring === undefined ? Infinity : pause ? life : holdAge(ring, life);
236
+ const frozen = age >= hold;
237
+ const step = frozen ? 0 : delta * scalars[scalar + ParticleStore.SPEED_MODIFIER];
202
238
  const vx = velocities[offset];
203
239
  const vy = velocities[offset + 1];
204
240
  const vz = velocities[offset + 2];
@@ -219,10 +255,31 @@ export function integrateSprites(
219
255
  }
220
256
  // Speed is tracked every step: it bounds how far the particles
221
257
  // can have moved since the last measurement.
258
+ if (frozen) {
259
+ continue;
260
+ }
222
261
  maxSpeedSq = Math.max(maxSpeedSq, vx * vx + vy * vy + vz * vz);
223
262
  age += delta;
224
263
  scalars[scalar + ParticleStore.AGE] = age;
225
- if (age >= scalars[scalar + ParticleStore.LIFE]) {
264
+ if (ring !== undefined) {
265
+ if (age > hold) {
266
+ scalars[scalar + ParticleStore.AGE] = hold;
267
+ } else if (!pause && hold === Infinity && age >= ring.loopEnd * life) {
268
+ const loopStart = ring.loopStart * life;
269
+ const wrapped = loopStart + ((age - ring.loopEnd * life) % ((ring.loopEnd - ring.loopStart) * life));
270
+ // Never exactly 0, which is a newborn's age: Birth sub emitters would fire again.
271
+ scalars[scalar + ParticleStore.AGE] = wrapped > 0 ? wrapped : HELD_AGE * life;
272
+ // The jump back is not motion: the renderer must not extrapolate size or colour across it.
273
+ prevSize[offset] = curSize[offset];
274
+ prevSize[offset + 1] = curSize[offset + 1];
275
+ prevSize[offset + 2] = curSize[offset + 2];
276
+ const o4 = i * 4;
277
+ prevColor[o4] = curColor[o4];
278
+ prevColor[o4 + 1] = curColor[o4 + 1];
279
+ prevColor[o4 + 2] = curColor[o4 + 2];
280
+ prevColor[o4 + 3] = curColor[o4 + 3];
281
+ }
282
+ } else if (age >= life) {
226
283
  const particle = retire(particles, store, i, liveParticleCount - 1);
227
284
  liveParticleCount--;
228
285
  i--;
@@ -1,5 +1,5 @@
1
1
  import type {BaseTexture} from '@babylonjs/core/Materials/Textures/baseTexture';
2
- import type {Texture} from '@babylonjs/core/Materials/Textures/texture';
2
+ import {Texture} from '@babylonjs/core/Materials/Textures/texture';
3
3
  import type {VFXBatchSettings} from '../BatchedRenderer';
4
4
 
5
5
  /** Settings given next to a material, which win over what is read from it. */
@@ -14,6 +14,52 @@ export interface MaterialOverrides {
14
14
  layerMask?: number;
15
15
  }
16
16
 
17
+ /**
18
+ * What a {@link ParticleSystem} reads its draw settings from: a Babylon material (`StandardMaterial`,
19
+ * `PBRMaterial`, a node material, …), or a material entry of an effect file once its texture slots
20
+ * are resolved to textures, as `ParticleSystem.fromJSON` does. Each field is read only when it is
21
+ * there, so any material fits; the particles are still drawn with quarks' own shaders. The particle
22
+ * texture is the first slot that holds a `Texture`.
23
+ */
24
+ export interface ParticleMaterialSource {
25
+ /** A PBR material's base colour texture. */
26
+ albedoTexture?: BaseTexture | null;
27
+ /** A standard material's diffuse texture. */
28
+ diffuseTexture?: BaseTexture | null;
29
+ /** An emissive texture, read when there is no albedo or diffuse texture. */
30
+ emissiveTexture?: BaseTexture | null;
31
+ /** An opacity texture, read when there is none of the above. */
32
+ opacityTexture?: BaseTexture | null;
33
+ /** A base texture, read last. */
34
+ baseTexture?: BaseTexture | null;
35
+ /** The reflection texture; a cube is drawn through a 3×2 atlas built from it. */
36
+ reflectionTexture?: BaseTexture | null;
37
+ /** A PBR material's environment texture, read when there is no reflection texture. */
38
+ environmentTexture?: BaseTexture | null;
39
+ /** How strong the reflection is; the reflection texture's `level` when unset. */
40
+ reflectionLevel?: number;
41
+ /** A 3×2 env atlas for the reflection; the system stores the atlas it builds from a cube here. */
42
+ reflectionAtlas?: BaseTexture | null;
43
+ /** A Babylon alpha mode constant. */
44
+ alphaMode?: number;
45
+ /** Whether the material blends; decides `transparent`. */
46
+ needAlphaBlending?(): boolean;
47
+ /** Opacity, read for `transparent` when there is no `needAlphaBlending`. */
48
+ alpha?: number;
49
+ /** Inverted into `depthTest`. */
50
+ disableDepthTest?: boolean;
51
+ /** Inverted into `depthWrite`. */
52
+ disableDepthWrite?: boolean;
53
+ /** Read for `depthWrite` when there is no `disableDepthWrite`. */
54
+ forceDepthWrite?: boolean;
55
+ /** The alpha test cutoff is read only in an alpha-test mode (1 or 3). */
56
+ transparencyMode?: number | null;
57
+ /** The alpha test cutoff. */
58
+ alphaCutOff?: number;
59
+ /** The alpha test cutoff, as some materials name it. */
60
+ alphaCutOffValue?: number;
61
+ }
62
+
17
63
  /** The draw settings a {@link ParticleSystem} takes from a material, before the env atlas is built. */
18
64
  export interface ResolvedMaterialSettings {
19
65
  texture: Texture | null;
@@ -39,18 +85,21 @@ export function liveAtlas<T extends BaseTexture>(atlas: T | null | undefined): T
39
85
  * material does not have keeps its `current` value.
40
86
  */
41
87
  export function resolveMaterialSettings(
42
- material: any,
88
+ material: ParticleMaterialSource | null | undefined,
43
89
  overrides: MaterialOverrides,
44
90
  current: VFXBatchSettings
45
91
  ): ResolvedMaterialSettings {
92
+ // The particle texture is a `Texture`: the first slot that holds one, so a cube in a slot is passed over.
46
93
  const texture =
47
94
  overrides.texture !== undefined
48
95
  ? overrides.texture
49
- : (material?.albedoTexture ??
50
- material?.diffuseTexture ??
51
- material?.emissiveTexture ??
52
- material?.opacityTexture ??
53
- material?.baseTexture ??
96
+ : ([
97
+ material?.albedoTexture,
98
+ material?.diffuseTexture,
99
+ material?.emissiveTexture,
100
+ material?.opacityTexture,
101
+ material?.baseTexture,
102
+ ].find((slot): slot is Texture => slot instanceof Texture) ??
54
103
  current.texture ??
55
104
  null);
56
105
  const reflectionTexture =
@@ -1,6 +1,6 @@
1
1
  import {ConstantValue, FunctionValueGenerator, ValueGenerator, Vector2, Vector3} from 'quarks.core';
2
- import type {MeshLighting, SortMode} from '../VFXBatch';
3
- import type {BurstParameters, CullingMode} from './types';
2
+ import type {MeshLighting, RenderFace, SortMode} from '../batchSettings';
3
+ import type {BurstParameters, CullingMode, RingBufferMode, ScalingMode, StopAction} from './types';
4
4
 
5
5
  // Normalisers shared by the ParticleSystem constructor, its setters and its JSON reader, so a
6
6
  // value read from a file and one set in code end up the same.
@@ -15,11 +15,43 @@ export function validSortMode(mode: unknown): SortMode {
15
15
  return mode === 'distance' || mode === 'oldestInFront' || mode === 'youngestInFront' ? mode : 'none';
16
16
  }
17
17
 
18
+ /** `face` if it is one of the render faces, else `'both'`. */
19
+ export function validRenderFace(face: unknown): RenderFace {
20
+ return face === 'front' || face === 'back' ? face : 'both';
21
+ }
22
+
18
23
  /** `mode` if it is one of the culling modes, else `'always'`. */
19
24
  export function validCullingMode(mode: unknown): CullingMode {
20
25
  return mode === 'pause' || mode === 'pauseAndCatchUp' ? mode : 'always';
21
26
  }
22
27
 
28
+ /** `action` if it is one of the stop actions, else `'none'`. */
29
+ export function validStopAction(action: unknown): StopAction {
30
+ return action === 'disable' || action === 'destroy' ? action : 'none';
31
+ }
32
+
33
+ /** `mode` if it is one of the ring buffer modes, else `'disabled'`. */
34
+ export function validRingBufferMode(mode: unknown): RingBufferMode {
35
+ return mode === 'pauseUntilReplaced' || mode === 'loopUntilReplaced' ? mode : 'disabled';
36
+ }
37
+
38
+ /**
39
+ * A ring buffer loop range within 0..1, start no later than end; a missing or non-finite bound
40
+ * takes its default (0 or 1).
41
+ */
42
+ export function validLoopRange(range: {x: number; y: number} | undefined): Vector2 {
43
+ const clamp = (value: number | undefined, fallback: number) =>
44
+ typeof value === 'number' && Number.isFinite(value) ? Math.min(1, Math.max(0, value)) : fallback;
45
+ const start = clamp(range?.x, 0);
46
+ const end = clamp(range?.y, 1);
47
+ return new Vector2(Math.min(start, end), Math.max(start, end));
48
+ }
49
+
50
+ /** `mode` if it is one of the scaling modes, else `'local'`. */
51
+ export function validScalingMode(mode: unknown): ScalingMode {
52
+ return mode === 'hierarchy' || mode === 'shape' ? mode : 'local';
53
+ }
54
+
23
55
  /** `count` as a whole number of particles, or undefined (no limit) when it is unset, NaN or negative. */
24
56
  export function validMaxParticles(count: unknown): number | undefined {
25
57
  return typeof count === 'number' && count >= 0 && count < Infinity ? Math.floor(count) : undefined;
@@ -3,14 +3,20 @@ import type {BaseTexture} from '@babylonjs/core/Materials/Textures/baseTexture';
3
3
  import {Texture} from '@babylonjs/core/Materials/Textures/texture';
4
4
  import type {Scene} from '@babylonjs/core/scene';
5
5
  import {
6
+ BehaviorToJSON,
6
7
  ConstantValue,
7
8
  EmitterFromJSON,
9
+ EmitterToJSON,
10
+ readFormatPart,
8
11
  RendererEmitterSettings,
9
12
  StretchedBillBoardSettings,
10
13
  TrailSettings,
11
14
  ValueGeneratorFromJSON,
12
15
  Vector3,
16
+ Vector3ArrayJSON,
17
+ warnFormat,
13
18
  } from 'quarks.core';
19
+ import type {MaterialTextureRef} from '../effectDocumentJSON';
14
20
  import type {ParticleSystem} from '../ParticleSystem';
15
21
  import {
16
22
  trailCurveTension,
@@ -19,6 +25,7 @@ import {
19
25
  trailTessellation,
20
26
  trailTextureMode,
21
27
  trailTextureScale,
28
+ type TrailOptions,
22
29
  } from '../trailSettings';
23
30
  import {
24
31
  EROSION_DEFAULT_SOFTNESS,
@@ -28,8 +35,15 @@ import {
28
35
  UV_DISTORTION_DEFAULT_UV,
29
36
  } from '../VFXBatch';
30
37
  import {resolveGeometryData} from './geometry';
38
+ import type {ParticleMaterialSource} from './materialResolve';
31
39
  import {settingsFromJSON, settingsToJSON} from './settingsSchema';
32
- import type {BabylonMetaData, ParticleSystemJSONParameters, ParticleSystemParameters} from './types';
40
+ import type {
41
+ BabylonMetaData,
42
+ ParticleSystemJSONParameters,
43
+ ParticleSystemParameters,
44
+ RendererEmitterSettingsJSON,
45
+ TrailSettingsJSON,
46
+ } from './types';
33
47
 
34
48
  // Reading and writing a ParticleSystem's JSON. The class's toJSON and fromJSON call these.
35
49
 
@@ -49,8 +63,8 @@ function nextSerializationId(meta: BabylonMetaData, prefix: string): string {
49
63
  }
50
64
 
51
65
  /** The optional trail flags that are switched on, so JSON and clones only carry the ones in use. */
52
- export function trailFlags(settings: TrailSettings | undefined): Partial<TrailSettings> {
53
- const flags: Partial<TrailSettings> = {};
66
+ export function trailFlags(settings: Partial<TrailOptions> | undefined): Partial<TrailOptions> {
67
+ const flags: Partial<TrailOptions> = {};
54
68
  if (settings?.uvFromHead) flags.uvFromHead = true;
55
69
  if (settings?.colorFromParticle) flags.colorFromParticle = true;
56
70
  if (settings?.dieWithParticle) flags.dieWithParticle = true;
@@ -59,8 +73,8 @@ export function trailFlags(settings: TrailSettings | undefined): Partial<TrailSe
59
73
  }
60
74
 
61
75
  /** A trail's tessellation and curve tension, normalised, each only when it differs from the default. */
62
- export function trailCurve(settings: TrailSettings | undefined): Partial<TrailSettings> {
63
- const curve: Partial<TrailSettings> = {};
76
+ export function trailCurve(settings: Partial<TrailOptions> | undefined): Partial<TrailOptions> {
77
+ const curve: Partial<TrailOptions> = {};
64
78
  const tessellation = trailTessellation(settings);
65
79
  const curveTension = trailCurveTension(settings);
66
80
  if (tessellation > 0) curve.tessellation = tessellation;
@@ -72,8 +86,8 @@ export function trailCurve(settings: TrailSettings | undefined): Partial<TrailSe
72
86
  * A trail's texture mode and scale, minimum vertex distance and lifetime, normalised, each only
73
87
  * when it differs from the default.
74
88
  */
75
- export function trailSampling(settings: TrailSettings | undefined): Partial<TrailSettings> {
76
- const out: Partial<TrailSettings> = {};
89
+ export function trailSampling(settings: Partial<TrailOptions> | undefined): Partial<TrailOptions> {
90
+ const out: Partial<TrailOptions> = {};
77
91
  const textureMode = trailTextureMode(settings);
78
92
  const textureScale = trailTextureScale(settings);
79
93
  const minVertexDistance = trailMinVertexDistance(settings);
@@ -86,12 +100,12 @@ export function trailSampling(settings: TrailSettings | undefined): Partial<Trai
86
100
  }
87
101
 
88
102
  /** A trail's `followLocalOrigin`, only when on: off is the default. */
89
- export function trailFollowLocalOrigin(settings: TrailSettings | undefined): Partial<TrailSettings> {
103
+ export function trailFollowLocalOrigin(settings: Partial<TrailOptions> | undefined): Partial<TrailOptions> {
90
104
  return settings?.followLocalOrigin ? {followLocalOrigin: true} : {};
91
105
  }
92
106
 
93
107
  /** A trail's side axis as JSON `[x, y, z]`, or nothing when unset. */
94
- function trailSideAxisJSON(settings: TrailSettings | undefined): {sideAxis?: number[]} {
108
+ function trailSideAxisJSON(settings: TrailSettings | undefined): {sideAxis?: Vector3ArrayJSON} {
95
109
  const axis = settings?.sideAxis;
96
110
  return axis ? {sideAxis: [axis.x, axis.y, axis.z]} : {};
97
111
  }
@@ -120,7 +134,7 @@ function trailSideAxisFromJSON(value: unknown): {sideAxis?: Vector3} {
120
134
  if (Number.isFinite(nx) && Number.isFinite(ny) && Number.isFinite(nz) && nx * nx + ny * ny + nz * nz > 0) {
121
135
  return {sideAxis: new Vector3(nx, ny, nz)};
122
136
  }
123
- console.warn(`[quarks] sideAxis ${JSON.stringify(value)} is not a direction and is ignored`);
137
+ warnFormat(`sideAxis ${JSON.stringify(value)} is not a direction and is ignored`);
124
138
  return {};
125
139
  }
126
140
 
@@ -149,15 +163,17 @@ export function particleSystemToJSON(
149
163
 
150
164
  // Only what differs from the defaults the constructor and fromJSON give a setting left out.
151
165
  // The shape, the settings the table marks `always`, the geometry, the material and the
152
- // behaviors are always written: the loaders of 0.27 and earlier, and three.quarks, need them.
166
+ // behaviors list are always written: the loaders of 0.27 and earlier need them. Within the
167
+ // shape and each behavior, only the fields that differ from their defaults, which 0.27 reads
168
+ // and three.quarks may not (see docs/FORMAT.md).
153
169
  return {
154
170
  version: '3.0',
155
- shape: system.emitterShape.toJSON(),
171
+ shape: EmitterToJSON(system.emitterShape),
156
172
  ...settingsToJSON(system),
157
173
  instancingGeometry: geometryUUID,
158
- ...(Object.keys(rendererSettingsJSON).length > 0 ? {rendererEmitterSettings: rendererSettingsJSON} : {}),
174
+ ...(rendererSettingsJSON ? {rendererEmitterSettings: rendererSettingsJSON} : {}),
159
175
  material: materialUUID,
160
- behaviors: system.behaviors.map((b) => b.toJSON()),
176
+ behaviors: system.behaviors.map((b) => BehaviorToJSON(b, system)),
161
177
  };
162
178
  }
163
179
 
@@ -172,10 +188,10 @@ export function particleSystemParametersFromJSON(
172
188
  scene?: Scene
173
189
  ): ParticleSystemParameters {
174
190
  // A part the file leaves out takes the constructor's default, as a system made in code does.
175
- const shape = json.shape ? EmitterFromJSON(json.shape, meta as any) : undefined;
191
+ const shape = json.shape ? readFormatPart('shape', () => EmitterFromJSON(json.shape!, meta)) : undefined;
176
192
  let rendererEmitterSettings: RendererEmitterSettings;
177
193
  if (json.renderMode === RenderMode.Trail) {
178
- const trailSettings = json.rendererEmitterSettings as TrailSettings;
194
+ const trailSettings = json.rendererEmitterSettings as TrailSettingsJSON | undefined;
179
195
  rendererEmitterSettings = {
180
196
  startLength:
181
197
  trailSettings?.startLength !== undefined
@@ -185,26 +201,31 @@ export function particleSystemParametersFromJSON(
185
201
  ...trailFlags(trailSettings),
186
202
  ...trailCurve(trailSettings),
187
203
  ...trailSampling(trailSettings),
188
- ...trailSideAxisFromJSON((trailSettings as {sideAxis?: unknown} | undefined)?.sideAxis),
204
+ ...trailSideAxisFromJSON(trailSettings?.sideAxis),
189
205
  };
190
206
  } else if (json.renderMode === RenderMode.StretchedBillBoard) {
191
- rendererEmitterSettings = {...(json.rendererEmitterSettings ?? {})};
207
+ // The render mode says which settings the file holds.
208
+ const stretched: Partial<StretchedBillBoardSettings> = {
209
+ ...(json.rendererEmitterSettings as StretchedBillBoardSettings | undefined),
210
+ };
192
211
  if (json.speedFactor !== undefined) {
193
- (rendererEmitterSettings as StretchedBillBoardSettings).speedFactor = json.speedFactor;
212
+ stretched.speedFactor = json.speedFactor;
194
213
  }
214
+ rendererEmitterSettings = stretched;
195
215
  } else {
196
216
  rendererEmitterSettings = {};
197
217
  }
198
218
 
199
219
  const materialMeta = json.material ? meta.materials?.[json.material] : undefined;
200
- const materialTextureRef = materialMeta?.texture ?? json.texture;
201
- const texture =
202
- typeof materialTextureRef === 'string'
203
- ? (meta.textures?.[materialTextureRef] ?? null)
204
- : (materialTextureRef ?? null);
205
- // Texture effects reference textures by uuid in a serialised meta, or hold them once a loader resolved them.
206
- const resolveTexture = (ref: unknown): BaseTexture | null =>
207
- typeof ref === 'string' ? (meta.textures?.[ref] ?? null) : ((ref as BaseTexture | null) ?? null);
220
+ // Texture slots reference textures by uuid in a serialised meta, or hold them once a loader resolved them.
221
+ const resolveTexture = (ref: MaterialTextureRef | undefined): BaseTexture | null =>
222
+ typeof ref === 'string' ? (meta.textures?.[ref] ?? null) : (ref ?? null);
223
+ const mainTexture = resolveTexture(materialMeta?.texture ?? json.texture);
224
+ // Particles sample a 2D texture.
225
+ const texture = mainTexture instanceof Texture ? mainTexture : null;
226
+ const material: ParticleMaterialSource | undefined =
227
+ materialMeta?.sourceMaterial ??
228
+ (materialMeta ? {...materialMeta, reflectionAtlas: resolveTexture(materialMeta.reflectionAtlas)} : undefined);
208
229
  const erosionMeta = materialMeta?.erosion;
209
230
  const gradientMeta = materialMeta?.gradientMap;
210
231
  const distortionMeta = materialMeta?.uvDistortion;
@@ -226,7 +247,7 @@ export function particleSystemParametersFromJSON(
226
247
  instancingNormals: resolvedGeometry.normals,
227
248
  rendererEmitterSettings,
228
249
  texture,
229
- material: materialMeta?.sourceMaterial ?? materialMeta,
250
+ material,
230
251
  // The material entry wins over the system's own fields, which older files wrote too.
231
252
  blendMode: materialMeta?.alphaMode ?? settings.blendMode ?? Constants.ALPHA_ADD,
232
253
  transparent: materialMeta?.transparent ?? settings.transparent ?? true,
@@ -253,7 +274,7 @@ export function particleSystemParametersFromJSON(
253
274
  }
254
275
 
255
276
  /** The render mode's own settings as JSON: a trail's or a stretched billboard's, else none. */
256
- function rendererEmitterSettingsJSON(system: ParticleSystem): RendererEmitterSettings {
277
+ function rendererEmitterSettingsJSON(system: ParticleSystem): RendererEmitterSettingsJSON | undefined {
257
278
  if (system.renderMode === RenderMode.Trail) {
258
279
  const settings = system.rendererEmitterSettings as TrailSettings;
259
280
  // `startLength` always, for loaders that read it without a default; the rest only when on.
@@ -263,8 +284,8 @@ function rendererEmitterSettingsJSON(system: ParticleSystem): RendererEmitterSet
263
284
  ...trailFlags(settings),
264
285
  ...trailCurve(settings),
265
286
  ...trailSampling(settings),
266
- ...(trailSideAxisJSON(settings) as {sideAxis?: any}),
267
- } as TrailSettings;
287
+ ...trailSideAxisJSON(settings),
288
+ };
268
289
  }
269
290
  if (system.renderMode === RenderMode.StretchedBillBoard) {
270
291
  return {
@@ -272,7 +293,7 @@ function rendererEmitterSettingsJSON(system: ParticleSystem): RendererEmitterSet
272
293
  lengthFactor: (system.rendererEmitterSettings as StretchedBillBoardSettings).lengthFactor,
273
294
  };
274
295
  }
275
- return {};
296
+ return undefined;
276
297
  }
277
298
 
278
299
  /** Writes the system's geometry into `meta` once per document, and returns its uuid. */
@@ -337,7 +358,7 @@ function materialMeta(system: ParticleSystem, meta: BabylonMetaData): string {
337
358
  if (uuid === undefined) {
338
359
  uuid = nextSerializationId(meta, 'quarks_texture');
339
360
  textures.set(value, uuid);
340
- meta.textures[uuid] = value as Texture;
361
+ meta.textures[uuid] = value;
341
362
  }
342
363
  return uuid;
343
364
  };