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.
- package/CHANGELOG.md +585 -0
- package/README.md +3 -1
- 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 +4811 -974
- package/dist/types/index.d.ts +4811 -974
- package/package.json +7 -2
- package/src/BatchedRenderer.ts +106 -79
- package/src/EffectPool.ts +96 -16
- package/src/MeshSurfaceEmitter.ts +24 -13
- package/src/ParticleEmitter.ts +119 -26
- package/src/ParticleSystem.ts +305 -55
- package/src/QuarksLoader.ts +298 -171
- package/src/QuarksPrefab.ts +301 -94
- package/src/QuarksSerializer.ts +44 -16
- package/src/QuarksUtil.ts +88 -11
- package/src/SpriteBatch.ts +24 -6
- package/src/TrailBatch.ts +20 -7
- package/src/VFXBatch.ts +182 -528
- package/src/batchKey.ts +6 -2
- package/src/batchSettings.ts +548 -0
- package/src/effectDocumentJSON.ts +261 -0
- package/src/effectResources.ts +21 -24
- package/src/envAtlas.ts +8 -2
- package/src/geometryUtil.ts +1 -1
- package/src/index.ts +41 -4
- package/src/nodeId.ts +25 -2
- package/src/shaderProgram.ts +213 -16
- package/src/shaderVariants.ts +124 -7
- package/src/shaders/chunks/blend.ts +39 -0
- package/src/shaders/chunks/fog.ts +8 -3
- package/src/shaders/chunks/imageProcessing.ts +143 -0
- package/src/shaders/chunks/soft.ts +6 -5
- package/src/shaders/chunks/surface.ts +2 -2
- package/src/shaders/particle_frag.glsl.ts +5 -1
- package/src/shaders/particle_frag.wgsl.ts +4 -0
- package/src/shaders/particle_physics_frag.glsl.ts +7 -2
- package/src/shaders/particle_physics_frag.wgsl.ts +4 -0
- package/src/shaders/trail_frag.glsl.ts +6 -1
- package/src/shaders/trail_frag.wgsl.ts +4 -0
- package/src/system/EmissionController.ts +23 -20
- package/src/system/EventDispatcher.ts +26 -10
- package/src/system/ParticleSpawner.ts +96 -19
- package/src/system/geometry.ts +22 -29
- package/src/system/integrate.ts +61 -4
- package/src/system/materialResolve.ts +56 -7
- package/src/system/normalize.ts +34 -2
- package/src/system/serialization.ts +54 -33
- package/src/system/settingsSchema.ts +57 -17
- package/src/system/types.ts +150 -42
- package/src/trailSettings.ts +9 -6
package/src/system/geometry.ts
CHANGED
|
@@ -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
|
|
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:
|
|
55
|
-
positions
|
|
56
|
-
|
|
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:
|
|
73
|
-
uvs2: entry.uvs2
|
|
74
|
-
|
|
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
|
|
87
|
-
|
|
88
|
-
|
|
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:
|
|
92
|
+
uvs: floats(attributes.uv?.array),
|
|
98
93
|
// three.js names the second set `uv1` (r151 on), `uv2` before.
|
|
99
|
-
uvs2: (
|
|
100
|
-
|
|
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
|
|
package/src/system/integrate.ts
CHANGED
|
@@ -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
|
|
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 (
|
|
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
|
|
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:
|
|
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
|
-
: (
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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 =
|
package/src/system/normalize.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {ConstantValue, FunctionValueGenerator, ValueGenerator, Vector2, Vector3} from 'quarks.core';
|
|
2
|
-
import type {MeshLighting, SortMode} from '../
|
|
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 {
|
|
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:
|
|
53
|
-
const flags: Partial<
|
|
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:
|
|
63
|
-
const curve: Partial<
|
|
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:
|
|
76
|
-
const out: Partial<
|
|
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:
|
|
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?:
|
|
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
|
-
|
|
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
|
|
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
|
|
171
|
+
shape: EmitterToJSON(system.emitterShape),
|
|
156
172
|
...settingsToJSON(system),
|
|
157
173
|
instancingGeometry: geometryUUID,
|
|
158
|
-
...(
|
|
174
|
+
...(rendererSettingsJSON ? {rendererEmitterSettings: rendererSettingsJSON} : {}),
|
|
159
175
|
material: materialUUID,
|
|
160
|
-
behaviors: system.behaviors.map((b) => b
|
|
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
|
|
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
|
|
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(
|
|
204
|
+
...trailSideAxisFromJSON(trailSettings?.sideAxis),
|
|
189
205
|
};
|
|
190
206
|
} else if (json.renderMode === RenderMode.StretchedBillBoard) {
|
|
191
|
-
|
|
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
|
-
|
|
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
|
-
|
|
201
|
-
const
|
|
202
|
-
typeof
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
const
|
|
207
|
-
|
|
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
|
|
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):
|
|
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
|
-
...
|
|
267
|
-
}
|
|
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
|
|
361
|
+
meta.textures[uuid] = value;
|
|
341
362
|
}
|
|
342
363
|
return uuid;
|
|
343
364
|
};
|