babylon.quarks 0.26.0 → 0.27.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 (79) hide show
  1. package/CHANGELOG.md +769 -8
  2. package/README.md +10 -8
  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 +6683 -0
  10. package/dist/types/index.d.ts +1959 -588
  11. package/package.json +12 -6
  12. package/src/BatchedRenderer.ts +204 -101
  13. package/src/EffectPool.ts +4 -1
  14. package/src/MeshSurfaceEmitter.ts +29 -4
  15. package/src/ParticleEmitter.ts +45 -15
  16. package/src/ParticleSystem.ts +909 -2061
  17. package/src/QuarksLoader.ts +421 -67
  18. package/src/QuarksPrefab.ts +91 -4
  19. package/src/QuarksSerializer.ts +484 -0
  20. package/src/QuarksUtil.ts +15 -0
  21. package/src/SpriteBatch.ts +106 -246
  22. package/src/TrailBatch.ts +533 -342
  23. package/src/VFXBatch.ts +236 -136
  24. package/src/batchKey.ts +237 -0
  25. package/src/effectResources.ts +79 -0
  26. package/src/envAtlas.ts +119 -17
  27. package/src/index.ts +32 -33
  28. package/src/nodeId.ts +13 -0
  29. package/src/shaderProgram.ts +325 -0
  30. package/src/shaderVariants.ts +225 -0
  31. package/src/shaders/chunks/fog.ts +4 -2
  32. package/src/shaders/chunks/sheet.ts +111 -0
  33. package/src/shaders/chunks/soft.ts +84 -0
  34. package/src/shaders/chunks/surface.ts +10 -0
  35. package/src/shaders/local_particle_physics_vert.glsl.ts +14 -28
  36. package/src/shaders/local_particle_physics_vert.wgsl.ts +16 -29
  37. package/src/shaders/particle_frag.glsl.ts +3 -22
  38. package/src/shaders/particle_frag.wgsl.ts +3 -23
  39. package/src/shaders/particle_physics_frag.glsl.ts +12 -25
  40. package/src/shaders/particle_physics_frag.wgsl.ts +12 -26
  41. package/src/shaders/particle_vert.glsl.ts +5 -27
  42. package/src/shaders/particle_vert.wgsl.ts +5 -26
  43. package/src/shaders/stretched_bb_particle_vert.glsl.ts +7 -33
  44. package/src/shaders/stretched_bb_particle_vert.wgsl.ts +7 -33
  45. package/src/shaders/trail_frag.glsl.ts +11 -1
  46. package/src/shaders/trail_frag.wgsl.ts +11 -0
  47. package/src/shaders/trail_vert.glsl.ts +45 -36
  48. package/src/shaders/trail_vert.wgsl.ts +46 -40
  49. package/src/softDepth.ts +136 -0
  50. package/src/system/BehaviorPlan.ts +85 -0
  51. package/src/system/BoundsTracker.ts +181 -0
  52. package/src/system/EmissionController.ts +360 -0
  53. package/src/system/EventDispatcher.ts +44 -0
  54. package/src/system/ParticleSpawner.ts +274 -0
  55. package/src/system/SimulationClock.ts +130 -0
  56. package/src/system/geometry.ts +111 -0
  57. package/src/system/integrate.ts +428 -0
  58. package/src/system/materialResolve.ts +116 -0
  59. package/src/system/normalize.ts +95 -0
  60. package/src/system/serialization.ts +401 -0
  61. package/src/system/settingsSchema.ts +317 -0
  62. package/src/system/types.ts +410 -0
  63. package/src/textureSource.ts +30 -0
  64. package/src/trailSettings.ts +51 -0
  65. package/src/BatchedParticleRenderer.ts +0 -3
  66. package/src/materials/ParticleMaterials.ts +0 -23
  67. package/src/materials/index.ts +0 -1
  68. package/src/shaders/chunks/index.ts +0 -1
  69. package/src/shaders/chunks/register-shader-chunks.ts +0 -32
  70. package/src/shaders/chunks/soft_fragment.glsl.ts +0 -13
  71. package/src/shaders/chunks/soft_pars_fragment.glsl.ts +0 -20
  72. package/src/shaders/chunks/soft_pars_vertex.glsl.ts +0 -6
  73. package/src/shaders/chunks/soft_vertex.glsl.ts +0 -6
  74. package/src/shaders/chunks/tile_fragment.glsl.ts +0 -9
  75. package/src/shaders/chunks/tile_pars_fragment.glsl.ts +0 -9
  76. package/src/shaders/chunks/tile_pars_vertex.glsl.ts +0 -28
  77. package/src/shaders/chunks/tile_vertex.glsl.ts +0 -18
  78. package/src/shaders/index.ts +0 -9
  79. package/src/shaders/local_particle_vert.glsl.ts +0 -50
