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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "babylon.quarks",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.29.0",
|
|
4
4
|
"description": "A High-Performance Particle System for Babylon.js",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"types": "./dist/types/index.d.ts",
|
|
@@ -28,6 +28,11 @@
|
|
|
28
28
|
"README.md",
|
|
29
29
|
"CHANGELOG.md"
|
|
30
30
|
],
|
|
31
|
+
"sideEffects": [
|
|
32
|
+
"./dist/babylon.quarks.esm.js",
|
|
33
|
+
"./dist/babylon.quarks.cjs",
|
|
34
|
+
"./dist/babylon.quarks.umd.min.js"
|
|
35
|
+
],
|
|
31
36
|
"scripts": {
|
|
32
37
|
"clean": "rimraf dist dist-types tsconfig.tsbuildinfo",
|
|
33
38
|
"build:types": "tsc --emitDeclarationOnly",
|
|
@@ -66,7 +71,7 @@
|
|
|
66
71
|
},
|
|
67
72
|
"homepage": "https://github.com/Soullnik/babylon.quarks-standalone#readme",
|
|
68
73
|
"peerDependencies": {
|
|
69
|
-
"@babylonjs/core": "
|
|
74
|
+
"@babylonjs/core": "^9.0.0"
|
|
70
75
|
},
|
|
71
76
|
"devDependencies": {
|
|
72
77
|
"@babylonjs/core": "^9.31.0",
|
package/src/BatchedRenderer.ts
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
ParticleErosion,
|
|
21
21
|
ParticleGradientMap,
|
|
22
22
|
ParticleUVDistortion,
|
|
23
|
+
RenderFace,
|
|
23
24
|
RenderMode,
|
|
24
25
|
SortMode,
|
|
25
26
|
TextureUV,
|
|
@@ -108,13 +109,27 @@ export interface VFXBatchSettings {
|
|
|
108
109
|
renderingGroupId: number;
|
|
109
110
|
/**
|
|
110
111
|
* Whether the scene's fog applies to the particles, as it does to any other
|
|
111
|
-
* mesh; true by default. Particles that
|
|
112
|
-
* `
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
112
|
+
* mesh; true by default. Particles that lay over the frame (`ALPHA_COMBINE`,
|
|
113
|
+
* `ALPHA_PREMULTIPLIED`, opaque ones) mix toward the fog colour; particles
|
|
114
|
+
* that add to it or subtract from it (`ALPHA_ADD`, `ALPHA_ONEONE`,
|
|
115
|
+
* `ALPHA_SCREENMODE`, `ALPHA_SUBTRACT`, …) fade toward black, since the fog
|
|
116
|
+
* colour would fog the whole screen; particles that multiply it
|
|
117
|
+
* (`ALPHA_MULTIPLY`) fade toward white. See {@link blendCategory}. Systems
|
|
118
|
+
* that differ are batched apart.
|
|
116
119
|
*/
|
|
117
120
|
applyFog: boolean;
|
|
121
|
+
/**
|
|
122
|
+
* Whether the material applies the scene's image processing (`scene.imageProcessingConfiguration`:
|
|
123
|
+
* exposure, contrast, tone mapping, vignette, colour curves, colour grading, dithering), as
|
|
124
|
+
* Babylon's own `ParticleSystem` and its standard and PBR materials do; unset reads as true.
|
|
125
|
+
* When the configuration is applied by a post process (a `DefaultRenderingPipeline` with image
|
|
126
|
+
* processing on), the material leaves its colour in linear space for it, as those materials do.
|
|
127
|
+
* Off, the material does neither: its colour is written as authored. That does not keep the
|
|
128
|
+
* particles out of a post process, which processes the whole frame; particles that must reach
|
|
129
|
+
* the screen untouched are drawn by a camera the pipeline is not attached to. Systems that
|
|
130
|
+
* differ are batched apart. See {@link ParticleSystem.applyImageProcessing}.
|
|
131
|
+
*/
|
|
132
|
+
applyImageProcessing?: boolean;
|
|
118
133
|
/**
|
|
119
134
|
* Offset of billboard quads from their particle, in particle-size units
|
|
120
135
|
* (Unity's renderer Pivot). On billboards x/y move the quad in its own plane
|
|
@@ -125,6 +140,11 @@ export interface VFXBatchSettings {
|
|
|
125
140
|
pivot?: Vector3;
|
|
126
141
|
/** Draw order within the batch, see {@link SortMode}. Unset is `'none'`. Systems that differ are batched apart. */
|
|
127
142
|
sortMode?: SortMode;
|
|
143
|
+
/**
|
|
144
|
+
* Which faces a mesh particle draws, see {@link RenderFace}. Unset is `'both'`. Mesh systems
|
|
145
|
+
* that differ are batched apart; other render modes ignore it.
|
|
146
|
+
*/
|
|
147
|
+
renderFace?: RenderFace;
|
|
128
148
|
/**
|
|
129
149
|
* Smallest size a billboard or stretched particle may have on screen, as a
|
|
130
150
|
* fraction of the viewport height (Unity's Min Particle Size); 0 by default.
|
|
@@ -196,20 +216,27 @@ export interface AdaptivePerformanceState extends AdaptivePerformanceOptions {
|
|
|
196
216
|
*
|
|
197
217
|
* Systems step on a fixed 60 Hz clock, so an effect plays out the same at any frame rate.
|
|
198
218
|
*
|
|
199
|
-
* The renderer is a node so the batches have a parent to live under
|
|
200
|
-
*
|
|
201
|
-
*
|
|
202
|
-
* belongs to one renderer at a time: adding it to another takes it out of the
|
|
219
|
+
* The renderer is a node so the batches have a parent to live under: disabling it hides them,
|
|
220
|
+
* disposing it disposes them. Its transform moves nothing: particles are placed in the world by
|
|
221
|
+
* their emitters, and the batch meshes draw them there whatever the renderer's position, rotation
|
|
222
|
+
* or scale. A system belongs to one renderer at a time: adding it to another takes it out of the
|
|
223
|
+
* first.
|
|
203
224
|
*/
|
|
204
225
|
export class BatchedRenderer extends TransformNode {
|
|
205
|
-
|
|
206
|
-
private
|
|
207
|
-
|
|
208
|
-
batches
|
|
226
|
+
private readonly batchList: Array<VFXBatch> = [];
|
|
227
|
+
private readonly batchIndexOf = new Map<IParticleSystem, number>();
|
|
228
|
+
|
|
229
|
+
/** The batches, one draw each, to inspect what is drawn; the renderer makes and frees them. */
|
|
230
|
+
get batches(): ReadonlyArray<VFXBatch> {
|
|
231
|
+
return this.batchList;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** Index into {@link batches} of each system's batch, to inspect; the renderer keeps it. */
|
|
235
|
+
get systemToBatchIndex(): ReadonlyMap<IParticleSystem, number> {
|
|
236
|
+
return this.batchIndexOf;
|
|
237
|
+
}
|
|
209
238
|
/** Seconds this renderer has been updated for; the clock UV and erosion scrolling run on. */
|
|
210
239
|
private elapsed = 0;
|
|
211
|
-
/** Index into {@link batches} of each system's batch. The renderer owns it. */
|
|
212
|
-
systemToBatchIndex: Map<IParticleSystem, number> = new Map<IParticleSystem, number>();
|
|
213
240
|
private depth: DepthBinding | null = null;
|
|
214
241
|
/** Systems in insertion order — iterated every frame, unlike the map. */
|
|
215
242
|
private systems: Array<IParticleSystem> = [];
|
|
@@ -272,8 +299,12 @@ export class BatchedRenderer extends TransformNode {
|
|
|
272
299
|
*/
|
|
273
300
|
simulationBudgetMs = Infinity;
|
|
274
301
|
|
|
275
|
-
/** Frustum planes of
|
|
276
|
-
private
|
|
302
|
+
/** Frustum planes of each active camera, six per camera, reused across updates. */
|
|
303
|
+
private readonly cameraPlanes: Plane[][] = [];
|
|
304
|
+
/** Each active camera's `layerMask`, parallel to {@link cameraPlanes}. */
|
|
305
|
+
private readonly cameraMasks: number[] = [];
|
|
306
|
+
/** How many cameras {@link cameraPlanes} holds this update. */
|
|
307
|
+
private cameraCount = 0;
|
|
277
308
|
private readonly boundsMin = new Vector3();
|
|
278
309
|
private readonly boundsMax = new Vector3();
|
|
279
310
|
|
|
@@ -282,21 +313,16 @@ export class BatchedRenderer extends TransformNode {
|
|
|
282
313
|
}
|
|
283
314
|
|
|
284
315
|
/**
|
|
285
|
-
* Gathers the frustum planes of the cameras the scene renders with,
|
|
286
|
-
*
|
|
316
|
+
* Gathers the frustum planes and layer masks of the cameras the scene renders with, and returns
|
|
317
|
+
* how many. 0 when there is no camera yet; systems are then never culled. Allocates only when a
|
|
318
|
+
* camera is added.
|
|
287
319
|
*/
|
|
288
|
-
private
|
|
320
|
+
private collectCameras(): number {
|
|
289
321
|
const scene = this.getScene();
|
|
290
|
-
const planes = this.frustumPlanes;
|
|
291
322
|
const activeCameras = scene.activeCameras;
|
|
292
323
|
const activeCamera = scene.activeCamera;
|
|
293
|
-
const cameras: Camera[] =
|
|
324
|
+
const cameras: readonly Camera[] =
|
|
294
325
|
activeCameras && activeCameras.length > 0 ? activeCameras : activeCamera ? [activeCamera] : [];
|
|
295
|
-
const needed = cameras.length * 6;
|
|
296
|
-
while (planes.length < needed) {
|
|
297
|
-
planes.push(new Plane(0, 0, 0, 0));
|
|
298
|
-
}
|
|
299
|
-
planes.length = needed;
|
|
300
326
|
for (let c = 0; c < cameras.length; c++) {
|
|
301
327
|
const camera = cameras[c];
|
|
302
328
|
// Both matrices are recomputed only on demand: a camera moved since
|
|
@@ -304,31 +330,33 @@ export class BatchedRenderer extends TransformNode {
|
|
|
304
330
|
// that has not rendered yet has no projection at all.
|
|
305
331
|
camera.getViewMatrix();
|
|
306
332
|
camera.getProjectionMatrix();
|
|
307
|
-
const
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
if (c !== 0) {
|
|
311
|
-
for (let p = 0; p < 6; p++) {
|
|
312
|
-
planes[c * 6 + p] = slice[p];
|
|
313
|
-
}
|
|
314
|
-
}
|
|
333
|
+
const planes = (this.cameraPlanes[c] ??= [0, 1, 2, 3, 4, 5].map(() => new Plane(0, 0, 0, 0)));
|
|
334
|
+
Frustum.GetPlanesToRef(camera.getTransformationMatrix(), planes);
|
|
335
|
+
this.cameraMasks[c] = camera.layerMask;
|
|
315
336
|
}
|
|
316
|
-
|
|
337
|
+
this.cameraCount = cameras.length;
|
|
338
|
+
return cameras.length;
|
|
317
339
|
}
|
|
318
340
|
|
|
319
341
|
/**
|
|
320
|
-
* True when `system`'s world bounds reach into at least one camera
|
|
321
|
-
*
|
|
342
|
+
* True when `system`'s world bounds reach into the frustum of at least one camera whose
|
|
343
|
+
* `layerMask` shares a bit with the system's (a camera that does not render the system's layer
|
|
344
|
+
* does not see it), or when it has no bounds to test (empty, or not yet measured).
|
|
322
345
|
*/
|
|
323
|
-
private isSystemInView(system: ParticleSystem
|
|
346
|
+
private isSystemInView(system: ParticleSystem): boolean {
|
|
324
347
|
const min = this.boundsMin;
|
|
325
348
|
const max = this.boundsMax;
|
|
326
349
|
if (!system.getWorldBounds(min, max)) {
|
|
327
350
|
return true;
|
|
328
351
|
}
|
|
329
|
-
|
|
352
|
+
const layerMask = system.layers.mask;
|
|
353
|
+
for (let c = 0; c < this.cameraCount; c++) {
|
|
354
|
+
if ((this.cameraMasks[c] & layerMask) === 0) {
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
const planes = this.cameraPlanes[c];
|
|
330
358
|
let inside = true;
|
|
331
|
-
for (let p =
|
|
359
|
+
for (let p = 0; p < 6; p++) {
|
|
332
360
|
const plane = planes[p];
|
|
333
361
|
const n = plane.normal;
|
|
334
362
|
// The box corner farthest along the plane's normal: if even it
|
|
@@ -361,6 +389,10 @@ export class BatchedRenderer extends TransformNode {
|
|
|
361
389
|
throw new Error('BatchedRenderer.addSystem: the renderer has been disposed');
|
|
362
390
|
}
|
|
363
391
|
const particleSystem = system as ParticleSystem;
|
|
392
|
+
if (particleSystem.emitter.getScene() !== this.getScene()) {
|
|
393
|
+
// Its batch would hold meshes and materials of two scenes.
|
|
394
|
+
throw new Error('BatchedRenderer.addSystem: the system belongs to another scene');
|
|
395
|
+
}
|
|
364
396
|
const previous = particleSystem._renderer;
|
|
365
397
|
if (previous !== undefined && previous !== this) {
|
|
366
398
|
// One renderer at a time, or two would step and draw it.
|
|
@@ -385,10 +417,10 @@ export class BatchedRenderer extends TransformNode {
|
|
|
385
417
|
const system = particleSystem as IParticleSystem;
|
|
386
418
|
const settings = particleSystem.getRendererSettings();
|
|
387
419
|
const stored = storedBatchSettings(settings);
|
|
388
|
-
for (let i = 0; i < this.
|
|
389
|
-
if (batchSettingsEqual(this.
|
|
390
|
-
this.
|
|
391
|
-
this.
|
|
420
|
+
for (let i = 0; i < this.batchList.length; i++) {
|
|
421
|
+
if (batchSettingsEqual(this.batchList[i].settings, stored)) {
|
|
422
|
+
this.batchList[i].addSystem(system);
|
|
423
|
+
this.batchIndexOf.set(system, i);
|
|
392
424
|
return;
|
|
393
425
|
}
|
|
394
426
|
}
|
|
@@ -409,12 +441,12 @@ export class BatchedRenderer extends TransformNode {
|
|
|
409
441
|
default:
|
|
410
442
|
throw new Error(`Unsupported render mode: ${settings.renderMode}`);
|
|
411
443
|
}
|
|
412
|
-
batch.
|
|
444
|
+
batch.attachTo(this);
|
|
413
445
|
batch.applyMeshLighting(this.meshLighting);
|
|
414
446
|
batch.addSystem(system);
|
|
415
|
-
this.
|
|
447
|
+
this.batchList.push(batch);
|
|
416
448
|
this.batchEmptyFrames.push(0);
|
|
417
|
-
this.
|
|
449
|
+
this.batchIndexOf.set(system, this.batchList.length - 1);
|
|
418
450
|
}
|
|
419
451
|
|
|
420
452
|
/**
|
|
@@ -440,12 +472,12 @@ export class BatchedRenderer extends TransformNode {
|
|
|
440
472
|
|
|
441
473
|
/** Detaches a system from its batch, leaving the renderer's system list alone. */
|
|
442
474
|
private removeFromBatch(system: IParticleSystem): boolean {
|
|
443
|
-
const batchIndex = this.
|
|
475
|
+
const batchIndex = this.batchIndexOf.get(system);
|
|
444
476
|
if (batchIndex === undefined) {
|
|
445
477
|
return false;
|
|
446
478
|
}
|
|
447
|
-
this.
|
|
448
|
-
this.
|
|
479
|
+
this.batchList[batchIndex].removeSystem(system);
|
|
480
|
+
this.batchIndexOf.delete(system);
|
|
449
481
|
return true;
|
|
450
482
|
}
|
|
451
483
|
|
|
@@ -456,16 +488,16 @@ export class BatchedRenderer extends TransformNode {
|
|
|
456
488
|
* system keeps its place in the update order, so this is safe to call mid-frame.
|
|
457
489
|
*/
|
|
458
490
|
updateSystem(system: IParticleSystem) {
|
|
459
|
-
const batchIndex = this.
|
|
491
|
+
const batchIndex = this.batchIndexOf.get(system);
|
|
460
492
|
if (batchIndex !== undefined) {
|
|
461
|
-
const batch = this.
|
|
493
|
+
const batch = this.batchList[batchIndex];
|
|
462
494
|
const settings = storedBatchSettings((system as ParticleSystem).getRendererSettings());
|
|
463
495
|
if (
|
|
464
496
|
batch.systems.size === 1 &&
|
|
465
497
|
shaderKeyEqual(batch.settings, settings) &&
|
|
466
498
|
// A batch that already draws with these values takes the system instead, so two
|
|
467
499
|
// batches never hold the same settings.
|
|
468
|
-
!this.
|
|
500
|
+
!this.batchList.some((other) => other !== batch && batchSettingsEqual(other.settings, settings))
|
|
469
501
|
) {
|
|
470
502
|
batch.applyLiveSettings(settings);
|
|
471
503
|
return;
|
|
@@ -491,7 +523,7 @@ export class BatchedRenderer extends TransformNode {
|
|
|
491
523
|
*/
|
|
492
524
|
setDepthTexture(depth: BaseTexture | DepthRenderer | null, options: DepthTextureOptions = {}) {
|
|
493
525
|
this.depth = depth ? depthBinding(depth, options) : null;
|
|
494
|
-
for (const batch of this.
|
|
526
|
+
for (const batch of this.batchList) {
|
|
495
527
|
batch.applyDepthTexture(this.depth);
|
|
496
528
|
}
|
|
497
529
|
}
|
|
@@ -532,7 +564,7 @@ export class BatchedRenderer extends TransformNode {
|
|
|
532
564
|
if (lighting.ambient) {
|
|
533
565
|
current.ambient = lighting.ambient.clone();
|
|
534
566
|
}
|
|
535
|
-
for (const batch of this.
|
|
567
|
+
for (const batch of this.batchList) {
|
|
536
568
|
batch.applyMeshLighting(current);
|
|
537
569
|
}
|
|
538
570
|
}
|
|
@@ -554,7 +586,7 @@ export class BatchedRenderer extends TransformNode {
|
|
|
554
586
|
*/
|
|
555
587
|
configureAdaptivePerformance(options: Partial<AdaptivePerformanceOptions> = {}): void {
|
|
556
588
|
const state = this.adaptivePerformanceState;
|
|
557
|
-
const minQuality = BatchedRenderer.clamp(options.minQuality ?? state.minQuality, 0.
|
|
589
|
+
const minQuality = BatchedRenderer.clamp(options.minQuality ?? state.minQuality, 0.1, 1);
|
|
558
590
|
const maxQuality = BatchedRenderer.clamp(options.maxQuality ?? state.maxQuality, minQuality, 1);
|
|
559
591
|
state.targetFrameMs = Math.max(options.targetFrameMs ?? state.targetFrameMs, 0.1);
|
|
560
592
|
state.minQuality = minQuality;
|
|
@@ -619,7 +651,9 @@ export class BatchedRenderer extends TransformNode {
|
|
|
619
651
|
if (behavior.type !== 'EmitSubParticleSystem') {
|
|
620
652
|
continue;
|
|
621
653
|
}
|
|
622
|
-
|
|
654
|
+
// A uuid string is a link the loader could not resolve: no target.
|
|
655
|
+
const sub = (behavior as EmitSubParticleSystem).subParticleSystem;
|
|
656
|
+
const target = typeof sub === 'object' ? sub.system : undefined;
|
|
623
657
|
if (target !== undefined && known.has(target)) {
|
|
624
658
|
targets.push(target);
|
|
625
659
|
}
|
|
@@ -656,19 +690,13 @@ export class BatchedRenderer extends TransformNode {
|
|
|
656
690
|
*/
|
|
657
691
|
update(delta: number): void {
|
|
658
692
|
this.elapsed += delta;
|
|
659
|
-
if (!this.warnedAboutTransform && !this.getWorldMatrix().isIdentity()) {
|
|
660
|
-
this.warnedAboutTransform = true;
|
|
661
|
-
console.warn(
|
|
662
|
-
`[babylon.quarks] BatchedRenderer "${this.name}" is transformed; particles are placed in the world by their emitters, so the renderer's own transform moves them again. Keep the renderer at the origin.`
|
|
663
|
-
);
|
|
664
|
-
}
|
|
665
693
|
const adaptiveState = this.adaptivePerformanceState;
|
|
666
694
|
const frameStart = adaptiveState.enabled ? performance.now() : 0;
|
|
667
695
|
if (adaptiveState.enabled) {
|
|
668
696
|
this.applyAdaptiveQuality();
|
|
669
697
|
}
|
|
670
698
|
const systems = this.orderSystems();
|
|
671
|
-
const
|
|
699
|
+
const cameraCount = this.cullingEnabled ? this.collectCameras() : 0;
|
|
672
700
|
const budgetStart =
|
|
673
701
|
this.simulationBudgetMs !== Infinity || this.catchUpBudgetMs !== Infinity ? performance.now() : 0;
|
|
674
702
|
const stepDeadline = this.simulationBudgetMs === Infinity ? Infinity : budgetStart + this.simulationBudgetMs;
|
|
@@ -682,14 +710,13 @@ export class BatchedRenderer extends TransformNode {
|
|
|
682
710
|
// A system no camera sees pauses, by its own choice. One that
|
|
683
711
|
// only serves as a sub emitter target follows its parent.
|
|
684
712
|
system.culled =
|
|
685
|
-
|
|
686
|
-
planes.length > 0 &&
|
|
713
|
+
cameraCount > 0 &&
|
|
687
714
|
system.cullingMode !== 'always' &&
|
|
688
715
|
system.onlyUsedByOther !== true &&
|
|
689
|
-
!this.isSystemInView(system
|
|
716
|
+
!this.isSystemInView(system);
|
|
690
717
|
system.update(delta, stepDeadline, catchUpDeadline);
|
|
691
718
|
}
|
|
692
|
-
//
|
|
719
|
+
// A system whose stop action is destroy disposes itself from inside update and drops
|
|
693
720
|
// out of the list; stay on the slot it vacated.
|
|
694
721
|
if (systems[i] !== system) {
|
|
695
722
|
i--;
|
|
@@ -719,8 +746,8 @@ export class BatchedRenderer extends TransformNode {
|
|
|
719
746
|
/** Rebuild instance buffers from current particle state without advancing simulation. */
|
|
720
747
|
refreshBatches(): void {
|
|
721
748
|
const emptyFrames = this.batchEmptyFrames;
|
|
722
|
-
for (let i = 0; i < this.
|
|
723
|
-
const batch = this.
|
|
749
|
+
for (let i = 0; i < this.batchList.length; i++) {
|
|
750
|
+
const batch = this.batchList[i];
|
|
724
751
|
batch.time = this.elapsed;
|
|
725
752
|
if (batch.systems.size > 0) {
|
|
726
753
|
emptyFrames[i] = 0;
|
|
@@ -744,12 +771,12 @@ export class BatchedRenderer extends TransformNode {
|
|
|
744
771
|
|
|
745
772
|
/** Disposes the batch at `index` and repairs the system→batch mapping. */
|
|
746
773
|
private removeBatch(index: number): void {
|
|
747
|
-
this.
|
|
748
|
-
this.
|
|
774
|
+
this.batchList[index].dispose();
|
|
775
|
+
this.batchList.splice(index, 1);
|
|
749
776
|
this.batchEmptyFrames.splice(index, 1);
|
|
750
|
-
for (const [system, batchIndex] of this.
|
|
777
|
+
for (const [system, batchIndex] of this.batchIndexOf) {
|
|
751
778
|
if (batchIndex > index) {
|
|
752
|
-
this.
|
|
779
|
+
this.batchIndexOf.set(system, batchIndex - 1);
|
|
753
780
|
}
|
|
754
781
|
}
|
|
755
782
|
}
|
|
@@ -767,7 +794,7 @@ export class BatchedRenderer extends TransformNode {
|
|
|
767
794
|
/** The emitter names of the batch's systems. */
|
|
768
795
|
systemNames: string[];
|
|
769
796
|
}> {
|
|
770
|
-
return this.
|
|
797
|
+
return this.batchList.map((batch, batchIndex) => {
|
|
771
798
|
let particleCount = 0;
|
|
772
799
|
const systemNames: string[] = [];
|
|
773
800
|
for (const system of batch.systems) {
|
|
@@ -790,10 +817,10 @@ export class BatchedRenderer extends TransformNode {
|
|
|
790
817
|
* their particles and can be added to another renderer. The renderer cannot be used again.
|
|
791
818
|
*/
|
|
792
819
|
dispose(): void {
|
|
793
|
-
for (const batch of this.
|
|
820
|
+
for (const batch of this.batchList) {
|
|
794
821
|
batch.dispose();
|
|
795
822
|
}
|
|
796
|
-
this.
|
|
823
|
+
this.batchList.length = 0;
|
|
797
824
|
this.batchEmptyFrames.length = 0;
|
|
798
825
|
for (const system of this.systems) {
|
|
799
826
|
if ((system as ParticleSystem)._renderer === this) {
|
|
@@ -803,7 +830,7 @@ export class BatchedRenderer extends TransformNode {
|
|
|
803
830
|
this.systems.length = 0;
|
|
804
831
|
this.orderedSystems = [];
|
|
805
832
|
this.orderedSystemsStale = true;
|
|
806
|
-
this.
|
|
833
|
+
this.batchIndexOf.clear();
|
|
807
834
|
super.dispose();
|
|
808
835
|
}
|
|
809
836
|
}
|
package/src/EffectPool.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {Vector3} from '@babylonjs/core/Maths/math.vector';
|
|
1
|
+
import {Quaternion, Vector3} from '@babylonjs/core/Maths/math.vector';
|
|
2
2
|
import {TransformNode} from '@babylonjs/core/Meshes/transformNode';
|
|
3
3
|
import {Node} from '@babylonjs/core/node';
|
|
4
4
|
import {BatchedRenderer} from './BatchedRenderer';
|
|
@@ -16,6 +16,31 @@ export interface EffectPoolOptions {
|
|
|
16
16
|
maxSize?: number;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Where {@link EffectPool.acquire} puts a copy. Each part left out is the template's own, as the
|
|
21
|
+
* copy was made, so nothing carries over from the copy's previous use.
|
|
22
|
+
*/
|
|
23
|
+
export interface EffectPlacement {
|
|
24
|
+
/** Local position under {@link parent}. */
|
|
25
|
+
position?: Vector3;
|
|
26
|
+
/** Local rotation as Euler angles in radians; ignored when {@link rotationQuaternion} is set. */
|
|
27
|
+
rotation?: Vector3;
|
|
28
|
+
/** Local rotation as a quaternion, for a direction: a hit normal, a muzzle's facing. */
|
|
29
|
+
rotationQuaternion?: Quaternion;
|
|
30
|
+
/** Local scale. */
|
|
31
|
+
scaling?: Vector3;
|
|
32
|
+
/** The node the copy is parented to; none (world space) by default. */
|
|
33
|
+
parent?: Node | null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** The local transform a copy is put back to on every acquire. */
|
|
37
|
+
interface RestTransform {
|
|
38
|
+
position: Vector3;
|
|
39
|
+
rotation: Vector3;
|
|
40
|
+
rotationQuaternion: Quaternion | null;
|
|
41
|
+
scaling: Vector3;
|
|
42
|
+
}
|
|
43
|
+
|
|
19
44
|
/**
|
|
20
45
|
* A pool of copies of one effect, for effects played many times over: hits,
|
|
21
46
|
* footsteps, muzzle flashes, pickups.
|
|
@@ -24,11 +49,16 @@ export interface EffectPoolOptions {
|
|
|
24
49
|
* it; a copy whose every system has finished goes back to the pool by itself,
|
|
25
50
|
* so a one-shot effect needs nothing more. A looping effect is ended with
|
|
26
51
|
* {@link stop}, which lets its particles die out before the copy returns, or
|
|
27
|
-
* dropped at once with {@link release}.
|
|
52
|
+
* dropped at once with {@link release}. A ring buffer effect (`ringBufferMode`)
|
|
53
|
+
* never finishes, since its particles never die: release it.
|
|
28
54
|
*
|
|
29
55
|
* Idle copies are parked disabled, which costs the renderer nothing, and keep
|
|
30
56
|
* their place in its batches, so acquiring one allocates nothing.
|
|
31
57
|
*
|
|
58
|
+
* A copy disposed from outside (by its own `dispose()`, or with a parent it
|
|
59
|
+
* was acquired under) leaves the pool on its own, and the pool makes a new
|
|
60
|
+
* one when it needs it.
|
|
61
|
+
*
|
|
32
62
|
* The template itself is never played by the pool; it is only cloned. Disable
|
|
33
63
|
* it, or keep it out of the renderer, if it is not meant to be seen.
|
|
34
64
|
*/
|
|
@@ -36,6 +66,7 @@ export class EffectPool {
|
|
|
36
66
|
private readonly idle: TransformNode[] = [];
|
|
37
67
|
private readonly playing = new Set<TransformNode>();
|
|
38
68
|
private readonly systemsOf = new Map<TransformNode, ParticleSystem[]>();
|
|
69
|
+
private readonly restOf = new Map<TransformNode, RestTransform>();
|
|
39
70
|
private disposed = false;
|
|
40
71
|
|
|
41
72
|
constructor(
|
|
@@ -63,10 +94,15 @@ export class EffectPool {
|
|
|
63
94
|
|
|
64
95
|
/**
|
|
65
96
|
* Takes an idle copy (making one when none is idle and the pool is under
|
|
66
|
-
* its size),
|
|
67
|
-
*
|
|
97
|
+
* its size), places it and starts it from the beginning. The copy's
|
|
98
|
+
* transform is put back to the template's first, so a rotation or scale
|
|
99
|
+
* from its last use does not stay. Null when the pool is full or disposed.
|
|
100
|
+
*
|
|
101
|
+
* @param placement - Where to put the copy, or just its position.
|
|
102
|
+
* @param parent - The node to parent it to, when `placement` is a position;
|
|
103
|
+
* ignored otherwise (use {@link EffectPlacement.parent}).
|
|
68
104
|
*/
|
|
69
|
-
acquire(
|
|
105
|
+
acquire(placement?: Vector3 | EffectPlacement, parent: Node | null = null): TransformNode | null {
|
|
70
106
|
if (this.disposed) {
|
|
71
107
|
return null;
|
|
72
108
|
}
|
|
@@ -74,10 +110,12 @@ export class EffectPool {
|
|
|
74
110
|
if (copy === null) {
|
|
75
111
|
return null;
|
|
76
112
|
}
|
|
77
|
-
copy
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
113
|
+
// Told apart by shape, not class: a vector from another copy of Babylon is still a position.
|
|
114
|
+
const place: EffectPlacement =
|
|
115
|
+
placement === undefined || typeof (placement as Vector3).x === 'number'
|
|
116
|
+
? {position: placement as Vector3 | undefined, parent}
|
|
117
|
+
: (placement as EffectPlacement);
|
|
118
|
+
this.placeCopy(copy, place);
|
|
81
119
|
copy.setEnabled(true);
|
|
82
120
|
this.playing.add(copy);
|
|
83
121
|
QuarksUtil.restart(copy);
|
|
@@ -100,6 +138,10 @@ export class EffectPool {
|
|
|
100
138
|
if (!this.playing.delete(copy)) {
|
|
101
139
|
return;
|
|
102
140
|
}
|
|
141
|
+
if (copy.isDisposed()) {
|
|
142
|
+
this.forget(copy);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
103
145
|
QuarksUtil.stop(copy);
|
|
104
146
|
copy.setEnabled(false);
|
|
105
147
|
copy.parent = null;
|
|
@@ -127,28 +169,66 @@ export class EffectPool {
|
|
|
127
169
|
const systems: ParticleSystem[] = [];
|
|
128
170
|
QuarksUtil.runOnAllParticleEmitters(copy, (emitter) => {
|
|
129
171
|
const system = emitter.system as unknown as ParticleSystem;
|
|
130
|
-
// The pool owns the copy's life; a system disposing itself
|
|
131
|
-
// pull one emitter out of an effect that is meant to play again.
|
|
132
|
-
system.
|
|
172
|
+
// The pool owns the copy's life; a system disposing or disabling itself
|
|
173
|
+
// would pull one emitter out of an effect that is meant to play again.
|
|
174
|
+
system.ignoreStopAction = true;
|
|
133
175
|
system.addEventListener('finished', this.onSystemFinished);
|
|
134
176
|
systems.push(system);
|
|
135
177
|
});
|
|
136
178
|
this.systemsOf.set(copy, systems);
|
|
179
|
+
this.restOf.set(copy, {
|
|
180
|
+
position: copy.position.clone(),
|
|
181
|
+
rotation: copy.rotation.clone(),
|
|
182
|
+
rotationQuaternion: copy.rotationQuaternion?.clone() ?? null,
|
|
183
|
+
scaling: copy.scaling.clone(),
|
|
184
|
+
});
|
|
137
185
|
QuarksUtil.addToBatchRenderer(copy, this.options.renderer);
|
|
138
186
|
copy.setEnabled(false);
|
|
139
187
|
copy.parent = null;
|
|
188
|
+
// A copy disposed from outside, or with a parent it was placed under, is let go of.
|
|
189
|
+
copy.onDisposeObservable.addOnce(() => this.forget(copy));
|
|
140
190
|
return copy;
|
|
141
191
|
}
|
|
142
192
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
193
|
+
/** Puts the copy back to the template's transform, then applies `place`. */
|
|
194
|
+
private placeCopy(copy: TransformNode, place: EffectPlacement): void {
|
|
195
|
+
const rest = this.restOf.get(copy)!;
|
|
196
|
+
copy.parent = place.parent ?? null;
|
|
197
|
+
// By components: Babylon's copyFrom reads private fields, which a vector from elsewhere lacks.
|
|
198
|
+
const position = place.position ?? rest.position;
|
|
199
|
+
const scaling = place.scaling ?? rest.scaling;
|
|
200
|
+
copy.position.copyFromFloats(position.x, position.y, position.z);
|
|
201
|
+
copy.scaling.copyFromFloats(scaling.x, scaling.y, scaling.z);
|
|
202
|
+
if (place.rotationQuaternion !== undefined) {
|
|
203
|
+
copy.rotationQuaternion = place.rotationQuaternion.clone();
|
|
204
|
+
} else if (place.rotation !== undefined) {
|
|
205
|
+
copy.rotationQuaternion = null;
|
|
206
|
+
copy.rotation.copyFromFloats(place.rotation.x, place.rotation.y, place.rotation.z);
|
|
207
|
+
} else {
|
|
208
|
+
copy.rotationQuaternion = rest.rotationQuaternion?.clone() ?? null;
|
|
209
|
+
copy.rotation.copyFrom(rest.rotation);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** Drops every trace of a copy, which was or is being disposed. */
|
|
214
|
+
private forget(copy: TransformNode): void {
|
|
215
|
+
for (const system of this.systemsOf.get(copy) ?? []) {
|
|
146
216
|
system.removeEventListener('finished', this.onSystemFinished);
|
|
147
217
|
this.options.renderer.deleteSystem(system);
|
|
148
218
|
system.dispose();
|
|
149
219
|
}
|
|
150
220
|
this.systemsOf.delete(copy);
|
|
151
|
-
|
|
221
|
+
this.restOf.delete(copy);
|
|
222
|
+
this.playing.delete(copy);
|
|
223
|
+
const index = this.idle.indexOf(copy);
|
|
224
|
+
if (index !== -1) {
|
|
225
|
+
this.idle.splice(index, 1);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
private disposeCopy(copy: TransformNode): void {
|
|
230
|
+
this.forget(copy);
|
|
231
|
+
// Lets go of the textures and materials the copy shares with the template.
|
|
152
232
|
disposeEffect(copy);
|
|
153
233
|
}
|
|
154
234
|
|