babylon.quarks 0.26.0 → 0.28.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.
- package/CHANGELOG.md +1194 -8
- package/README.md +13 -9
- package/dist/babylon.quarks.cjs +1 -1
- package/dist/babylon.quarks.cjs.map +1 -1
- package/dist/babylon.quarks.esm.js +1 -1
- package/dist/babylon.quarks.esm.js.map +1 -1
- package/dist/babylon.quarks.umd.min.js +1 -1
- package/dist/babylon.quarks.umd.min.js.map +1 -1
- package/dist/types/index.d.cts +10108 -0
- package/dist/types/index.d.ts +5794 -998
- package/package.json +18 -7
- package/src/BatchedRenderer.ts +274 -156
- package/src/EffectPool.ts +94 -12
- package/src/MeshSurfaceEmitter.ts +50 -14
- package/src/ParticleEmitter.ts +75 -17
- package/src/ParticleSystem.ts +981 -2066
- package/src/QuarksLoader.ts +666 -194
- package/src/QuarksPrefab.ts +367 -73
- package/src/QuarksSerializer.ts +512 -0
- package/src/QuarksUtil.ts +77 -7
- package/src/SpriteBatch.ts +127 -252
- package/src/TrailBatch.ts +545 -348
- package/src/VFXBatch.ts +238 -596
- package/src/batchKey.ts +239 -0
- package/src/batchSettings.ts +546 -0
- package/src/effectDocumentJSON.ts +261 -0
- package/src/effectResources.ts +76 -0
- package/src/envAtlas.ts +126 -18
- package/src/geometryUtil.ts +1 -1
- package/src/index.ts +69 -35
- package/src/nodeId.ts +36 -0
- package/src/shaderProgram.ts +419 -0
- package/src/shaderVariants.ts +245 -0
- package/src/shaders/chunks/blend.ts +31 -0
- package/src/shaders/chunks/fog.ts +12 -5
- package/src/shaders/chunks/sheet.ts +111 -0
- package/src/shaders/chunks/soft.ts +85 -0
- package/src/shaders/chunks/surface.ts +12 -2
- package/src/shaders/local_particle_physics_vert.glsl.ts +14 -28
- package/src/shaders/local_particle_physics_vert.wgsl.ts +16 -29
- package/src/shaders/particle_frag.glsl.ts +5 -22
- package/src/shaders/particle_frag.wgsl.ts +5 -23
- package/src/shaders/particle_physics_frag.glsl.ts +16 -26
- package/src/shaders/particle_physics_frag.wgsl.ts +14 -26
- package/src/shaders/particle_vert.glsl.ts +5 -27
- package/src/shaders/particle_vert.wgsl.ts +5 -26
- package/src/shaders/stretched_bb_particle_vert.glsl.ts +7 -33
- package/src/shaders/stretched_bb_particle_vert.wgsl.ts +7 -33
- package/src/shaders/trail_frag.glsl.ts +13 -0
- package/src/shaders/trail_frag.wgsl.ts +13 -0
- package/src/shaders/trail_vert.glsl.ts +45 -36
- package/src/shaders/trail_vert.wgsl.ts +46 -40
- package/src/softDepth.ts +136 -0
- package/src/system/BehaviorPlan.ts +85 -0
- package/src/system/BoundsTracker.ts +181 -0
- package/src/system/EmissionController.ts +358 -0
- package/src/system/EventDispatcher.ts +60 -0
- package/src/system/ParticleSpawner.ts +274 -0
- package/src/system/SimulationClock.ts +130 -0
- package/src/system/geometry.ts +104 -0
- package/src/system/integrate.ts +428 -0
- package/src/system/materialResolve.ts +165 -0
- package/src/system/normalize.ts +100 -0
- package/src/system/serialization.ts +422 -0
- package/src/system/settingsSchema.ts +339 -0
- package/src/system/types.ts +449 -0
- package/src/textureSource.ts +30 -0
- package/src/trailSettings.ts +54 -0
- package/src/BatchedParticleRenderer.ts +0 -3
- package/src/materials/ParticleMaterials.ts +0 -23
- package/src/materials/index.ts +0 -1
- package/src/shaders/chunks/index.ts +0 -1
- package/src/shaders/chunks/register-shader-chunks.ts +0 -32
- package/src/shaders/chunks/soft_fragment.glsl.ts +0 -13
- package/src/shaders/chunks/soft_pars_fragment.glsl.ts +0 -20
- package/src/shaders/chunks/soft_pars_vertex.glsl.ts +0 -6
- package/src/shaders/chunks/soft_vertex.glsl.ts +0 -6
- package/src/shaders/chunks/tile_fragment.glsl.ts +0 -9
- package/src/shaders/chunks/tile_pars_fragment.glsl.ts +0 -9
- package/src/shaders/chunks/tile_pars_vertex.glsl.ts +0 -28
- package/src/shaders/chunks/tile_vertex.glsl.ts +0 -18
- package/src/shaders/index.ts +0 -9
- package/src/shaders/local_particle_vert.glsl.ts +0 -50
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import type {BaseTexture} from '@babylonjs/core/Materials/Textures/baseTexture';
|
|
2
|
+
import {Texture} from '@babylonjs/core/Materials/Textures/texture';
|
|
3
|
+
import type {VFXBatchSettings} from '../BatchedRenderer';
|
|
4
|
+
|
|
5
|
+
/** Settings given next to a material, which win over what is read from it. */
|
|
6
|
+
export interface MaterialOverrides {
|
|
7
|
+
blendMode?: number;
|
|
8
|
+
transparent?: boolean;
|
|
9
|
+
depthTest?: boolean;
|
|
10
|
+
depthWrite?: boolean;
|
|
11
|
+
alphaTest?: number;
|
|
12
|
+
texture?: Texture | null;
|
|
13
|
+
reflectionAtlas?: BaseTexture | null;
|
|
14
|
+
layerMask?: number;
|
|
15
|
+
}
|
|
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
|
+
|
|
63
|
+
/** The draw settings a {@link ParticleSystem} takes from a material, before the env atlas is built. */
|
|
64
|
+
export interface ResolvedMaterialSettings {
|
|
65
|
+
texture: Texture | null;
|
|
66
|
+
reflectionTexture: BaseTexture | null;
|
|
67
|
+
reflectionLevel: number;
|
|
68
|
+
/** A live atlas the material, the overrides or the current settings already have; null if none. */
|
|
69
|
+
reflectionAtlas: BaseTexture | null;
|
|
70
|
+
blendMode: number;
|
|
71
|
+
transparent: boolean;
|
|
72
|
+
depthTest: boolean;
|
|
73
|
+
depthWrite: boolean;
|
|
74
|
+
alphaTest: number;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** `atlas` unless it has been disposed (it no longer has a scene). */
|
|
78
|
+
export function liveAtlas<T extends BaseTexture>(atlas: T | null | undefined): T | null {
|
|
79
|
+
return atlas?.getScene() ? atlas : null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Reads the draw settings from a Babylon material, or from the material record of a loaded file:
|
|
84
|
+
* its texture, reflection, alpha mode, depth flags and alpha test. `overrides` win; a setting the
|
|
85
|
+
* material does not have keeps its `current` value.
|
|
86
|
+
*/
|
|
87
|
+
export function resolveMaterialSettings(
|
|
88
|
+
material: ParticleMaterialSource | null | undefined,
|
|
89
|
+
overrides: MaterialOverrides,
|
|
90
|
+
current: VFXBatchSettings
|
|
91
|
+
): ResolvedMaterialSettings {
|
|
92
|
+
// The particle texture is a `Texture`: the first slot that holds one, so a cube in a slot is passed over.
|
|
93
|
+
const texture =
|
|
94
|
+
overrides.texture !== undefined
|
|
95
|
+
? overrides.texture
|
|
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) ??
|
|
103
|
+
current.texture ??
|
|
104
|
+
null);
|
|
105
|
+
const reflectionTexture =
|
|
106
|
+
material?.reflectionTexture ?? material?.environmentTexture ?? current.reflectionTexture ?? null;
|
|
107
|
+
const reflectionTextureLevel = typeof reflectionTexture?.level === 'number' ? reflectionTexture.level : 1;
|
|
108
|
+
const blendMode =
|
|
109
|
+
overrides.blendMode ??
|
|
110
|
+
(typeof material?.alphaMode === 'number' ? material.alphaMode : current.materialBlendMode);
|
|
111
|
+
const transparent =
|
|
112
|
+
overrides.transparent ??
|
|
113
|
+
(typeof material?.needAlphaBlending === 'function'
|
|
114
|
+
? material.needAlphaBlending()
|
|
115
|
+
: typeof material?.alpha === 'number'
|
|
116
|
+
? material.alpha < 1
|
|
117
|
+
: current.materialTransparent);
|
|
118
|
+
const depthTest =
|
|
119
|
+
overrides.depthTest ??
|
|
120
|
+
(typeof material?.disableDepthTest === 'boolean' ? !material.disableDepthTest : current.materialDepthTest);
|
|
121
|
+
const depthWrite =
|
|
122
|
+
overrides.depthWrite ??
|
|
123
|
+
(typeof material?.disableDepthWrite === 'boolean'
|
|
124
|
+
? !material.disableDepthWrite
|
|
125
|
+
: typeof material?.forceDepthWrite === 'boolean'
|
|
126
|
+
? material.forceDepthWrite
|
|
127
|
+
: current.materialDepthWrite);
|
|
128
|
+
const alphaTest =
|
|
129
|
+
overrides.alphaTest ??
|
|
130
|
+
(() => {
|
|
131
|
+
// StandardMaterial always has alphaCutOff (default 0.4). Only treat it as
|
|
132
|
+
// particle alpha-test when the material is actually in an alpha-test mode —
|
|
133
|
+
// otherwise Mesh Material demos wrongly enable USE_ALPHATEST.
|
|
134
|
+
const mode = material?.transparencyMode;
|
|
135
|
+
const alphaTestMode = mode === 1 || mode === 3;
|
|
136
|
+
if (alphaTestMode && typeof material?.alphaCutOff === 'number') {
|
|
137
|
+
return material.alphaCutOff;
|
|
138
|
+
}
|
|
139
|
+
if (alphaTestMode && typeof material?.alphaCutOffValue === 'number') {
|
|
140
|
+
return material.alphaCutOffValue;
|
|
141
|
+
}
|
|
142
|
+
if (material) {
|
|
143
|
+
return 0;
|
|
144
|
+
}
|
|
145
|
+
return current.materialAlphaTest;
|
|
146
|
+
})();
|
|
147
|
+
return {
|
|
148
|
+
texture,
|
|
149
|
+
reflectionTexture,
|
|
150
|
+
reflectionLevel:
|
|
151
|
+
typeof material?.reflectionLevel === 'number' ? material.reflectionLevel : reflectionTextureLevel,
|
|
152
|
+
// A disposed atlas (its cube was disposed, or disposeEnvAtlas ran) is skipped, so a new one is built.
|
|
153
|
+
reflectionAtlas:
|
|
154
|
+
liveAtlas(material?.reflectionAtlas) ??
|
|
155
|
+
liveAtlas(overrides.reflectionAtlas) ??
|
|
156
|
+
liveAtlas(current.reflectionAtlas),
|
|
157
|
+
// Do not expand cube files into six face textures anymore — that path
|
|
158
|
+
// still trips GL_INVALID_OPERATION on iOS. Prefer a single atlas.
|
|
159
|
+
blendMode,
|
|
160
|
+
transparent,
|
|
161
|
+
depthTest,
|
|
162
|
+
depthWrite,
|
|
163
|
+
alphaTest,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import {ConstantValue, FunctionValueGenerator, ValueGenerator, Vector2, Vector3} from 'quarks.core';
|
|
2
|
+
import type {MeshLighting, RenderFace, SortMode} from '../batchSettings';
|
|
3
|
+
import type {BurstParameters, CullingMode} from './types';
|
|
4
|
+
|
|
5
|
+
// Normalisers shared by the ParticleSystem constructor, its setters and its JSON reader, so a
|
|
6
|
+
// value read from a file and one set in code end up the same.
|
|
7
|
+
|
|
8
|
+
/** `lighting` if it is one of the modes, else the default. */
|
|
9
|
+
export function validLighting(lighting: unknown): MeshLighting {
|
|
10
|
+
return lighting === 'unlit' || lighting === 'scene' ? lighting : 'fixed';
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** `mode` if it is one of the sort modes, else `'none'`. */
|
|
14
|
+
export function validSortMode(mode: unknown): SortMode {
|
|
15
|
+
return mode === 'distance' || mode === 'oldestInFront' || mode === 'youngestInFront' ? mode : 'none';
|
|
16
|
+
}
|
|
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
|
+
|
|
23
|
+
/** `mode` if it is one of the culling modes, else `'always'`. */
|
|
24
|
+
export function validCullingMode(mode: unknown): CullingMode {
|
|
25
|
+
return mode === 'pause' || mode === 'pauseAndCatchUp' ? mode : 'always';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** `count` as a whole number of particles, or undefined (no limit) when it is unset, NaN or negative. */
|
|
29
|
+
export function validMaxParticles(count: unknown): number | undefined {
|
|
30
|
+
return typeof count === 'number' && count >= 0 && count < Infinity ? Math.floor(count) : undefined;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** A copy of `v`, or undefined when it is unset or zero, which is the same as no pivot. */
|
|
34
|
+
export function nonZeroPivot(v: Vector3 | undefined): Vector3 | undefined {
|
|
35
|
+
return v && (v.x !== 0 || v.y !== 0 || v.z !== 0) ? v.clone() : undefined;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** A copy of `v`, or undefined when neither axis flips. */
|
|
39
|
+
export function nonZeroFlip(v: Vector2 | undefined): Vector2 | undefined {
|
|
40
|
+
return v && (v.x > 0 || v.y > 0) ? v.clone() : undefined;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** `cutoff` kept to 0…1, or 0 (no alpha test) when it is unset, NaN or negative. */
|
|
44
|
+
export function validAlphaTest(cutoff: unknown): number {
|
|
45
|
+
return typeof cutoff === 'number' && cutoff > 0 ? Math.min(cutoff, 1) : 0;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** A flipbook's columns or rows: a whole number of cells, at least 1; anything else is 1. */
|
|
49
|
+
export function validTileCount(count: unknown): number {
|
|
50
|
+
return typeof count === 'number' && Number.isFinite(count) && count >= 1 ? Math.floor(count) : 1;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** World units in front of the scene over which a soft particle is fully faded by default. */
|
|
54
|
+
export const DEFAULT_SOFT_FAR_FADE = 1;
|
|
55
|
+
|
|
56
|
+
/** A soft-particle fade distance: finite and at least 0, else `fallback`. */
|
|
57
|
+
export function validSoftFade(distance: unknown, fallback: number): number {
|
|
58
|
+
return typeof distance === 'number' && Number.isFinite(distance) && distance >= 0 ? distance : fallback;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Seconds between a burst's waves when a burst leaves `interval` out, in code and in a file. */
|
|
62
|
+
export const DEFAULT_BURST_INTERVAL = 0.1;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* A burst with every field present and in range: a `time` that is a finite number of seconds
|
|
66
|
+
* (else 0), a `count` generator (a number becomes a constant, nothing becomes 0), `cycle` at least
|
|
67
|
+
* 1, `interval` {@link DEFAULT_BURST_INTERVAL} when left out and never negative, `probability`
|
|
68
|
+
* within 0..1 (1 when left out). The same rules apply to a burst given to the constructor and one
|
|
69
|
+
* read from a file, so both fire alike.
|
|
70
|
+
*/
|
|
71
|
+
export function normalizeBurst(burst: Partial<BurstParameters> & {count?: unknown}): BurstParameters {
|
|
72
|
+
const count = burst.count;
|
|
73
|
+
const time = typeof burst.time === 'number' && Number.isFinite(burst.time) && burst.time > 0 ? burst.time : 0;
|
|
74
|
+
const cycle = typeof burst.cycle === 'number' && Number.isFinite(burst.cycle) && burst.cycle >= 1 ? burst.cycle : 1;
|
|
75
|
+
const interval =
|
|
76
|
+
typeof burst.interval === 'number' && Number.isFinite(burst.interval)
|
|
77
|
+
? Math.max(0, burst.interval)
|
|
78
|
+
: DEFAULT_BURST_INTERVAL;
|
|
79
|
+
const probability =
|
|
80
|
+
typeof burst.probability === 'number' && Number.isFinite(burst.probability)
|
|
81
|
+
? Math.min(1, Math.max(0, burst.probability))
|
|
82
|
+
: 1;
|
|
83
|
+
return {
|
|
84
|
+
time,
|
|
85
|
+
count:
|
|
86
|
+
typeof count === 'number'
|
|
87
|
+
? new ConstantValue(count)
|
|
88
|
+
: count !== undefined && count !== null
|
|
89
|
+
? (count as ValueGenerator | FunctionValueGenerator)
|
|
90
|
+
: new ConstantValue(0),
|
|
91
|
+
cycle,
|
|
92
|
+
interval,
|
|
93
|
+
probability,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** The bursts normalised (see {@link normalizeBurst}) and in time order, which emission relies on. */
|
|
98
|
+
export function normalizeBursts(bursts: ReadonlyArray<Partial<BurstParameters>> | undefined): BurstParameters[] {
|
|
99
|
+
return (bursts ?? []).map(normalizeBurst).sort((a, b) => a.time - b.time);
|
|
100
|
+
}
|
|
@@ -0,0 +1,422 @@
|
|
|
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
|
+
BehaviorToJSON,
|
|
7
|
+
ConstantValue,
|
|
8
|
+
EmitterFromJSON,
|
|
9
|
+
EmitterToJSON,
|
|
10
|
+
readFormatPart,
|
|
11
|
+
RendererEmitterSettings,
|
|
12
|
+
StretchedBillBoardSettings,
|
|
13
|
+
TrailSettings,
|
|
14
|
+
ValueGeneratorFromJSON,
|
|
15
|
+
Vector3,
|
|
16
|
+
Vector3ArrayJSON,
|
|
17
|
+
warnFormat,
|
|
18
|
+
} from 'quarks.core';
|
|
19
|
+
import type {MaterialTextureRef} from '../effectDocumentJSON';
|
|
20
|
+
import type {ParticleSystem} from '../ParticleSystem';
|
|
21
|
+
import {
|
|
22
|
+
trailCurveTension,
|
|
23
|
+
trailLifetime,
|
|
24
|
+
trailMinVertexDistance,
|
|
25
|
+
trailTessellation,
|
|
26
|
+
trailTextureMode,
|
|
27
|
+
trailTextureScale,
|
|
28
|
+
type TrailOptions,
|
|
29
|
+
} from '../trailSettings';
|
|
30
|
+
import {
|
|
31
|
+
EROSION_DEFAULT_SOFTNESS,
|
|
32
|
+
RenderMode,
|
|
33
|
+
textureUVToJSON,
|
|
34
|
+
UV_DISTORTION_DEFAULT_STRENGTH,
|
|
35
|
+
UV_DISTORTION_DEFAULT_UV,
|
|
36
|
+
} from '../VFXBatch';
|
|
37
|
+
import {resolveGeometryData} from './geometry';
|
|
38
|
+
import type {ParticleMaterialSource} from './materialResolve';
|
|
39
|
+
import {settingsFromJSON, settingsToJSON} from './settingsSchema';
|
|
40
|
+
import type {
|
|
41
|
+
BabylonMetaData,
|
|
42
|
+
ParticleSystemJSONParameters,
|
|
43
|
+
ParticleSystemParameters,
|
|
44
|
+
RendererEmitterSettingsJSON,
|
|
45
|
+
TrailSettingsJSON,
|
|
46
|
+
} from './types';
|
|
47
|
+
|
|
48
|
+
// Reading and writing a ParticleSystem's JSON. The class's toJSON and fromJSON call these.
|
|
49
|
+
|
|
50
|
+
/** Per document being written: the last id handed out, and the geometries and textures written. */
|
|
51
|
+
const serializationCounters = new WeakMap<object, number>();
|
|
52
|
+
const serializedGeometries = new WeakMap<object, Array<{arrays: unknown[]; uuid: string}>>();
|
|
53
|
+
const serializedTextures = new WeakMap<object, Map<BaseTexture, string>>();
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The next id for an entry of `meta`. Counted per document, so serialising the same effect
|
|
57
|
+
* twice writes the same ids.
|
|
58
|
+
*/
|
|
59
|
+
function nextSerializationId(meta: BabylonMetaData, prefix: string): string {
|
|
60
|
+
const id = (serializationCounters.get(meta) ?? 0) + 1;
|
|
61
|
+
serializationCounters.set(meta, id);
|
|
62
|
+
return `${prefix}_${id}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** The optional trail flags that are switched on, so JSON and clones only carry the ones in use. */
|
|
66
|
+
export function trailFlags(settings: Partial<TrailOptions> | undefined): Partial<TrailOptions> {
|
|
67
|
+
const flags: Partial<TrailOptions> = {};
|
|
68
|
+
if (settings?.uvFromHead) flags.uvFromHead = true;
|
|
69
|
+
if (settings?.colorFromParticle) flags.colorFromParticle = true;
|
|
70
|
+
if (settings?.dieWithParticle) flags.dieWithParticle = true;
|
|
71
|
+
if (settings?.recordWorldSpace) flags.recordWorldSpace = true;
|
|
72
|
+
return flags;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** A trail's tessellation and curve tension, normalised, each only when it differs from the default. */
|
|
76
|
+
export function trailCurve(settings: Partial<TrailOptions> | undefined): Partial<TrailOptions> {
|
|
77
|
+
const curve: Partial<TrailOptions> = {};
|
|
78
|
+
const tessellation = trailTessellation(settings);
|
|
79
|
+
const curveTension = trailCurveTension(settings);
|
|
80
|
+
if (tessellation > 0) curve.tessellation = tessellation;
|
|
81
|
+
if (curveTension > 0) curve.curveTension = curveTension;
|
|
82
|
+
return curve;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* A trail's texture mode and scale, minimum vertex distance and lifetime, normalised, each only
|
|
87
|
+
* when it differs from the default.
|
|
88
|
+
*/
|
|
89
|
+
export function trailSampling(settings: Partial<TrailOptions> | undefined): Partial<TrailOptions> {
|
|
90
|
+
const out: Partial<TrailOptions> = {};
|
|
91
|
+
const textureMode = trailTextureMode(settings);
|
|
92
|
+
const textureScale = trailTextureScale(settings);
|
|
93
|
+
const minVertexDistance = trailMinVertexDistance(settings);
|
|
94
|
+
const lifetime = trailLifetime(settings);
|
|
95
|
+
if (textureMode !== 'stretch') out.textureMode = textureMode;
|
|
96
|
+
if (textureScale !== 1) out.textureScale = textureScale;
|
|
97
|
+
if (minVertexDistance > 0) out.minVertexDistance = minVertexDistance;
|
|
98
|
+
if (lifetime !== Infinity) out.lifetime = lifetime;
|
|
99
|
+
return out;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** A trail's `followLocalOrigin`, only when on: off is the default. */
|
|
103
|
+
export function trailFollowLocalOrigin(settings: Partial<TrailOptions> | undefined): Partial<TrailOptions> {
|
|
104
|
+
return settings?.followLocalOrigin ? {followLocalOrigin: true} : {};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** A trail's side axis as JSON `[x, y, z]`, or nothing when unset. */
|
|
108
|
+
function trailSideAxisJSON(settings: TrailSettings | undefined): {sideAxis?: Vector3ArrayJSON} {
|
|
109
|
+
const axis = settings?.sideAxis;
|
|
110
|
+
return axis ? {sideAxis: [axis.x, axis.y, axis.z]} : {};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* A trail's side axis from JSON `[x, y, z]` (or an object with x, y, z), or nothing: an axis
|
|
115
|
+
* that is not three finite numbers, or has no length, cannot sweep a ribbon and is dropped with
|
|
116
|
+
* a warning, so the trail faces the camera instead of vanishing.
|
|
117
|
+
*/
|
|
118
|
+
function trailSideAxisFromJSON(value: unknown): {sideAxis?: Vector3} {
|
|
119
|
+
if (value === undefined || value === null) {
|
|
120
|
+
return {};
|
|
121
|
+
}
|
|
122
|
+
let x: unknown;
|
|
123
|
+
let y: unknown;
|
|
124
|
+
let z: unknown;
|
|
125
|
+
if (Array.isArray(value)) {
|
|
126
|
+
[x, y, z] = value;
|
|
127
|
+
} else if (typeof value === 'object') {
|
|
128
|
+
({x, y, z} = value as {x?: unknown; y?: unknown; z?: unknown});
|
|
129
|
+
}
|
|
130
|
+
// A number, or a numeric string, as an older file could hold.
|
|
131
|
+
const component = (v: unknown): number =>
|
|
132
|
+
typeof v === 'number' ? v : typeof v === 'string' && v.trim() !== '' ? Number(v) : NaN;
|
|
133
|
+
const [nx, ny, nz] = [component(x), component(y), component(z)];
|
|
134
|
+
if (Number.isFinite(nx) && Number.isFinite(ny) && Number.isFinite(nz) && nx * nx + ny * ny + nz * nz > 0) {
|
|
135
|
+
return {sideAxis: new Vector3(nx, ny, nz)};
|
|
136
|
+
}
|
|
137
|
+
warnFormat(`sideAxis ${JSON.stringify(value)} is not a direction and is ignored`);
|
|
138
|
+
return {};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** {@link ParticleSystem.toJSON}: the system's settings, its geometry, material and textures written into the document's meta. */
|
|
142
|
+
export function particleSystemToJSON(
|
|
143
|
+
system: ParticleSystem,
|
|
144
|
+
metaData?: BabylonMetaData | string
|
|
145
|
+
): ParticleSystemJSONParameters {
|
|
146
|
+
const isRootObject = metaData === undefined || typeof metaData === 'string';
|
|
147
|
+
const meta: BabylonMetaData = isRootObject
|
|
148
|
+
? {
|
|
149
|
+
geometries: {},
|
|
150
|
+
materials: {},
|
|
151
|
+
textures: {},
|
|
152
|
+
images: {},
|
|
153
|
+
shapes: {},
|
|
154
|
+
skeletons: {},
|
|
155
|
+
animations: {},
|
|
156
|
+
nodes: {},
|
|
157
|
+
}
|
|
158
|
+
: metaData;
|
|
159
|
+
|
|
160
|
+
const geometryUUID = geometryMeta(system, meta);
|
|
161
|
+
const materialUUID = materialMeta(system, meta);
|
|
162
|
+
const rendererSettingsJSON = rendererEmitterSettingsJSON(system);
|
|
163
|
+
|
|
164
|
+
// Only what differs from the defaults the constructor and fromJSON give a setting left out.
|
|
165
|
+
// The shape, the settings the table marks `always`, the geometry, the material and the
|
|
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).
|
|
169
|
+
return {
|
|
170
|
+
version: '3.0',
|
|
171
|
+
shape: EmitterToJSON(system.emitterShape),
|
|
172
|
+
...settingsToJSON(system),
|
|
173
|
+
instancingGeometry: geometryUUID,
|
|
174
|
+
...(rendererSettingsJSON ? {rendererEmitterSettings: rendererSettingsJSON} : {}),
|
|
175
|
+
material: materialUUID,
|
|
176
|
+
behaviors: system.behaviors.map((b) => BehaviorToJSON(b, system)),
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* The constructor parameters {@link ParticleSystem.fromJSON} builds a system from: a setting the file
|
|
182
|
+
* leaves out takes the constructor's default, and geometry, material and texture uuids are resolved
|
|
183
|
+
* through `meta`. Behaviors are read by `fromJSON` itself, since they need the system.
|
|
184
|
+
*/
|
|
185
|
+
export function particleSystemParametersFromJSON(
|
|
186
|
+
json: ParticleSystemJSONParameters,
|
|
187
|
+
meta: BabylonMetaData,
|
|
188
|
+
scene?: Scene
|
|
189
|
+
): ParticleSystemParameters {
|
|
190
|
+
// A part the file leaves out takes the constructor's default, as a system made in code does.
|
|
191
|
+
const shape = json.shape ? readFormatPart('shape', () => EmitterFromJSON(json.shape!, meta)) : undefined;
|
|
192
|
+
let rendererEmitterSettings: RendererEmitterSettings;
|
|
193
|
+
if (json.renderMode === RenderMode.Trail) {
|
|
194
|
+
const trailSettings = json.rendererEmitterSettings as TrailSettingsJSON | undefined;
|
|
195
|
+
rendererEmitterSettings = {
|
|
196
|
+
startLength:
|
|
197
|
+
trailSettings?.startLength !== undefined
|
|
198
|
+
? ValueGeneratorFromJSON(trailSettings.startLength)
|
|
199
|
+
: new ConstantValue(30),
|
|
200
|
+
followLocalOrigin: Boolean(trailSettings?.followLocalOrigin),
|
|
201
|
+
...trailFlags(trailSettings),
|
|
202
|
+
...trailCurve(trailSettings),
|
|
203
|
+
...trailSampling(trailSettings),
|
|
204
|
+
...trailSideAxisFromJSON(trailSettings?.sideAxis),
|
|
205
|
+
};
|
|
206
|
+
} else if (json.renderMode === RenderMode.StretchedBillBoard) {
|
|
207
|
+
// The render mode says which settings the file holds.
|
|
208
|
+
const stretched: Partial<StretchedBillBoardSettings> = {
|
|
209
|
+
...(json.rendererEmitterSettings as StretchedBillBoardSettings | undefined),
|
|
210
|
+
};
|
|
211
|
+
if (json.speedFactor !== undefined) {
|
|
212
|
+
stretched.speedFactor = json.speedFactor;
|
|
213
|
+
}
|
|
214
|
+
rendererEmitterSettings = stretched;
|
|
215
|
+
} else {
|
|
216
|
+
rendererEmitterSettings = {};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const materialMeta = json.material ? meta.materials?.[json.material] : undefined;
|
|
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);
|
|
229
|
+
const erosionMeta = materialMeta?.erosion;
|
|
230
|
+
const gradientMeta = materialMeta?.gradientMap;
|
|
231
|
+
const distortionMeta = materialMeta?.uvDistortion;
|
|
232
|
+
const resolvedGeometryEntry =
|
|
233
|
+
typeof json.instancingGeometry === 'string'
|
|
234
|
+
? meta.geometries?.[json.instancingGeometry]
|
|
235
|
+
: json.instancingGeometry;
|
|
236
|
+
const resolvedGeometry = resolveGeometryData(resolvedGeometryEntry);
|
|
237
|
+
|
|
238
|
+
const settings = settingsFromJSON(json);
|
|
239
|
+
return {
|
|
240
|
+
...settings,
|
|
241
|
+
scene,
|
|
242
|
+
shape,
|
|
243
|
+
instancingGeometry: resolvedGeometry.positions,
|
|
244
|
+
instancingIndices: resolvedGeometry.indices,
|
|
245
|
+
instancingUVs: resolvedGeometry.uvs,
|
|
246
|
+
instancingUVs2: resolvedGeometry.uvs2,
|
|
247
|
+
instancingNormals: resolvedGeometry.normals,
|
|
248
|
+
rendererEmitterSettings,
|
|
249
|
+
texture,
|
|
250
|
+
material,
|
|
251
|
+
// The material entry wins over the system's own fields, which older files wrote too.
|
|
252
|
+
blendMode: materialMeta?.alphaMode ?? settings.blendMode ?? Constants.ALPHA_ADD,
|
|
253
|
+
transparent: materialMeta?.transparent ?? settings.transparent ?? true,
|
|
254
|
+
depthTest: materialMeta?.depthTest ?? settings.depthTest ?? true,
|
|
255
|
+
depthWrite: materialMeta?.depthWrite ?? settings.depthWrite ?? false,
|
|
256
|
+
alphaTest: materialMeta?.alphaTest ?? settings.alphaTest ?? 0,
|
|
257
|
+
behaviors: [],
|
|
258
|
+
lighting: materialMeta?.lighting,
|
|
259
|
+
rim: materialMeta?.rim,
|
|
260
|
+
mapUV: materialMeta?.mapUV,
|
|
261
|
+
gradientMap:
|
|
262
|
+
gradientMeta && typeof gradientMeta === 'object'
|
|
263
|
+
? {...gradientMeta, ramp: resolveTexture(gradientMeta.ramp)}
|
|
264
|
+
: undefined,
|
|
265
|
+
erosion:
|
|
266
|
+
erosionMeta && typeof erosionMeta === 'object'
|
|
267
|
+
? {...erosionMeta, mask: resolveTexture(erosionMeta.mask)}
|
|
268
|
+
: undefined,
|
|
269
|
+
uvDistortion:
|
|
270
|
+
distortionMeta && typeof distortionMeta === 'object'
|
|
271
|
+
? {...distortionMeta, map: resolveTexture(distortionMeta.map)}
|
|
272
|
+
: undefined,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** The render mode's own settings as JSON: a trail's or a stretched billboard's, else none. */
|
|
277
|
+
function rendererEmitterSettingsJSON(system: ParticleSystem): RendererEmitterSettingsJSON | undefined {
|
|
278
|
+
if (system.renderMode === RenderMode.Trail) {
|
|
279
|
+
const settings = system.rendererEmitterSettings as TrailSettings;
|
|
280
|
+
// `startLength` always, for loaders that read it without a default; the rest only when on.
|
|
281
|
+
return {
|
|
282
|
+
startLength: settings.startLength.toJSON(),
|
|
283
|
+
...trailFollowLocalOrigin(settings),
|
|
284
|
+
...trailFlags(settings),
|
|
285
|
+
...trailCurve(settings),
|
|
286
|
+
...trailSampling(settings),
|
|
287
|
+
...trailSideAxisJSON(settings),
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
if (system.renderMode === RenderMode.StretchedBillBoard) {
|
|
291
|
+
return {
|
|
292
|
+
speedFactor: (system.rendererEmitterSettings as StretchedBillBoardSettings).speedFactor,
|
|
293
|
+
lengthFactor: (system.rendererEmitterSettings as StretchedBillBoardSettings).lengthFactor,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
return undefined;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** Writes the system's geometry into `meta` once per document, and returns its uuid. */
|
|
300
|
+
function geometryMeta(system: ParticleSystem, meta: BabylonMetaData): string {
|
|
301
|
+
// Systems that share geometry arrays (the default quad, a loaded mesh) share one entry.
|
|
302
|
+
const rs = system.rendererSettings;
|
|
303
|
+
const arrays = [
|
|
304
|
+
rs.instancingGeometry,
|
|
305
|
+
rs.instancingIndices,
|
|
306
|
+
rs.instancingUVs,
|
|
307
|
+
rs.instancingUVs2,
|
|
308
|
+
rs.instancingNormals,
|
|
309
|
+
];
|
|
310
|
+
let written = serializedGeometries.get(meta);
|
|
311
|
+
if (written === undefined) {
|
|
312
|
+
written = [];
|
|
313
|
+
serializedGeometries.set(meta, written);
|
|
314
|
+
}
|
|
315
|
+
const same = written.find((entry) => entry.arrays.every((array, i) => array === arrays[i]));
|
|
316
|
+
if (same !== undefined) {
|
|
317
|
+
return same.uuid;
|
|
318
|
+
}
|
|
319
|
+
const geometryUUID = nextSerializationId(meta, 'quarks_geometry');
|
|
320
|
+
written.push({arrays, uuid: geometryUUID});
|
|
321
|
+
meta.geometries[geometryUUID] = {
|
|
322
|
+
uuid: geometryUUID,
|
|
323
|
+
type: 'QuarksGeometry',
|
|
324
|
+
positions: Array.from(system.rendererSettings.instancingGeometry),
|
|
325
|
+
indices: Array.from(system.rendererSettings.instancingIndices),
|
|
326
|
+
uvs: system.rendererSettings.instancingUVs ? Array.from(system.rendererSettings.instancingUVs) : undefined,
|
|
327
|
+
...(system.rendererSettings.instancingUVs2 ? {uvs2: Array.from(system.rendererSettings.instancingUVs2)} : {}),
|
|
328
|
+
normals: system.rendererSettings.instancingNormals
|
|
329
|
+
? Array.from(system.rendererSettings.instancingNormals)
|
|
330
|
+
: undefined,
|
|
331
|
+
};
|
|
332
|
+
return geometryUUID;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/** Writes the system's material, and each texture it uses once per document, into `meta`; returns the material's uuid. */
|
|
336
|
+
function materialMeta(system: ParticleSystem, meta: BabylonMetaData): string {
|
|
337
|
+
// One entry per texture in a document, however many systems and slots use it.
|
|
338
|
+
let written = serializedTextures.get(meta);
|
|
339
|
+
if (written === undefined) {
|
|
340
|
+
written = new Map();
|
|
341
|
+
serializedTextures.set(meta, written);
|
|
342
|
+
}
|
|
343
|
+
const textures = written;
|
|
344
|
+
const textureRef = (
|
|
345
|
+
value: BaseTexture | null | undefined,
|
|
346
|
+
anyKind = false,
|
|
347
|
+
slot = 'texture'
|
|
348
|
+
): string | undefined => {
|
|
349
|
+
if (!(value instanceof Texture) && !(anyKind && value)) {
|
|
350
|
+
if (value) {
|
|
351
|
+
// A RawTexture or a render target has no url a file can point at, so the slot is left
|
|
352
|
+
// out; silently, the effect would just lose it on reload.
|
|
353
|
+
console.warn(`[quarks] ${slot} is not a url-backed Texture and is not written to the file`);
|
|
354
|
+
}
|
|
355
|
+
return undefined;
|
|
356
|
+
}
|
|
357
|
+
let uuid = textures.get(value);
|
|
358
|
+
if (uuid === undefined) {
|
|
359
|
+
uuid = nextSerializationId(meta, 'quarks_texture');
|
|
360
|
+
textures.set(value, uuid);
|
|
361
|
+
meta.textures[uuid] = value;
|
|
362
|
+
}
|
|
363
|
+
return uuid;
|
|
364
|
+
};
|
|
365
|
+
const textureUUID = textureRef(system.rendererSettings.texture, true);
|
|
366
|
+
const reflectionAtlasUUID = textureRef(system.rendererSettings.reflectionAtlas);
|
|
367
|
+
const erosion = system.erosion;
|
|
368
|
+
const distortion = system.uvDistortion;
|
|
369
|
+
const gradientMap = system.gradientMap;
|
|
370
|
+
const optional = <T>(key: string, value: T | undefined) => (value !== undefined ? {[key]: value} : {});
|
|
371
|
+
|
|
372
|
+
const materialUUID = nextSerializationId(meta, 'quarks_material');
|
|
373
|
+
meta.materials[materialUUID] = {
|
|
374
|
+
uuid: materialUUID,
|
|
375
|
+
type: 'QuarksMaterial',
|
|
376
|
+
transparent: system.rendererSettings.materialTransparent,
|
|
377
|
+
alphaMode: system.rendererSettings.materialBlendMode,
|
|
378
|
+
depthTest: system.rendererSettings.materialDepthTest,
|
|
379
|
+
depthWrite: system.rendererSettings.materialDepthWrite,
|
|
380
|
+
alphaTest: system.rendererSettings.materialAlphaTest,
|
|
381
|
+
texture: textureUUID,
|
|
382
|
+
reflectionAtlas: reflectionAtlasUUID,
|
|
383
|
+
reflectionLevel: system.rendererSettings.reflectionLevel,
|
|
384
|
+
// Written only when not the default, so existing effects serialise as before.
|
|
385
|
+
...(system.lighting !== 'fixed' ? {lighting: system.lighting} : {}),
|
|
386
|
+
...(system.rim ? {rim: {...system.rim, color: [...system.rim.color]}} : {}),
|
|
387
|
+
...optional('mapUV', system.mapUV ? textureUVToJSON(system.mapUV) : undefined),
|
|
388
|
+
...(gradientMap
|
|
389
|
+
? {
|
|
390
|
+
gradientMap: {
|
|
391
|
+
ramp: textureRef(gradientMap.ramp, false, 'gradientMap.ramp'),
|
|
392
|
+
...(gradientMap.channel !== 'r' ? {channel: gradientMap.channel} : {}),
|
|
393
|
+
},
|
|
394
|
+
}
|
|
395
|
+
: {}),
|
|
396
|
+
...(erosion
|
|
397
|
+
? {
|
|
398
|
+
erosion: {
|
|
399
|
+
...optional('mask', textureRef(erosion.mask, false, 'erosion.mask')),
|
|
400
|
+
...optional('maskUV', textureUVToJSON(erosion.maskUV)),
|
|
401
|
+
// Written only when not the default, like every other setting.
|
|
402
|
+
...(erosion.softness !== EROSION_DEFAULT_SOFTNESS ? {softness: erosion.softness} : {}),
|
|
403
|
+
...(erosion.edgeWidth !== 0 ? {edgeWidth: erosion.edgeWidth} : {}),
|
|
404
|
+
...(erosion.edgeColor.some((c) => c !== 1) ? {edgeColor: [...erosion.edgeColor]} : {}),
|
|
405
|
+
},
|
|
406
|
+
}
|
|
407
|
+
: {}),
|
|
408
|
+
...(distortion
|
|
409
|
+
? {
|
|
410
|
+
uvDistortion: {
|
|
411
|
+
map: textureRef(distortion.map, false, 'uvDistortion.map'),
|
|
412
|
+
...optional('mapUV', textureUVToJSON(distortion.mapUV, UV_DISTORTION_DEFAULT_UV)),
|
|
413
|
+
...(distortion.strength !== UV_DISTORTION_DEFAULT_STRENGTH
|
|
414
|
+
? {strength: distortion.strength}
|
|
415
|
+
: {}),
|
|
416
|
+
},
|
|
417
|
+
}
|
|
418
|
+
: {}),
|
|
419
|
+
sourceMaterial: system.material ?? undefined,
|
|
420
|
+
};
|
|
421
|
+
return materialUUID;
|
|
422
|
+
}
|