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.
- package/CHANGELOG.md +769 -8
- package/README.md +10 -8
- 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 +6683 -0
- package/dist/types/index.d.ts +1959 -588
- package/package.json +12 -6
- package/src/BatchedRenderer.ts +204 -101
- package/src/EffectPool.ts +4 -1
- package/src/MeshSurfaceEmitter.ts +29 -4
- package/src/ParticleEmitter.ts +45 -15
- package/src/ParticleSystem.ts +909 -2061
- package/src/QuarksLoader.ts +421 -67
- package/src/QuarksPrefab.ts +91 -4
- package/src/QuarksSerializer.ts +484 -0
- package/src/QuarksUtil.ts +15 -0
- package/src/SpriteBatch.ts +106 -246
- package/src/TrailBatch.ts +533 -342
- package/src/VFXBatch.ts +236 -136
- package/src/batchKey.ts +237 -0
- package/src/effectResources.ts +79 -0
- package/src/envAtlas.ts +119 -17
- package/src/index.ts +32 -33
- package/src/nodeId.ts +13 -0
- package/src/shaderProgram.ts +325 -0
- package/src/shaderVariants.ts +225 -0
- package/src/shaders/chunks/fog.ts +4 -2
- package/src/shaders/chunks/sheet.ts +111 -0
- package/src/shaders/chunks/soft.ts +84 -0
- package/src/shaders/chunks/surface.ts +10 -0
- 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 +3 -22
- package/src/shaders/particle_frag.wgsl.ts +3 -23
- package/src/shaders/particle_physics_frag.glsl.ts +12 -25
- package/src/shaders/particle_physics_frag.wgsl.ts +12 -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 +11 -1
- package/src/shaders/trail_frag.wgsl.ts +11 -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 +360 -0
- package/src/system/EventDispatcher.ts +44 -0
- package/src/system/ParticleSpawner.ts +274 -0
- package/src/system/SimulationClock.ts +130 -0
- package/src/system/geometry.ts +111 -0
- package/src/system/integrate.ts +428 -0
- package/src/system/materialResolve.ts +116 -0
- package/src/system/normalize.ts +95 -0
- package/src/system/serialization.ts +401 -0
- package/src/system/settingsSchema.ts +317 -0
- package/src/system/types.ts +410 -0
- package/src/textureSource.ts +30 -0
- package/src/trailSettings.ts +51 -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,428 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Behavior,
|
|
3
|
+
FusionStep,
|
|
4
|
+
Matrix4,
|
|
5
|
+
Particle,
|
|
6
|
+
ParticleStore,
|
|
7
|
+
RendererEmitterSettings,
|
|
8
|
+
TrailParticle,
|
|
9
|
+
TrailSettings,
|
|
10
|
+
Vector3,
|
|
11
|
+
} from 'quarks.core';
|
|
12
|
+
import type {VFXBatchSettings} from '../BatchedRenderer';
|
|
13
|
+
import {trailLifetime, trailMinVertexDistance} from '../trailSettings';
|
|
14
|
+
import type {BoundsTracker} from './BoundsTracker';
|
|
15
|
+
import {SIMULATION_STEP} from './SimulationClock';
|
|
16
|
+
|
|
17
|
+
// The passes of one simulation step after emission: the snapshot the renderer extrapolates
|
|
18
|
+
// from, the behaviors, then the motion, ageing and removal of the sprite path (one pass over
|
|
19
|
+
// the store's columns) or of the trail path (over the particle objects their history lives on).
|
|
20
|
+
// Plain functions over the store and the particle array, so the hot loops stay monomorphic.
|
|
21
|
+
|
|
22
|
+
/** Particle backed by a {@link ParticleStore} row, as a system always creates. */
|
|
23
|
+
type StoreBackedParticle = Particle & {storeIndex: number; setStoreIndex(index: number): void};
|
|
24
|
+
|
|
25
|
+
/** Called with each particle that died this step, when the system has `particleDied` listeners. */
|
|
26
|
+
export type DeathListener = (particle: Particle) => void;
|
|
27
|
+
|
|
28
|
+
/** Settings the bounds measurement reads, see {@link BoundsTracker.store}. */
|
|
29
|
+
export interface BoundsSettings {
|
|
30
|
+
/** The render settings, for the particle size the bounds are padded by. */
|
|
31
|
+
rendererSettings: VFXBatchSettings;
|
|
32
|
+
/** The render mode's settings, for a stretched billboard's length. */
|
|
33
|
+
rendererEmitterSettings: RendererEmitterSettings;
|
|
34
|
+
/** Whether the positions are in world space already. */
|
|
35
|
+
worldSpace: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const tempV = new Vector3();
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Remembers where everything starts this step, so the renderer can carry the motion the step
|
|
42
|
+
* produces on into the part of the frame that comes after it. Size and colour are only
|
|
43
|
+
* snapshotted when some behavior can change them; otherwise the columns stay what spawn wrote
|
|
44
|
+
* and the renderer reads them as they are. Only stretched billboards draw from velocity, and only
|
|
45
|
+
* they pay for keeping the previous one; `rebind` is called when keeping it first allocates its
|
|
46
|
+
* column, since every particle then has to be re-pointed at the store.
|
|
47
|
+
*/
|
|
48
|
+
export function snapshotPreviousState(
|
|
49
|
+
store: ParticleStore,
|
|
50
|
+
particleCount: number,
|
|
51
|
+
snapshotSize: boolean,
|
|
52
|
+
snapshotColor: boolean,
|
|
53
|
+
keepVelocity: boolean,
|
|
54
|
+
rebind: () => void
|
|
55
|
+
): void {
|
|
56
|
+
const vector3Rows = particleCount * 3;
|
|
57
|
+
store.useSizeSnapshot(snapshotSize);
|
|
58
|
+
store.useColorSnapshot(snapshotColor);
|
|
59
|
+
store.previousPosition.set(store.position.subarray(0, vector3Rows));
|
|
60
|
+
if (snapshotSize) {
|
|
61
|
+
store.previousSize.set(store.size.subarray(0, vector3Rows));
|
|
62
|
+
}
|
|
63
|
+
if (keepVelocity) {
|
|
64
|
+
if (store.keepPreviousVelocity()) {
|
|
65
|
+
rebind();
|
|
66
|
+
}
|
|
67
|
+
store.previousVelocity.set(store.velocity.subarray(0, vector3Rows));
|
|
68
|
+
}
|
|
69
|
+
if (snapshotColor) {
|
|
70
|
+
store.previousColor.set(store.color.subarray(0, particleCount * 4));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Runs the behaviors' frame updates and then each step of the plan over the live particles. */
|
|
75
|
+
export function runBehaviorSteps(
|
|
76
|
+
steps: ReadonlyArray<FusionStep>,
|
|
77
|
+
behaviors: Behavior[],
|
|
78
|
+
particles: Particle[],
|
|
79
|
+
particleCount: number,
|
|
80
|
+
delta: number,
|
|
81
|
+
store: ParticleStore
|
|
82
|
+
): void {
|
|
83
|
+
for (let s = 0; s < steps.length; s++) {
|
|
84
|
+
const step = steps[s];
|
|
85
|
+
const stepBehaviors = step.behaviors;
|
|
86
|
+
for (let j = 0; j < stepBehaviors.length; j++) {
|
|
87
|
+
stepBehaviors[j].frameUpdate(delta);
|
|
88
|
+
}
|
|
89
|
+
// Nothing to update: skip, matching what the per-particle loop did.
|
|
90
|
+
// A behavior may set up state before its loop that is only valid
|
|
91
|
+
// once it has seen a particle.
|
|
92
|
+
if (particleCount === 0) {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (step.run !== undefined) {
|
|
96
|
+
step.run(behaviors, particles, particleCount, delta, store);
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
const behavior = stepBehaviors[0];
|
|
100
|
+
if (behavior.updateAll !== undefined) {
|
|
101
|
+
behavior.updateAll(particles, particleCount, delta);
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
for (let i = 0; i < particleCount; i++) {
|
|
105
|
+
const particle = particles[i];
|
|
106
|
+
// Inlined `died` — this runs once per behavior per particle.
|
|
107
|
+
if (particle.age < particle.life) {
|
|
108
|
+
behavior.update(particle, delta);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Moves a dead particle's slot and row to the end of the live range, so slot k always owns
|
|
116
|
+
* row k and the live particles stay one contiguous range the renderer copies as is. Returns
|
|
117
|
+
* the particle that died.
|
|
118
|
+
*/
|
|
119
|
+
function retire(particles: Particle[], store: ParticleStore, i: number, last: number): Particle {
|
|
120
|
+
const particle = particles[i];
|
|
121
|
+
const survivor = particles[last] as StoreBackedParticle;
|
|
122
|
+
particles[i] = survivor;
|
|
123
|
+
particles[last] = particle;
|
|
124
|
+
if (i !== last) {
|
|
125
|
+
store.swapRows(i, last);
|
|
126
|
+
survivor.setStoreIndex(i);
|
|
127
|
+
(particle as StoreBackedParticle).setStoreIndex(last);
|
|
128
|
+
}
|
|
129
|
+
return particle;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* The sprite path's step after the behaviors, as one pass over the columns that does three
|
|
134
|
+
* things, none of which touches a particle object until a death. Returns the live count.
|
|
135
|
+
*
|
|
136
|
+
* Newborns were snapshotted with their spawn size and colour, before SizeOverLife and
|
|
137
|
+
* ColorOverLife ran. That false jump would be extrapolated by the renderer when the residual is
|
|
138
|
+
* negative (common with the rounded fixed step) and flash the start appearance at the emitter,
|
|
139
|
+
* so the snapshot is collapsed onto the curves' t=0 value. Age is still 0 at that point; the
|
|
140
|
+
* same iteration then ages it.
|
|
141
|
+
*
|
|
142
|
+
* Position is integrated along the velocity.
|
|
143
|
+
*
|
|
144
|
+
* 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.
|
|
146
|
+
*
|
|
147
|
+
* The bounds fall out of the same pass. Measuring every particle on every step costs about as
|
|
148
|
+
* much as the integration itself, so the full measurement runs every few steps; in between,
|
|
149
|
+
* newborns extend the last bounds and the fastest particle seen since pads them for the motion,
|
|
150
|
+
* which keeps them conservative.
|
|
151
|
+
*/
|
|
152
|
+
export function integrateSprites(
|
|
153
|
+
delta: number,
|
|
154
|
+
particles: Particle[],
|
|
155
|
+
store: ParticleStore,
|
|
156
|
+
liveParticleCount: number,
|
|
157
|
+
collapseSize: boolean,
|
|
158
|
+
collapseColor: boolean,
|
|
159
|
+
bounds: BoundsTracker,
|
|
160
|
+
settings: BoundsSettings,
|
|
161
|
+
onDied: DeathListener | undefined
|
|
162
|
+
): number {
|
|
163
|
+
const scalars = store.scalars;
|
|
164
|
+
const stride = ParticleStore.SCALAR_STRIDE;
|
|
165
|
+
const positions = store.position;
|
|
166
|
+
const velocities = store.velocity;
|
|
167
|
+
const prevSize = store.previousSize;
|
|
168
|
+
const curSize = store.size;
|
|
169
|
+
const prevColor = store.previousColor;
|
|
170
|
+
const curColor = store.color;
|
|
171
|
+
const measure = bounds.due;
|
|
172
|
+
const carriedMin = bounds.min;
|
|
173
|
+
const carriedMax = bounds.max;
|
|
174
|
+
let minX = measure ? Infinity : carriedMin.x;
|
|
175
|
+
let minY = measure ? Infinity : carriedMin.y;
|
|
176
|
+
let minZ = measure ? Infinity : carriedMin.z;
|
|
177
|
+
let maxX = measure ? -Infinity : carriedMax.x;
|
|
178
|
+
let maxY = measure ? -Infinity : carriedMax.y;
|
|
179
|
+
let maxZ = measure ? -Infinity : carriedMax.z;
|
|
180
|
+
let maxSize = measure ? 0 : bounds.maxSize;
|
|
181
|
+
let maxSpeedSq = measure ? 0 : bounds.maxSpeedSq;
|
|
182
|
+
for (let i = 0; i < liveParticleCount; i++) {
|
|
183
|
+
const scalar = i * stride;
|
|
184
|
+
let age = scalars[scalar + ParticleStore.AGE];
|
|
185
|
+
const offset = i * 3;
|
|
186
|
+
const track = measure || age === 0;
|
|
187
|
+
if (age === 0) {
|
|
188
|
+
if (collapseSize) {
|
|
189
|
+
prevSize[offset] = curSize[offset];
|
|
190
|
+
prevSize[offset + 1] = curSize[offset + 1];
|
|
191
|
+
prevSize[offset + 2] = curSize[offset + 2];
|
|
192
|
+
}
|
|
193
|
+
if (collapseColor) {
|
|
194
|
+
const o4 = i * 4;
|
|
195
|
+
prevColor[o4] = curColor[o4];
|
|
196
|
+
prevColor[o4 + 1] = curColor[o4 + 1];
|
|
197
|
+
prevColor[o4 + 2] = curColor[o4 + 2];
|
|
198
|
+
prevColor[o4 + 3] = curColor[o4 + 3];
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
const step = delta * scalars[scalar + ParticleStore.SPEED_MODIFIER];
|
|
202
|
+
const vx = velocities[offset];
|
|
203
|
+
const vy = velocities[offset + 1];
|
|
204
|
+
const vz = velocities[offset + 2];
|
|
205
|
+
const x = positions[offset] + vx * step;
|
|
206
|
+
const y = positions[offset + 1] + vy * step;
|
|
207
|
+
const z = positions[offset + 2] + vz * step;
|
|
208
|
+
positions[offset] = x;
|
|
209
|
+
positions[offset + 1] = y;
|
|
210
|
+
positions[offset + 2] = z;
|
|
211
|
+
if (track) {
|
|
212
|
+
minX = Math.min(minX, x);
|
|
213
|
+
maxX = Math.max(maxX, x);
|
|
214
|
+
minY = Math.min(minY, y);
|
|
215
|
+
maxY = Math.max(maxY, y);
|
|
216
|
+
minZ = Math.min(minZ, z);
|
|
217
|
+
maxZ = Math.max(maxZ, z);
|
|
218
|
+
maxSize = Math.max(maxSize, Math.max(curSize[offset], Math.max(curSize[offset + 1], curSize[offset + 2])));
|
|
219
|
+
}
|
|
220
|
+
// Speed is tracked every step: it bounds how far the particles
|
|
221
|
+
// can have moved since the last measurement.
|
|
222
|
+
maxSpeedSq = Math.max(maxSpeedSq, vx * vx + vy * vy + vz * vz);
|
|
223
|
+
age += delta;
|
|
224
|
+
scalars[scalar + ParticleStore.AGE] = age;
|
|
225
|
+
if (age >= scalars[scalar + ParticleStore.LIFE]) {
|
|
226
|
+
const particle = retire(particles, store, i, liveParticleCount - 1);
|
|
227
|
+
liveParticleCount--;
|
|
228
|
+
i--;
|
|
229
|
+
if (onDied !== undefined) {
|
|
230
|
+
onDied(particle);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
bounds.store(
|
|
235
|
+
liveParticleCount,
|
|
236
|
+
measure,
|
|
237
|
+
minX,
|
|
238
|
+
minY,
|
|
239
|
+
minZ,
|
|
240
|
+
maxX,
|
|
241
|
+
maxY,
|
|
242
|
+
maxZ,
|
|
243
|
+
maxSize,
|
|
244
|
+
maxSpeedSq,
|
|
245
|
+
settings.rendererSettings,
|
|
246
|
+
settings.rendererEmitterSettings,
|
|
247
|
+
settings.worldSpace,
|
|
248
|
+
SIMULATION_STEP
|
|
249
|
+
);
|
|
250
|
+
return liveParticleCount;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** The newborn collapse of the trail path, see {@link integrateSprites}; trails keep their history on the objects, so it is its own pass. */
|
|
254
|
+
export function collapseTrailNewborns(
|
|
255
|
+
store: ParticleStore,
|
|
256
|
+
particleCount: number,
|
|
257
|
+
snapshotSize: boolean,
|
|
258
|
+
snapshotColor: boolean
|
|
259
|
+
): void {
|
|
260
|
+
const scalars = store.scalars;
|
|
261
|
+
const stride = ParticleStore.SCALAR_STRIDE;
|
|
262
|
+
const prevSize = store.previousSize;
|
|
263
|
+
const curSize = store.size;
|
|
264
|
+
const prevColor = store.previousColor;
|
|
265
|
+
const curColor = store.color;
|
|
266
|
+
for (let i = 0, o3 = 0, o4 = 0, s = 0; i < particleCount; i++, o3 += 3, o4 += 4, s += stride) {
|
|
267
|
+
if (scalars[s + ParticleStore.AGE] !== 0) {
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
if (snapshotSize) {
|
|
271
|
+
prevSize[o3] = curSize[o3];
|
|
272
|
+
prevSize[o3 + 1] = curSize[o3 + 1];
|
|
273
|
+
prevSize[o3 + 2] = curSize[o3 + 2];
|
|
274
|
+
}
|
|
275
|
+
if (snapshotColor) {
|
|
276
|
+
prevColor[o4] = curColor[o4];
|
|
277
|
+
prevColor[o4 + 1] = curColor[o4 + 1];
|
|
278
|
+
prevColor[o4 + 2] = curColor[o4 + 2];
|
|
279
|
+
prevColor[o4 + 3] = curColor[o4 + 3];
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Moves and ages the trail particles: a trail that follows its local origin is placed by its
|
|
286
|
+
* emitter (or parent) matrix every step, any other along its velocity.
|
|
287
|
+
*/
|
|
288
|
+
export function moveTrails(
|
|
289
|
+
delta: number,
|
|
290
|
+
particles: Particle[],
|
|
291
|
+
store: ParticleStore,
|
|
292
|
+
particleCount: number,
|
|
293
|
+
followLocalOrigin: boolean,
|
|
294
|
+
emitterMatrix: Matrix4
|
|
295
|
+
): void {
|
|
296
|
+
if (followLocalOrigin) {
|
|
297
|
+
for (let i = 0; i < particleCount; i++) {
|
|
298
|
+
const particle = particles[i];
|
|
299
|
+
if ((particle as TrailParticle).localPosition) {
|
|
300
|
+
particle.position.copy((particle as TrailParticle).localPosition!);
|
|
301
|
+
particle.position.applyMatrix4(particle.parentMatrix ?? emitterMatrix);
|
|
302
|
+
} else {
|
|
303
|
+
particle.position.addScaledVector(particle.velocity, delta * particle.speedModifier);
|
|
304
|
+
}
|
|
305
|
+
particle.age += delta;
|
|
306
|
+
}
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
const scalars = store.scalars;
|
|
310
|
+
const stride = ParticleStore.SCALAR_STRIDE;
|
|
311
|
+
const positions = store.position;
|
|
312
|
+
const velocities = store.velocity;
|
|
313
|
+
for (let i = 0, offset = 0, scalar = 0; i < particleCount; i++, offset += 3, scalar += stride) {
|
|
314
|
+
const step = delta * scalars[scalar + ParticleStore.SPEED_MODIFIER];
|
|
315
|
+
positions[offset] += velocities[offset] * step;
|
|
316
|
+
positions[offset + 1] += velocities[offset + 1] * step;
|
|
317
|
+
positions[offset + 2] += velocities[offset + 2] * step;
|
|
318
|
+
scalars[scalar + ParticleStore.AGE] += delta;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Records this step's sample into each trail's history: its position (in world space when the
|
|
324
|
+
* trail asks for it and the system is not already there) and, for a swept trail, the side axis
|
|
325
|
+
* as the emitter holds it, in the space the positions are recorded in. With a minimum vertex
|
|
326
|
+
* distance the newest sample follows the particle until it is that far from the sample before;
|
|
327
|
+
* samples older than the trail's lifetime are dropped from the tail. A trail that dies with its
|
|
328
|
+
* particle has its history emptied instead, so the removal pass takes it this same step.
|
|
329
|
+
*/
|
|
330
|
+
export function recordTrailHistory(
|
|
331
|
+
particles: Particle[],
|
|
332
|
+
particleCount: number,
|
|
333
|
+
trailSettings: TrailSettings,
|
|
334
|
+
worldSpace: boolean,
|
|
335
|
+
emitterMatrix: Matrix4
|
|
336
|
+
): void {
|
|
337
|
+
const dieWithParticle = trailSettings.dieWithParticle;
|
|
338
|
+
// A world-space system records world positions already.
|
|
339
|
+
const recordWorldSpace = trailSettings.recordWorldSpace && !worldSpace;
|
|
340
|
+
const sideAxis = trailSettings.sideAxis;
|
|
341
|
+
const sidesInWorld = worldSpace || Boolean(trailSettings.recordWorldSpace);
|
|
342
|
+
const minDistance = trailMinVertexDistance(trailSettings);
|
|
343
|
+
const lifetime = trailLifetime(trailSettings);
|
|
344
|
+
for (let i = 0; i < particleCount; i++) {
|
|
345
|
+
const trail = particles[i] as TrailParticle;
|
|
346
|
+
// A particle at the end of its life is dead: it records nothing more and its trail fades.
|
|
347
|
+
const alive = trail.age < trail.life;
|
|
348
|
+
if (dieWithParticle && !alive) {
|
|
349
|
+
trail.resetHistory();
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
// The newest sample follows the particle until it is the minimum distance from the sample
|
|
353
|
+
// before it; then it is fixed where it is and this step's sample starts a new one.
|
|
354
|
+
let replaceHead = false;
|
|
355
|
+
if (minDistance > 0 && alive && trail.historyCount > 1) {
|
|
356
|
+
const distances = trail.historyDistances;
|
|
357
|
+
const count = trail.historyCount;
|
|
358
|
+
replaceHead =
|
|
359
|
+
distances[trail.getHistoryIndex(count - 1)] - distances[trail.getHistoryIndex(count - 2)] < minDistance;
|
|
360
|
+
}
|
|
361
|
+
trail.update(replaceHead, !recordWorldSpace);
|
|
362
|
+
// update() wrote a sample only while the particle is alive.
|
|
363
|
+
if (recordWorldSpace && alive) {
|
|
364
|
+
const offset = ((trail.historyHead + trail.historyCapacity - 1) % trail.historyCapacity) * 3;
|
|
365
|
+
const positions = trail.historyPositions;
|
|
366
|
+
tempV.set(positions[offset], positions[offset + 1], positions[offset + 2]);
|
|
367
|
+
tempV.applyMatrix4(trail.parentMatrix ?? emitterMatrix);
|
|
368
|
+
positions[offset] = tempV.x;
|
|
369
|
+
positions[offset + 1] = tempV.y;
|
|
370
|
+
positions[offset + 2] = tempV.z;
|
|
371
|
+
trail.measureHeadDistance();
|
|
372
|
+
}
|
|
373
|
+
if (lifetime !== Infinity) {
|
|
374
|
+
const ages = trail.historyAges;
|
|
375
|
+
const age = trail.age;
|
|
376
|
+
while (trail.historyCount > 0 && age - ages[trail.getHistoryIndex(0)] > lifetime) {
|
|
377
|
+
trail.dropOldest();
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
if (sideAxis && alive) {
|
|
381
|
+
trail.ensureSideHistory();
|
|
382
|
+
const offset = ((trail.historyHead + trail.historyCapacity - 1) % trail.historyCapacity) * 3;
|
|
383
|
+
const sides = trail.historySides;
|
|
384
|
+
let x = sideAxis.x;
|
|
385
|
+
let y = sideAxis.y;
|
|
386
|
+
let z = sideAxis.z;
|
|
387
|
+
if (sidesInWorld) {
|
|
388
|
+
const m = (trail.parentMatrix ?? emitterMatrix).elements;
|
|
389
|
+
const tx = m[0] * x + m[4] * y + m[8] * z;
|
|
390
|
+
const ty = m[1] * x + m[5] * y + m[9] * z;
|
|
391
|
+
z = m[2] * x + m[6] * y + m[10] * z;
|
|
392
|
+
x = tx;
|
|
393
|
+
y = ty;
|
|
394
|
+
}
|
|
395
|
+
sides[offset] = x;
|
|
396
|
+
sides[offset + 1] = y;
|
|
397
|
+
sides[offset + 2] = z;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Retires each trail particle that has died and whose ribbon has faded out (an empty history),
|
|
404
|
+
* the same swap as the sprite path. Returns the live count.
|
|
405
|
+
*/
|
|
406
|
+
export function removeDeadTrails(
|
|
407
|
+
particles: Particle[],
|
|
408
|
+
store: ParticleStore,
|
|
409
|
+
liveParticleCount: number,
|
|
410
|
+
onDied: DeathListener | undefined
|
|
411
|
+
): number {
|
|
412
|
+
const scalars = store.scalars;
|
|
413
|
+
const stride = ParticleStore.SCALAR_STRIDE;
|
|
414
|
+
for (let i = 0; i < liveParticleCount; i++) {
|
|
415
|
+
const particle = particles[i] as TrailParticle;
|
|
416
|
+
const scalar = i * stride;
|
|
417
|
+
const dead = scalars[scalar + ParticleStore.AGE] >= scalars[scalar + ParticleStore.LIFE];
|
|
418
|
+
if (dead && particle.historyCount === 0) {
|
|
419
|
+
retire(particles, store, i, liveParticleCount - 1);
|
|
420
|
+
liveParticleCount--;
|
|
421
|
+
i--;
|
|
422
|
+
if (onDied !== undefined) {
|
|
423
|
+
onDied(particle);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
return liveParticleCount;
|
|
428
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import type {BaseTexture} from '@babylonjs/core/Materials/Textures/baseTexture';
|
|
2
|
+
import type {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
|
+
/** The draw settings a {@link ParticleSystem} takes from a material, before the env atlas is built. */
|
|
18
|
+
export interface ResolvedMaterialSettings {
|
|
19
|
+
texture: Texture | null;
|
|
20
|
+
reflectionTexture: BaseTexture | null;
|
|
21
|
+
reflectionLevel: number;
|
|
22
|
+
/** A live atlas the material, the overrides or the current settings already have; null if none. */
|
|
23
|
+
reflectionAtlas: BaseTexture | null;
|
|
24
|
+
blendMode: number;
|
|
25
|
+
transparent: boolean;
|
|
26
|
+
depthTest: boolean;
|
|
27
|
+
depthWrite: boolean;
|
|
28
|
+
alphaTest: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** `atlas` unless it has been disposed (it no longer has a scene). */
|
|
32
|
+
export function liveAtlas<T extends BaseTexture>(atlas: T | null | undefined): T | null {
|
|
33
|
+
return atlas?.getScene() ? atlas : null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Reads the draw settings from a Babylon material, or from the material record of a loaded file:
|
|
38
|
+
* its texture, reflection, alpha mode, depth flags and alpha test. `overrides` win; a setting the
|
|
39
|
+
* material does not have keeps its `current` value.
|
|
40
|
+
*/
|
|
41
|
+
export function resolveMaterialSettings(
|
|
42
|
+
material: any,
|
|
43
|
+
overrides: MaterialOverrides,
|
|
44
|
+
current: VFXBatchSettings
|
|
45
|
+
): ResolvedMaterialSettings {
|
|
46
|
+
const texture =
|
|
47
|
+
overrides.texture !== undefined
|
|
48
|
+
? overrides.texture
|
|
49
|
+
: (material?.albedoTexture ??
|
|
50
|
+
material?.diffuseTexture ??
|
|
51
|
+
material?.emissiveTexture ??
|
|
52
|
+
material?.opacityTexture ??
|
|
53
|
+
material?.baseTexture ??
|
|
54
|
+
current.texture ??
|
|
55
|
+
null);
|
|
56
|
+
const reflectionTexture =
|
|
57
|
+
material?.reflectionTexture ?? material?.environmentTexture ?? current.reflectionTexture ?? null;
|
|
58
|
+
const reflectionTextureLevel = typeof reflectionTexture?.level === 'number' ? reflectionTexture.level : 1;
|
|
59
|
+
const blendMode =
|
|
60
|
+
overrides.blendMode ??
|
|
61
|
+
(typeof material?.alphaMode === 'number' ? material.alphaMode : current.materialBlendMode);
|
|
62
|
+
const transparent =
|
|
63
|
+
overrides.transparent ??
|
|
64
|
+
(typeof material?.needAlphaBlending === 'function'
|
|
65
|
+
? material.needAlphaBlending()
|
|
66
|
+
: typeof material?.alpha === 'number'
|
|
67
|
+
? material.alpha < 1
|
|
68
|
+
: current.materialTransparent);
|
|
69
|
+
const depthTest =
|
|
70
|
+
overrides.depthTest ??
|
|
71
|
+
(typeof material?.disableDepthTest === 'boolean' ? !material.disableDepthTest : current.materialDepthTest);
|
|
72
|
+
const depthWrite =
|
|
73
|
+
overrides.depthWrite ??
|
|
74
|
+
(typeof material?.disableDepthWrite === 'boolean'
|
|
75
|
+
? !material.disableDepthWrite
|
|
76
|
+
: typeof material?.forceDepthWrite === 'boolean'
|
|
77
|
+
? material.forceDepthWrite
|
|
78
|
+
: current.materialDepthWrite);
|
|
79
|
+
const alphaTest =
|
|
80
|
+
overrides.alphaTest ??
|
|
81
|
+
(() => {
|
|
82
|
+
// StandardMaterial always has alphaCutOff (default 0.4). Only treat it as
|
|
83
|
+
// particle alpha-test when the material is actually in an alpha-test mode —
|
|
84
|
+
// otherwise Mesh Material demos wrongly enable USE_ALPHATEST.
|
|
85
|
+
const mode = material?.transparencyMode;
|
|
86
|
+
const alphaTestMode = mode === 1 || mode === 3;
|
|
87
|
+
if (alphaTestMode && typeof material?.alphaCutOff === 'number') {
|
|
88
|
+
return material.alphaCutOff;
|
|
89
|
+
}
|
|
90
|
+
if (alphaTestMode && typeof material?.alphaCutOffValue === 'number') {
|
|
91
|
+
return material.alphaCutOffValue;
|
|
92
|
+
}
|
|
93
|
+
if (material) {
|
|
94
|
+
return 0;
|
|
95
|
+
}
|
|
96
|
+
return current.materialAlphaTest;
|
|
97
|
+
})();
|
|
98
|
+
return {
|
|
99
|
+
texture,
|
|
100
|
+
reflectionTexture,
|
|
101
|
+
reflectionLevel:
|
|
102
|
+
typeof material?.reflectionLevel === 'number' ? material.reflectionLevel : reflectionTextureLevel,
|
|
103
|
+
// A disposed atlas (its cube was disposed, or disposeEnvAtlas ran) is skipped, so a new one is built.
|
|
104
|
+
reflectionAtlas:
|
|
105
|
+
liveAtlas(material?.reflectionAtlas) ??
|
|
106
|
+
liveAtlas(overrides.reflectionAtlas) ??
|
|
107
|
+
liveAtlas(current.reflectionAtlas),
|
|
108
|
+
// Do not expand cube files into six face textures anymore — that path
|
|
109
|
+
// still trips GL_INVALID_OPERATION on iOS. Prefer a single atlas.
|
|
110
|
+
blendMode,
|
|
111
|
+
transparent,
|
|
112
|
+
depthTest,
|
|
113
|
+
depthWrite,
|
|
114
|
+
alphaTest,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import {ConstantValue, FunctionValueGenerator, ValueGenerator, Vector2, Vector3} from 'quarks.core';
|
|
2
|
+
import type {MeshLighting, SortMode} from '../VFXBatch';
|
|
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
|
+
/** `mode` if it is one of the culling modes, else `'always'`. */
|
|
19
|
+
export function validCullingMode(mode: unknown): CullingMode {
|
|
20
|
+
return mode === 'pause' || mode === 'pauseAndCatchUp' ? mode : 'always';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** `count` as a whole number of particles, or undefined (no limit) when it is unset, NaN or negative. */
|
|
24
|
+
export function validMaxParticles(count: unknown): number | undefined {
|
|
25
|
+
return typeof count === 'number' && count >= 0 && count < Infinity ? Math.floor(count) : undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** A copy of `v`, or undefined when it is unset or zero, which is the same as no pivot. */
|
|
29
|
+
export function nonZeroPivot(v: Vector3 | undefined): Vector3 | undefined {
|
|
30
|
+
return v && (v.x !== 0 || v.y !== 0 || v.z !== 0) ? v.clone() : undefined;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** A copy of `v`, or undefined when neither axis flips. */
|
|
34
|
+
export function nonZeroFlip(v: Vector2 | undefined): Vector2 | undefined {
|
|
35
|
+
return v && (v.x > 0 || v.y > 0) ? v.clone() : undefined;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** `cutoff` kept to 0…1, or 0 (no alpha test) when it is unset, NaN or negative. */
|
|
39
|
+
export function validAlphaTest(cutoff: unknown): number {
|
|
40
|
+
return typeof cutoff === 'number' && cutoff > 0 ? Math.min(cutoff, 1) : 0;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** A flipbook's columns or rows: a whole number of cells, at least 1; anything else is 1. */
|
|
44
|
+
export function validTileCount(count: unknown): number {
|
|
45
|
+
return typeof count === 'number' && Number.isFinite(count) && count >= 1 ? Math.floor(count) : 1;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** World units in front of the scene over which a soft particle is fully faded by default. */
|
|
49
|
+
export const DEFAULT_SOFT_FAR_FADE = 1;
|
|
50
|
+
|
|
51
|
+
/** A soft-particle fade distance: finite and at least 0, else `fallback`. */
|
|
52
|
+
export function validSoftFade(distance: unknown, fallback: number): number {
|
|
53
|
+
return typeof distance === 'number' && Number.isFinite(distance) && distance >= 0 ? distance : fallback;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Seconds between a burst's waves when a burst leaves `interval` out, in code and in a file. */
|
|
57
|
+
export const DEFAULT_BURST_INTERVAL = 0.1;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* A burst with every field present and in range: a `time` that is a finite number of seconds
|
|
61
|
+
* (else 0), a `count` generator (a number becomes a constant, nothing becomes 0), `cycle` at least
|
|
62
|
+
* 1, `interval` {@link DEFAULT_BURST_INTERVAL} when left out and never negative, `probability`
|
|
63
|
+
* within 0..1 (1 when left out). The same rules apply to a burst given to the constructor and one
|
|
64
|
+
* read from a file, so both fire alike.
|
|
65
|
+
*/
|
|
66
|
+
export function normalizeBurst(burst: Partial<BurstParameters> & {count?: unknown}): BurstParameters {
|
|
67
|
+
const count = burst.count;
|
|
68
|
+
const time = typeof burst.time === 'number' && Number.isFinite(burst.time) && burst.time > 0 ? burst.time : 0;
|
|
69
|
+
const cycle = typeof burst.cycle === 'number' && Number.isFinite(burst.cycle) && burst.cycle >= 1 ? burst.cycle : 1;
|
|
70
|
+
const interval =
|
|
71
|
+
typeof burst.interval === 'number' && Number.isFinite(burst.interval)
|
|
72
|
+
? Math.max(0, burst.interval)
|
|
73
|
+
: DEFAULT_BURST_INTERVAL;
|
|
74
|
+
const probability =
|
|
75
|
+
typeof burst.probability === 'number' && Number.isFinite(burst.probability)
|
|
76
|
+
? Math.min(1, Math.max(0, burst.probability))
|
|
77
|
+
: 1;
|
|
78
|
+
return {
|
|
79
|
+
time,
|
|
80
|
+
count:
|
|
81
|
+
typeof count === 'number'
|
|
82
|
+
? new ConstantValue(count)
|
|
83
|
+
: count !== undefined && count !== null
|
|
84
|
+
? (count as ValueGenerator | FunctionValueGenerator)
|
|
85
|
+
: new ConstantValue(0),
|
|
86
|
+
cycle,
|
|
87
|
+
interval,
|
|
88
|
+
probability,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** The bursts normalised (see {@link normalizeBurst}) and in time order, which emission relies on. */
|
|
93
|
+
export function normalizeBursts(bursts: ReadonlyArray<Partial<BurstParameters>> | undefined): BurstParameters[] {
|
|
94
|
+
return (bursts ?? []).map(normalizeBurst).sort((a, b) => a.time - b.time);
|
|
95
|
+
}
|