@@ -0,0 +1,401 @@
1
+ import {Constants} from '@babylonjs/core/Engines/constants';
2
+ import type {BaseTexture} from '@babylonjs/core/Materials/Textures/baseTexture';
3
+ import {Texture} from '@babylonjs/core/Materials/Textures/texture';
4
+ import type {Scene} from '@babylonjs/core/scene';
5
+ import {
6
+ ConstantValue,
7
+ EmitterFromJSON,
8
+ RendererEmitterSettings,
9
+ StretchedBillBoardSettings,
10
+ TrailSettings,
11
+ ValueGeneratorFromJSON,
12
+ Vector3,
13
+ } from 'quarks.core';
14
+ import type {ParticleSystem} from '../ParticleSystem';
15
+ import {
16
+ trailCurveTension,
17
+ trailLifetime,
18
+ trailMinVertexDistance,
19
+ trailTessellation,
20
+ trailTextureMode,
21
+ trailTextureScale,
22
+ } from '../trailSettings';
23
+ import {
24
+ EROSION_DEFAULT_SOFTNESS,
25
+ RenderMode,
26
+ textureUVToJSON,
27
+ UV_DISTORTION_DEFAULT_STRENGTH,
28
+ UV_DISTORTION_DEFAULT_UV,
29
+ } from '../VFXBatch';
30
+ import {resolveGeometryData} from './geometry';
31
+ import {settingsFromJSON, settingsToJSON} from './settingsSchema';
32
+ import type {BabylonMetaData, ParticleSystemJSONParameters, ParticleSystemParameters} from './types';
33
+
34
+ // Reading and writing a ParticleSystem's JSON. The class's toJSON and fromJSON call these.
35
+
36
+ /** Per document being written: the last id handed out, and the geometries and textures written. */
37
+ const serializationCounters = new WeakMap<object, number>();
38
+ const serializedGeometries = new WeakMap<object, Array<{arrays: unknown[]; uuid: string}>>();
39
+ const serializedTextures = new WeakMap<object, Map<BaseTexture, string>>();
40
+
41
+ /**
42
+ * The next id for an entry of `meta`. Counted per document, so serialising the same effect
43
+ * twice writes the same ids.
44
+ */
45
+ function nextSerializationId(meta: BabylonMetaData, prefix: string): string {
46
+ const id = (serializationCounters.get(meta) ?? 0) + 1;
47
+ serializationCounters.set(meta, id);
48
+ return `${prefix}_${id}`;
49
+ }
50
+
51
+ /** 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> = {};
54
+ if (settings?.uvFromHead) flags.uvFromHead = true;
55
+ if (settings?.colorFromParticle) flags.colorFromParticle = true;
56
+ if (settings?.dieWithParticle) flags.dieWithParticle = true;
57
+ if (settings?.recordWorldSpace) flags.recordWorldSpace = true;
58
+ return flags;
59
+ }
60
+
61
+ /** 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> = {};
64
+ const tessellation = trailTessellation(settings);
65
+ const curveTension = trailCurveTension(settings);
66
+ if (tessellation > 0) curve.tessellation = tessellation;
67
+ if (curveTension > 0) curve.curveTension = curveTension;
68
+ return curve;
69
+ }
70
+
71
+ /**
72
+ * A trail's texture mode and scale, minimum vertex distance and lifetime, normalised, each only
73
+ * when it differs from the default.
74
+ */
75
+ export function trailSampling(settings: TrailSettings | undefined): Partial<TrailSettings> {
76
+ const out: Partial<TrailSettings> = {};
77
+ const textureMode = trailTextureMode(settings);
78
+ const textureScale = trailTextureScale(settings);
79
+ const minVertexDistance = trailMinVertexDistance(settings);
80
+ const lifetime = trailLifetime(settings);
81
+ if (textureMode !== 'stretch') out.textureMode = textureMode;
82
+ if (textureScale !== 1) out.textureScale = textureScale;
83
+ if (minVertexDistance > 0) out.minVertexDistance = minVertexDistance;
84
+ if (lifetime !== Infinity) out.lifetime = lifetime;
85
+ return out;
86
+ }
87
+
88
+ /** A trail's `followLocalOrigin`, only when on: off is the default. */
89
+ export function trailFollowLocalOrigin(settings: TrailSettings | undefined): Partial<TrailSettings> {
90
+ return settings?.followLocalOrigin ? {followLocalOrigin: true} : {};
91
+ }
92
+
93
+ /** A trail's side axis as JSON `[x, y, z]`, or nothing when unset. */
94
+ function trailSideAxisJSON(settings: TrailSettings | undefined): {sideAxis?: number[]} {
95
+ const axis = settings?.sideAxis;
96
+ return axis ? {sideAxis: [axis.x, axis.y, axis.z]} : {};
97
+ }
98
+
99
+ /**
100
+ * A trail's side axis from JSON `[x, y, z]` (or an object with x, y, z), or nothing: an axis
101
+ * that is not three finite numbers, or has no length, cannot sweep a ribbon and is dropped with
102
+ * a warning, so the trail faces the camera instead of vanishing.
103
+ */
104
+ function trailSideAxisFromJSON(value: unknown): {sideAxis?: Vector3} {
105
+ if (value === undefined || value === null) {
106
+ return {};
107
+ }
108
+ let x: unknown;
109
+ let y: unknown;
110
+ let z: unknown;
111
+ if (Array.isArray(value)) {
112
+ [x, y, z] = value;
113
+ } else if (typeof value === 'object') {
114
+ ({x, y, z} = value as {x?: unknown; y?: unknown; z?: unknown});
115
+ }
116
+ // A number, or a numeric string, as an older file could hold.
117
+ const component = (v: unknown): number =>
118
+ typeof v === 'number' ? v : typeof v === 'string' && v.trim() !== '' ? Number(v) : NaN;
119
+ const [nx, ny, nz] = [component(x), component(y), component(z)];
120
+ if (Number.isFinite(nx) && Number.isFinite(ny) && Number.isFinite(nz) && nx * nx + ny * ny + nz * nz > 0) {
121
+ return {sideAxis: new Vector3(nx, ny, nz)};
122
+ }
123
+ console.warn(`[quarks] sideAxis ${JSON.stringify(value)} is not a direction and is ignored`);
124
+ return {};
125
+ }
126
+
127
+ /** {@link ParticleSystem.toJSON}: the system's settings, its geometry, material and textures written into the document's meta. */
128
+ export function particleSystemToJSON(
129
+ system: ParticleSystem,
130
+ metaData?: BabylonMetaData | string
131
+ ): ParticleSystemJSONParameters {
132
+ const isRootObject = metaData === undefined || typeof metaData === 'string';
133
+ const meta: BabylonMetaData = isRootObject
134
+ ? {
135
+ geometries: {},
136
+ materials: {},
137
+ textures: {},
138
+ images: {},
139
+ shapes: {},
140
+ skeletons: {},
141
+ animations: {},
142
+ nodes: {},
143
+ }
144
+ : metaData;
145
+
146
+ const geometryUUID = geometryMeta(system, meta);
147
+ const materialUUID = materialMeta(system, meta);
148
+ const rendererSettingsJSON = rendererEmitterSettingsJSON(system);
149
+
150
+ // Only what differs from the defaults the constructor and fromJSON give a setting left out.
151
+ // 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.
153
+ return {
154
+ version: '3.0',
155
+ shape: system.emitterShape.toJSON(),
156
+ ...settingsToJSON(system),
157
+ instancingGeometry: geometryUUID,
158
+ ...(Object.keys(rendererSettingsJSON).length > 0 ? {rendererEmitterSettings: rendererSettingsJSON} : {}),
159
+ material: materialUUID,
160
+ behaviors: system.behaviors.map((b) => b.toJSON()),
161
+ };
162
+ }
163
+
164
+ /**
165
+ * The constructor parameters {@link ParticleSystem.fromJSON} builds a system from: a setting the file
166
+ * leaves out takes the constructor's default, and geometry, material and texture uuids are resolved
167
+ * through `meta`. Behaviors are read by `fromJSON` itself, since they need the system.
168
+ */
169
+ export function particleSystemParametersFromJSON(
170
+ json: ParticleSystemJSONParameters,
171
+ meta: BabylonMetaData,
172
+ scene?: Scene
173
+ ): ParticleSystemParameters {
174
+ // 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;
176
+ let rendererEmitterSettings: RendererEmitterSettings;
177
+ if (json.renderMode === RenderMode.Trail) {
178
+ const trailSettings = json.rendererEmitterSettings as TrailSettings;
179
+ rendererEmitterSettings = {
180
+ startLength:
181
+ trailSettings?.startLength !== undefined
182
+ ? ValueGeneratorFromJSON(trailSettings.startLength)
183
+ : new ConstantValue(30),
184
+ followLocalOrigin: Boolean(trailSettings?.followLocalOrigin),
185
+ ...trailFlags(trailSettings),
186
+ ...trailCurve(trailSettings),
187
+ ...trailSampling(trailSettings),
188
+ ...trailSideAxisFromJSON((trailSettings as {sideAxis?: unknown} | undefined)?.sideAxis),
189
+ };
190
+ } else if (json.renderMode === RenderMode.StretchedBillBoard) {
191
+ rendererEmitterSettings = {...(json.rendererEmitterSettings ?? {})};
192
+ if (json.speedFactor !== undefined) {
193
+ (rendererEmitterSettings as StretchedBillBoardSettings).speedFactor = json.speedFactor;
194
+ }
195
+ } else {
196
+ rendererEmitterSettings = {};
197
+ }
198
+
199
+ 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);
208
+ const erosionMeta = materialMeta?.erosion;
209
+ const gradientMeta = materialMeta?.gradientMap;
210
+ const distortionMeta = materialMeta?.uvDistortion;
211
+ const resolvedGeometryEntry =
212
+ typeof json.instancingGeometry === 'string'
213
+ ? meta.geometries?.[json.instancingGeometry]
214
+ : json.instancingGeometry;
215
+ const resolvedGeometry = resolveGeometryData(resolvedGeometryEntry);
216
+
217
+ const settings = settingsFromJSON(json);
218
+ return {
219
+ ...settings,
220
+ scene,
221
+ shape,
222
+ instancingGeometry: resolvedGeometry.positions,
223
+ instancingIndices: resolvedGeometry.indices,
224
+ instancingUVs: resolvedGeometry.uvs,
225
+ instancingUVs2: resolvedGeometry.uvs2,
226
+ instancingNormals: resolvedGeometry.normals,
227
+ rendererEmitterSettings,
228
+ texture,
229
+ material: materialMeta?.sourceMaterial ?? materialMeta,
230
+ // The material entry wins over the system's own fields, which older files wrote too.
231
+ blendMode: materialMeta?.alphaMode ?? settings.blendMode ?? Constants.ALPHA_ADD,
232
+ transparent: materialMeta?.transparent ?? settings.transparent ?? true,
233
+ depthTest: materialMeta?.depthTest ?? settings.depthTest ?? true,
234
+ depthWrite: materialMeta?.depthWrite ?? settings.depthWrite ?? false,
235
+ alphaTest: materialMeta?.alphaTest ?? settings.alphaTest ?? 0,
236
+ behaviors: [],
237
+ lighting: materialMeta?.lighting,
238
+ rim: materialMeta?.rim,
239
+ mapUV: materialMeta?.mapUV,
240
+ gradientMap:
241
+ gradientMeta && typeof gradientMeta === 'object'
242
+ ? {...gradientMeta, ramp: resolveTexture(gradientMeta.ramp)}
243
+ : undefined,
244
+ erosion:
245
+ erosionMeta && typeof erosionMeta === 'object'
246
+ ? {...erosionMeta, mask: resolveTexture(erosionMeta.mask)}
247
+ : undefined,
248
+ uvDistortion:
249
+ distortionMeta && typeof distortionMeta === 'object'
250
+ ? {...distortionMeta, map: resolveTexture(distortionMeta.map)}
251
+ : undefined,
252
+ };
253
+ }
254
+
255
+ /** The render mode's own settings as JSON: a trail's or a stretched billboard's, else none. */
256
+ function rendererEmitterSettingsJSON(system: ParticleSystem): RendererEmitterSettings {
257
+ if (system.renderMode === RenderMode.Trail) {
258
+ const settings = system.rendererEmitterSettings as TrailSettings;
259
+ // `startLength` always, for loaders that read it without a default; the rest only when on.
260
+ return {
261
+ startLength: settings.startLength.toJSON(),
262
+ ...trailFollowLocalOrigin(settings),
263
+ ...trailFlags(settings),
264
+ ...trailCurve(settings),
265
+ ...trailSampling(settings),
266
+ ...(trailSideAxisJSON(settings) as {sideAxis?: any}),
267
+ } as TrailSettings;
268
+ }
269
+ if (system.renderMode === RenderMode.StretchedBillBoard) {
270
+ return {
271
+ speedFactor: (system.rendererEmitterSettings as StretchedBillBoardSettings).speedFactor,
272
+ lengthFactor: (system.rendererEmitterSettings as StretchedBillBoardSettings).lengthFactor,
273
+ };
274
+ }
275
+ return {};
276
+ }
277
+
278
+ /** Writes the system's geometry into `meta` once per document, and returns its uuid. */
279
+ function geometryMeta(system: ParticleSystem, meta: BabylonMetaData): string {
280
+ // Systems that share geometry arrays (the default quad, a loaded mesh) share one entry.
281
+ const rs = system.rendererSettings;
282
+ const arrays = [
283
+ rs.instancingGeometry,
284
+ rs.instancingIndices,
285
+ rs.instancingUVs,
286
+ rs.instancingUVs2,
287
+ rs.instancingNormals,
288
+ ];
289
+ let written = serializedGeometries.get(meta);
290
+ if (written === undefined) {
291
+ written = [];
292
+ serializedGeometries.set(meta, written);
293
+ }
294
+ const same = written.find((entry) => entry.arrays.every((array, i) => array === arrays[i]));
295
+ if (same !== undefined) {
296
+ return same.uuid;
297
+ }
298
+ const geometryUUID = nextSerializationId(meta, 'quarks_geometry');
299
+ written.push({arrays, uuid: geometryUUID});
300
+ meta.geometries[geometryUUID] = {
301
+ uuid: geometryUUID,
302
+ type: 'QuarksGeometry',
303
+ positions: Array.from(system.rendererSettings.instancingGeometry),
304
+ indices: Array.from(system.rendererSettings.instancingIndices),
305
+ uvs: system.rendererSettings.instancingUVs ? Array.from(system.rendererSettings.instancingUVs) : undefined,
306
+ ...(system.rendererSettings.instancingUVs2 ? {uvs2: Array.from(system.rendererSettings.instancingUVs2)} : {}),
307
+ normals: system.rendererSettings.instancingNormals
308
+ ? Array.from(system.rendererSettings.instancingNormals)
309
+ : undefined,
310
+ };
311
+ return geometryUUID;
312
+ }
313
+
314
+ /** Writes the system's material, and each texture it uses once per document, into `meta`; returns the material's uuid. */
315
+ function materialMeta(system: ParticleSystem, meta: BabylonMetaData): string {
316
+ // One entry per texture in a document, however many systems and slots use it.
317
+ let written = serializedTextures.get(meta);
318
+ if (written === undefined) {
319
+ written = new Map();
320
+ serializedTextures.set(meta, written);
321
+ }
322
+ const textures = written;
323
+ const textureRef = (
324
+ value: BaseTexture | null | undefined,
325
+ anyKind = false,
326
+ slot = 'texture'
327
+ ): string | undefined => {
328
+ if (!(value instanceof Texture) && !(anyKind && value)) {
329
+ if (value) {
330
+ // A RawTexture or a render target has no url a file can point at, so the slot is left
331
+ // out; silently, the effect would just lose it on reload.
332
+ console.warn(`[quarks] ${slot} is not a url-backed Texture and is not written to the file`);
333
+ }
334
+ return undefined;
335
+ }
336
+ let uuid = textures.get(value);
337
+ if (uuid === undefined) {
338
+ uuid = nextSerializationId(meta, 'quarks_texture');
339
+ textures.set(value, uuid);
340
+ meta.textures[uuid] = value as Texture;
341
+ }
342
+ return uuid;
343
+ };
344
+ const textureUUID = textureRef(system.rendererSettings.texture, true);
345
+ const reflectionAtlasUUID = textureRef(system.rendererSettings.reflectionAtlas);
346
+ const erosion = system.erosion;
347
+ const distortion = system.uvDistortion;
348
+ const gradientMap = system.gradientMap;
349
+ const optional = <T>(key: string, value: T | undefined) => (value !== undefined ? {[key]: value} : {});
350
+
351
+ const materialUUID = nextSerializationId(meta, 'quarks_material');
352
+ meta.materials[materialUUID] = {
353
+ uuid: materialUUID,
354
+ type: 'QuarksMaterial',
355
+ transparent: system.rendererSettings.materialTransparent,
356
+ alphaMode: system.rendererSettings.materialBlendMode,
357
+ depthTest: system.rendererSettings.materialDepthTest,
358
+ depthWrite: system.rendererSettings.materialDepthWrite,
359
+ alphaTest: system.rendererSettings.materialAlphaTest,
360
+ texture: textureUUID,
361
+ reflectionAtlas: reflectionAtlasUUID,
362
+ reflectionLevel: system.rendererSettings.reflectionLevel,
363
+ // Written only when not the default, so existing effects serialise as before.
364
+ ...(system.lighting !== 'fixed' ? {lighting: system.lighting} : {}),
365
+ ...(system.rim ? {rim: {...system.rim, color: [...system.rim.color]}} : {}),
366
+ ...optional('mapUV', system.mapUV ? textureUVToJSON(system.mapUV) : undefined),
367
+ ...(gradientMap
368
+ ? {
369
+ gradientMap: {
370
+ ramp: textureRef(gradientMap.ramp, false, 'gradientMap.ramp'),
371
+ ...(gradientMap.channel !== 'r' ? {channel: gradientMap.channel} : {}),
372
+ },
373
+ }
374
+ : {}),
375
+ ...(erosion
376
+ ? {
377
+ erosion: {
378
+ ...optional('mask', textureRef(erosion.mask, false, 'erosion.mask')),
379
+ ...optional('maskUV', textureUVToJSON(erosion.maskUV)),
380
+ // Written only when not the default, like every other setting.
381
+ ...(erosion.softness !== EROSION_DEFAULT_SOFTNESS ? {softness: erosion.softness} : {}),
382
+ ...(erosion.edgeWidth !== 0 ? {edgeWidth: erosion.edgeWidth} : {}),
383
+ ...(erosion.edgeColor.some((c) => c !== 1) ? {edgeColor: [...erosion.edgeColor]} : {}),
384
+ },
385
+ }
386
+ : {}),
387
+ ...(distortion
388
+ ? {
389
+ uvDistortion: {
390
+ map: textureRef(distortion.map, false, 'uvDistortion.map'),
391
+ ...optional('mapUV', textureUVToJSON(distortion.mapUV, UV_DISTORTION_DEFAULT_UV)),
392
+ ...(distortion.strength !== UV_DISTORTION_DEFAULT_STRENGTH
393
+ ? {strength: distortion.strength}
394
+ : {}),
395
+ },
396
+ }
397
+ : {}),
398
+ sourceMaterial: system.material ?? undefined,
399
+ };
400
+ return materialUUID;
401
+ }