babylon.quarks 0.27.0 → 0.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +425 -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 +4256 -831
- package/dist/types/index.d.ts +4256 -831
- package/package.json +7 -2
- package/src/BatchedRenderer.ts +93 -78
- package/src/EffectPool.ts +91 -12
- package/src/MeshSurfaceEmitter.ts +24 -13
- package/src/ParticleEmitter.ts +31 -3
- package/src/ParticleSystem.ts +92 -25
- package/src/QuarksLoader.ts +289 -171
- package/src/QuarksPrefab.ts +301 -94
- package/src/QuarksSerializer.ts +43 -15
- package/src/QuarksUtil.ts +64 -9
- package/src/SpriteBatch.ts +21 -6
- package/src/TrailBatch.ts +12 -6
- package/src/VFXBatch.ts +70 -528
- package/src/batchKey.ts +4 -2
- package/src/batchSettings.ts +546 -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 +39 -4
- package/src/nodeId.ts +25 -2
- package/src/shaderProgram.ts +110 -16
- package/src/shaderVariants.ts +24 -4
- package/src/shaders/chunks/blend.ts +31 -0
- package/src/shaders/chunks/fog.ts +8 -3
- package/src/shaders/chunks/soft.ts +6 -5
- package/src/shaders/chunks/surface.ts +2 -2
- package/src/shaders/particle_frag.glsl.ts +3 -1
- package/src/shaders/particle_frag.wgsl.ts +2 -0
- package/src/shaders/particle_physics_frag.glsl.ts +5 -2
- package/src/shaders/particle_physics_frag.wgsl.ts +2 -0
- package/src/shaders/trail_frag.glsl.ts +4 -1
- package/src/shaders/trail_frag.wgsl.ts +2 -0
- package/src/system/EmissionController.ts +2 -4
- package/src/system/EventDispatcher.ts +26 -10
- package/src/system/geometry.ts +22 -29
- package/src/system/materialResolve.ts +56 -7
- package/src/system/normalize.ts +6 -1
- package/src/system/serialization.ts +54 -33
- package/src/system/settingsSchema.ts +32 -10
- package/src/system/types.ts +67 -28
- 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.28.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,11 +109,13 @@ 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;
|
|
118
121
|
/**
|
|
@@ -125,6 +128,11 @@ export interface VFXBatchSettings {
|
|
|
125
128
|
pivot?: Vector3;
|
|
126
129
|
/** Draw order within the batch, see {@link SortMode}. Unset is `'none'`. Systems that differ are batched apart. */
|
|
127
130
|
sortMode?: SortMode;
|
|
131
|
+
/**
|
|
132
|
+
* Which faces a mesh particle draws, see {@link RenderFace}. Unset is `'both'`. Mesh systems
|
|
133
|
+
* that differ are batched apart; other render modes ignore it.
|
|
134
|
+
*/
|
|
135
|
+
renderFace?: RenderFace;
|
|
128
136
|
/**
|
|
129
137
|
* Smallest size a billboard or stretched particle may have on screen, as a
|
|
130
138
|
* fraction of the viewport height (Unity's Min Particle Size); 0 by default.
|
|
@@ -196,20 +204,27 @@ export interface AdaptivePerformanceState extends AdaptivePerformanceOptions {
|
|
|
196
204
|
*
|
|
197
205
|
* Systems step on a fixed 60 Hz clock, so an effect plays out the same at any frame rate.
|
|
198
206
|
*
|
|
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
|
|
207
|
+
* The renderer is a node so the batches have a parent to live under: disabling it hides them,
|
|
208
|
+
* disposing it disposes them. Its transform moves nothing: particles are placed in the world by
|
|
209
|
+
* their emitters, and the batch meshes draw them there whatever the renderer's position, rotation
|
|
210
|
+
* or scale. A system belongs to one renderer at a time: adding it to another takes it out of the
|
|
211
|
+
* first.
|
|
203
212
|
*/
|
|
204
213
|
export class BatchedRenderer extends TransformNode {
|
|
205
|
-
|
|
206
|
-
private
|
|
207
|
-
|
|
208
|
-
batches
|
|
214
|
+
private readonly batchList: Array<VFXBatch> = [];
|
|
215
|
+
private readonly batchIndexOf = new Map<IParticleSystem, number>();
|
|
216
|
+
|
|
217
|
+
/** The batches, one draw each, to inspect what is drawn; the renderer makes and frees them. */
|
|
218
|
+
get batches(): ReadonlyArray<VFXBatch> {
|
|
219
|
+
return this.batchList;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Index into {@link batches} of each system's batch, to inspect; the renderer keeps it. */
|
|
223
|
+
get systemToBatchIndex(): ReadonlyMap<IParticleSystem, number> {
|
|
224
|
+
return this.batchIndexOf;
|
|
225
|
+
}
|
|
209
226
|
/** Seconds this renderer has been updated for; the clock UV and erosion scrolling run on. */
|
|
210
227
|
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
228
|
private depth: DepthBinding | null = null;
|
|
214
229
|
/** Systems in insertion order — iterated every frame, unlike the map. */
|
|
215
230
|
private systems: Array<IParticleSystem> = [];
|
|
@@ -272,8 +287,12 @@ export class BatchedRenderer extends TransformNode {
|
|
|
272
287
|
*/
|
|
273
288
|
simulationBudgetMs = Infinity;
|
|
274
289
|
|
|
275
|
-
/** Frustum planes of
|
|
276
|
-
private
|
|
290
|
+
/** Frustum planes of each active camera, six per camera, reused across updates. */
|
|
291
|
+
private readonly cameraPlanes: Plane[][] = [];
|
|
292
|
+
/** Each active camera's `layerMask`, parallel to {@link cameraPlanes}. */
|
|
293
|
+
private readonly cameraMasks: number[] = [];
|
|
294
|
+
/** How many cameras {@link cameraPlanes} holds this update. */
|
|
295
|
+
private cameraCount = 0;
|
|
277
296
|
private readonly boundsMin = new Vector3();
|
|
278
297
|
private readonly boundsMax = new Vector3();
|
|
279
298
|
|
|
@@ -282,21 +301,16 @@ export class BatchedRenderer extends TransformNode {
|
|
|
282
301
|
}
|
|
283
302
|
|
|
284
303
|
/**
|
|
285
|
-
* Gathers the frustum planes of the cameras the scene renders with,
|
|
286
|
-
*
|
|
304
|
+
* Gathers the frustum planes and layer masks of the cameras the scene renders with, and returns
|
|
305
|
+
* how many. 0 when there is no camera yet; systems are then never culled. Allocates only when a
|
|
306
|
+
* camera is added.
|
|
287
307
|
*/
|
|
288
|
-
private
|
|
308
|
+
private collectCameras(): number {
|
|
289
309
|
const scene = this.getScene();
|
|
290
|
-
const planes = this.frustumPlanes;
|
|
291
310
|
const activeCameras = scene.activeCameras;
|
|
292
311
|
const activeCamera = scene.activeCamera;
|
|
293
|
-
const cameras: Camera[] =
|
|
312
|
+
const cameras: readonly Camera[] =
|
|
294
313
|
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
314
|
for (let c = 0; c < cameras.length; c++) {
|
|
301
315
|
const camera = cameras[c];
|
|
302
316
|
// Both matrices are recomputed only on demand: a camera moved since
|
|
@@ -304,31 +318,33 @@ export class BatchedRenderer extends TransformNode {
|
|
|
304
318
|
// that has not rendered yet has no projection at all.
|
|
305
319
|
camera.getViewMatrix();
|
|
306
320
|
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
|
-
}
|
|
321
|
+
const planes = (this.cameraPlanes[c] ??= [0, 1, 2, 3, 4, 5].map(() => new Plane(0, 0, 0, 0)));
|
|
322
|
+
Frustum.GetPlanesToRef(camera.getTransformationMatrix(), planes);
|
|
323
|
+
this.cameraMasks[c] = camera.layerMask;
|
|
315
324
|
}
|
|
316
|
-
|
|
325
|
+
this.cameraCount = cameras.length;
|
|
326
|
+
return cameras.length;
|
|
317
327
|
}
|
|
318
328
|
|
|
319
329
|
/**
|
|
320
|
-
* True when `system`'s world bounds reach into at least one camera
|
|
321
|
-
*
|
|
330
|
+
* True when `system`'s world bounds reach into the frustum of at least one camera whose
|
|
331
|
+
* `layerMask` shares a bit with the system's (a camera that does not render the system's layer
|
|
332
|
+
* does not see it), or when it has no bounds to test (empty, or not yet measured).
|
|
322
333
|
*/
|
|
323
|
-
private isSystemInView(system: ParticleSystem
|
|
334
|
+
private isSystemInView(system: ParticleSystem): boolean {
|
|
324
335
|
const min = this.boundsMin;
|
|
325
336
|
const max = this.boundsMax;
|
|
326
337
|
if (!system.getWorldBounds(min, max)) {
|
|
327
338
|
return true;
|
|
328
339
|
}
|
|
329
|
-
|
|
340
|
+
const layerMask = system.layers.mask;
|
|
341
|
+
for (let c = 0; c < this.cameraCount; c++) {
|
|
342
|
+
if ((this.cameraMasks[c] & layerMask) === 0) {
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
const planes = this.cameraPlanes[c];
|
|
330
346
|
let inside = true;
|
|
331
|
-
for (let p =
|
|
347
|
+
for (let p = 0; p < 6; p++) {
|
|
332
348
|
const plane = planes[p];
|
|
333
349
|
const n = plane.normal;
|
|
334
350
|
// The box corner farthest along the plane's normal: if even it
|
|
@@ -361,6 +377,10 @@ export class BatchedRenderer extends TransformNode {
|
|
|
361
377
|
throw new Error('BatchedRenderer.addSystem: the renderer has been disposed');
|
|
362
378
|
}
|
|
363
379
|
const particleSystem = system as ParticleSystem;
|
|
380
|
+
if (particleSystem.emitter.getScene() !== this.getScene()) {
|
|
381
|
+
// Its batch would hold meshes and materials of two scenes.
|
|
382
|
+
throw new Error('BatchedRenderer.addSystem: the system belongs to another scene');
|
|
383
|
+
}
|
|
364
384
|
const previous = particleSystem._renderer;
|
|
365
385
|
if (previous !== undefined && previous !== this) {
|
|
366
386
|
// One renderer at a time, or two would step and draw it.
|
|
@@ -385,10 +405,10 @@ export class BatchedRenderer extends TransformNode {
|
|
|
385
405
|
const system = particleSystem as IParticleSystem;
|
|
386
406
|
const settings = particleSystem.getRendererSettings();
|
|
387
407
|
const stored = storedBatchSettings(settings);
|
|
388
|
-
for (let i = 0; i < this.
|
|
389
|
-
if (batchSettingsEqual(this.
|
|
390
|
-
this.
|
|
391
|
-
this.
|
|
408
|
+
for (let i = 0; i < this.batchList.length; i++) {
|
|
409
|
+
if (batchSettingsEqual(this.batchList[i].settings, stored)) {
|
|
410
|
+
this.batchList[i].addSystem(system);
|
|
411
|
+
this.batchIndexOf.set(system, i);
|
|
392
412
|
return;
|
|
393
413
|
}
|
|
394
414
|
}
|
|
@@ -409,12 +429,12 @@ export class BatchedRenderer extends TransformNode {
|
|
|
409
429
|
default:
|
|
410
430
|
throw new Error(`Unsupported render mode: ${settings.renderMode}`);
|
|
411
431
|
}
|
|
412
|
-
batch.
|
|
432
|
+
batch.attachTo(this);
|
|
413
433
|
batch.applyMeshLighting(this.meshLighting);
|
|
414
434
|
batch.addSystem(system);
|
|
415
|
-
this.
|
|
435
|
+
this.batchList.push(batch);
|
|
416
436
|
this.batchEmptyFrames.push(0);
|
|
417
|
-
this.
|
|
437
|
+
this.batchIndexOf.set(system, this.batchList.length - 1);
|
|
418
438
|
}
|
|
419
439
|
|
|
420
440
|
/**
|
|
@@ -440,12 +460,12 @@ export class BatchedRenderer extends TransformNode {
|
|
|
440
460
|
|
|
441
461
|
/** Detaches a system from its batch, leaving the renderer's system list alone. */
|
|
442
462
|
private removeFromBatch(system: IParticleSystem): boolean {
|
|
443
|
-
const batchIndex = this.
|
|
463
|
+
const batchIndex = this.batchIndexOf.get(system);
|
|
444
464
|
if (batchIndex === undefined) {
|
|
445
465
|
return false;
|
|
446
466
|
}
|
|
447
|
-
this.
|
|
448
|
-
this.
|
|
467
|
+
this.batchList[batchIndex].removeSystem(system);
|
|
468
|
+
this.batchIndexOf.delete(system);
|
|
449
469
|
return true;
|
|
450
470
|
}
|
|
451
471
|
|
|
@@ -456,16 +476,16 @@ export class BatchedRenderer extends TransformNode {
|
|
|
456
476
|
* system keeps its place in the update order, so this is safe to call mid-frame.
|
|
457
477
|
*/
|
|
458
478
|
updateSystem(system: IParticleSystem) {
|
|
459
|
-
const batchIndex = this.
|
|
479
|
+
const batchIndex = this.batchIndexOf.get(system);
|
|
460
480
|
if (batchIndex !== undefined) {
|
|
461
|
-
const batch = this.
|
|
481
|
+
const batch = this.batchList[batchIndex];
|
|
462
482
|
const settings = storedBatchSettings((system as ParticleSystem).getRendererSettings());
|
|
463
483
|
if (
|
|
464
484
|
batch.systems.size === 1 &&
|
|
465
485
|
shaderKeyEqual(batch.settings, settings) &&
|
|
466
486
|
// A batch that already draws with these values takes the system instead, so two
|
|
467
487
|
// batches never hold the same settings.
|
|
468
|
-
!this.
|
|
488
|
+
!this.batchList.some((other) => other !== batch && batchSettingsEqual(other.settings, settings))
|
|
469
489
|
) {
|
|
470
490
|
batch.applyLiveSettings(settings);
|
|
471
491
|
return;
|
|
@@ -491,7 +511,7 @@ export class BatchedRenderer extends TransformNode {
|
|
|
491
511
|
*/
|
|
492
512
|
setDepthTexture(depth: BaseTexture | DepthRenderer | null, options: DepthTextureOptions = {}) {
|
|
493
513
|
this.depth = depth ? depthBinding(depth, options) : null;
|
|
494
|
-
for (const batch of this.
|
|
514
|
+
for (const batch of this.batchList) {
|
|
495
515
|
batch.applyDepthTexture(this.depth);
|
|
496
516
|
}
|
|
497
517
|
}
|
|
@@ -532,7 +552,7 @@ export class BatchedRenderer extends TransformNode {
|
|
|
532
552
|
if (lighting.ambient) {
|
|
533
553
|
current.ambient = lighting.ambient.clone();
|
|
534
554
|
}
|
|
535
|
-
for (const batch of this.
|
|
555
|
+
for (const batch of this.batchList) {
|
|
536
556
|
batch.applyMeshLighting(current);
|
|
537
557
|
}
|
|
538
558
|
}
|
|
@@ -554,7 +574,7 @@ export class BatchedRenderer extends TransformNode {
|
|
|
554
574
|
*/
|
|
555
575
|
configureAdaptivePerformance(options: Partial<AdaptivePerformanceOptions> = {}): void {
|
|
556
576
|
const state = this.adaptivePerformanceState;
|
|
557
|
-
const minQuality = BatchedRenderer.clamp(options.minQuality ?? state.minQuality, 0.
|
|
577
|
+
const minQuality = BatchedRenderer.clamp(options.minQuality ?? state.minQuality, 0.1, 1);
|
|
558
578
|
const maxQuality = BatchedRenderer.clamp(options.maxQuality ?? state.maxQuality, minQuality, 1);
|
|
559
579
|
state.targetFrameMs = Math.max(options.targetFrameMs ?? state.targetFrameMs, 0.1);
|
|
560
580
|
state.minQuality = minQuality;
|
|
@@ -619,7 +639,9 @@ export class BatchedRenderer extends TransformNode {
|
|
|
619
639
|
if (behavior.type !== 'EmitSubParticleSystem') {
|
|
620
640
|
continue;
|
|
621
641
|
}
|
|
622
|
-
|
|
642
|
+
// A uuid string is a link the loader could not resolve: no target.
|
|
643
|
+
const sub = (behavior as EmitSubParticleSystem).subParticleSystem;
|
|
644
|
+
const target = typeof sub === 'object' ? sub.system : undefined;
|
|
623
645
|
if (target !== undefined && known.has(target)) {
|
|
624
646
|
targets.push(target);
|
|
625
647
|
}
|
|
@@ -656,19 +678,13 @@ export class BatchedRenderer extends TransformNode {
|
|
|
656
678
|
*/
|
|
657
679
|
update(delta: number): void {
|
|
658
680
|
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
681
|
const adaptiveState = this.adaptivePerformanceState;
|
|
666
682
|
const frameStart = adaptiveState.enabled ? performance.now() : 0;
|
|
667
683
|
if (adaptiveState.enabled) {
|
|
668
684
|
this.applyAdaptiveQuality();
|
|
669
685
|
}
|
|
670
686
|
const systems = this.orderSystems();
|
|
671
|
-
const
|
|
687
|
+
const cameraCount = this.cullingEnabled ? this.collectCameras() : 0;
|
|
672
688
|
const budgetStart =
|
|
673
689
|
this.simulationBudgetMs !== Infinity || this.catchUpBudgetMs !== Infinity ? performance.now() : 0;
|
|
674
690
|
const stepDeadline = this.simulationBudgetMs === Infinity ? Infinity : budgetStart + this.simulationBudgetMs;
|
|
@@ -682,11 +698,10 @@ export class BatchedRenderer extends TransformNode {
|
|
|
682
698
|
// A system no camera sees pauses, by its own choice. One that
|
|
683
699
|
// only serves as a sub emitter target follows its parent.
|
|
684
700
|
system.culled =
|
|
685
|
-
|
|
686
|
-
planes.length > 0 &&
|
|
701
|
+
cameraCount > 0 &&
|
|
687
702
|
system.cullingMode !== 'always' &&
|
|
688
703
|
system.onlyUsedByOther !== true &&
|
|
689
|
-
!this.isSystemInView(system
|
|
704
|
+
!this.isSystemInView(system);
|
|
690
705
|
system.update(delta, stepDeadline, catchUpDeadline);
|
|
691
706
|
}
|
|
692
707
|
// An autoDestroy system disposes itself from inside update and drops
|
|
@@ -719,8 +734,8 @@ export class BatchedRenderer extends TransformNode {
|
|
|
719
734
|
/** Rebuild instance buffers from current particle state without advancing simulation. */
|
|
720
735
|
refreshBatches(): void {
|
|
721
736
|
const emptyFrames = this.batchEmptyFrames;
|
|
722
|
-
for (let i = 0; i < this.
|
|
723
|
-
const batch = this.
|
|
737
|
+
for (let i = 0; i < this.batchList.length; i++) {
|
|
738
|
+
const batch = this.batchList[i];
|
|
724
739
|
batch.time = this.elapsed;
|
|
725
740
|
if (batch.systems.size > 0) {
|
|
726
741
|
emptyFrames[i] = 0;
|
|
@@ -744,12 +759,12 @@ export class BatchedRenderer extends TransformNode {
|
|
|
744
759
|
|
|
745
760
|
/** Disposes the batch at `index` and repairs the system→batch mapping. */
|
|
746
761
|
private removeBatch(index: number): void {
|
|
747
|
-
this.
|
|
748
|
-
this.
|
|
762
|
+
this.batchList[index].dispose();
|
|
763
|
+
this.batchList.splice(index, 1);
|
|
749
764
|
this.batchEmptyFrames.splice(index, 1);
|
|
750
|
-
for (const [system, batchIndex] of this.
|
|
765
|
+
for (const [system, batchIndex] of this.batchIndexOf) {
|
|
751
766
|
if (batchIndex > index) {
|
|
752
|
-
this.
|
|
767
|
+
this.batchIndexOf.set(system, batchIndex - 1);
|
|
753
768
|
}
|
|
754
769
|
}
|
|
755
770
|
}
|
|
@@ -767,7 +782,7 @@ export class BatchedRenderer extends TransformNode {
|
|
|
767
782
|
/** The emitter names of the batch's systems. */
|
|
768
783
|
systemNames: string[];
|
|
769
784
|
}> {
|
|
770
|
-
return this.
|
|
785
|
+
return this.batchList.map((batch, batchIndex) => {
|
|
771
786
|
let particleCount = 0;
|
|
772
787
|
const systemNames: string[] = [];
|
|
773
788
|
for (const system of batch.systems) {
|
|
@@ -790,10 +805,10 @@ export class BatchedRenderer extends TransformNode {
|
|
|
790
805
|
* their particles and can be added to another renderer. The renderer cannot be used again.
|
|
791
806
|
*/
|
|
792
807
|
dispose(): void {
|
|
793
|
-
for (const batch of this.
|
|
808
|
+
for (const batch of this.batchList) {
|
|
794
809
|
batch.dispose();
|
|
795
810
|
}
|
|
796
|
-
this.
|
|
811
|
+
this.batchList.length = 0;
|
|
797
812
|
this.batchEmptyFrames.length = 0;
|
|
798
813
|
for (const system of this.systems) {
|
|
799
814
|
if ((system as ParticleSystem)._renderer === this) {
|
|
@@ -803,7 +818,7 @@ export class BatchedRenderer extends TransformNode {
|
|
|
803
818
|
this.systems.length = 0;
|
|
804
819
|
this.orderedSystems = [];
|
|
805
820
|
this.orderedSystemsStale = true;
|
|
806
|
-
this.
|
|
821
|
+
this.batchIndexOf.clear();
|
|
807
822
|
super.dispose();
|
|
808
823
|
}
|
|
809
824
|
}
|
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.
|
|
@@ -29,6 +54,10 @@ export interface EffectPoolOptions {
|
|
|
29
54
|
* Idle copies are parked disabled, which costs the renderer nothing, and keep
|
|
30
55
|
* their place in its batches, so acquiring one allocates nothing.
|
|
31
56
|
*
|
|
57
|
+
* A copy disposed from outside (by its own `dispose()`, or with a parent it
|
|
58
|
+
* was acquired under) leaves the pool on its own, and the pool makes a new
|
|
59
|
+
* one when it needs it.
|
|
60
|
+
*
|
|
32
61
|
* The template itself is never played by the pool; it is only cloned. Disable
|
|
33
62
|
* it, or keep it out of the renderer, if it is not meant to be seen.
|
|
34
63
|
*/
|
|
@@ -36,6 +65,7 @@ export class EffectPool {
|
|
|
36
65
|
private readonly idle: TransformNode[] = [];
|
|
37
66
|
private readonly playing = new Set<TransformNode>();
|
|
38
67
|
private readonly systemsOf = new Map<TransformNode, ParticleSystem[]>();
|
|
68
|
+
private readonly restOf = new Map<TransformNode, RestTransform>();
|
|
39
69
|
private disposed = false;
|
|
40
70
|
|
|
41
71
|
constructor(
|
|
@@ -63,10 +93,15 @@ export class EffectPool {
|
|
|
63
93
|
|
|
64
94
|
/**
|
|
65
95
|
* Takes an idle copy (making one when none is idle and the pool is under
|
|
66
|
-
* its size),
|
|
67
|
-
*
|
|
96
|
+
* its size), places it and starts it from the beginning. The copy's
|
|
97
|
+
* transform is put back to the template's first, so a rotation or scale
|
|
98
|
+
* from its last use does not stay. Null when the pool is full or disposed.
|
|
99
|
+
*
|
|
100
|
+
* @param placement - Where to put the copy, or just its position.
|
|
101
|
+
* @param parent - The node to parent it to, when `placement` is a position;
|
|
102
|
+
* ignored otherwise (use {@link EffectPlacement.parent}).
|
|
68
103
|
*/
|
|
69
|
-
acquire(
|
|
104
|
+
acquire(placement?: Vector3 | EffectPlacement, parent: Node | null = null): TransformNode | null {
|
|
70
105
|
if (this.disposed) {
|
|
71
106
|
return null;
|
|
72
107
|
}
|
|
@@ -74,10 +109,12 @@ export class EffectPool {
|
|
|
74
109
|
if (copy === null) {
|
|
75
110
|
return null;
|
|
76
111
|
}
|
|
77
|
-
copy
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
112
|
+
// Told apart by shape, not class: a vector from another copy of Babylon is still a position.
|
|
113
|
+
const place: EffectPlacement =
|
|
114
|
+
placement === undefined || typeof (placement as Vector3).x === 'number'
|
|
115
|
+
? {position: placement as Vector3 | undefined, parent}
|
|
116
|
+
: (placement as EffectPlacement);
|
|
117
|
+
this.placeCopy(copy, place);
|
|
81
118
|
copy.setEnabled(true);
|
|
82
119
|
this.playing.add(copy);
|
|
83
120
|
QuarksUtil.restart(copy);
|
|
@@ -100,6 +137,10 @@ export class EffectPool {
|
|
|
100
137
|
if (!this.playing.delete(copy)) {
|
|
101
138
|
return;
|
|
102
139
|
}
|
|
140
|
+
if (copy.isDisposed()) {
|
|
141
|
+
this.forget(copy);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
103
144
|
QuarksUtil.stop(copy);
|
|
104
145
|
copy.setEnabled(false);
|
|
105
146
|
copy.parent = null;
|
|
@@ -134,21 +175,59 @@ export class EffectPool {
|
|
|
134
175
|
systems.push(system);
|
|
135
176
|
});
|
|
136
177
|
this.systemsOf.set(copy, systems);
|
|
178
|
+
this.restOf.set(copy, {
|
|
179
|
+
position: copy.position.clone(),
|
|
180
|
+
rotation: copy.rotation.clone(),
|
|
181
|
+
rotationQuaternion: copy.rotationQuaternion?.clone() ?? null,
|
|
182
|
+
scaling: copy.scaling.clone(),
|
|
183
|
+
});
|
|
137
184
|
QuarksUtil.addToBatchRenderer(copy, this.options.renderer);
|
|
138
185
|
copy.setEnabled(false);
|
|
139
186
|
copy.parent = null;
|
|
187
|
+
// A copy disposed from outside, or with a parent it was placed under, is let go of.
|
|
188
|
+
copy.onDisposeObservable.addOnce(() => this.forget(copy));
|
|
140
189
|
return copy;
|
|
141
190
|
}
|
|
142
191
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
192
|
+
/** Puts the copy back to the template's transform, then applies `place`. */
|
|
193
|
+
private placeCopy(copy: TransformNode, place: EffectPlacement): void {
|
|
194
|
+
const rest = this.restOf.get(copy)!;
|
|
195
|
+
copy.parent = place.parent ?? null;
|
|
196
|
+
// By components: Babylon's copyFrom reads private fields, which a vector from elsewhere lacks.
|
|
197
|
+
const position = place.position ?? rest.position;
|
|
198
|
+
const scaling = place.scaling ?? rest.scaling;
|
|
199
|
+
copy.position.copyFromFloats(position.x, position.y, position.z);
|
|
200
|
+
copy.scaling.copyFromFloats(scaling.x, scaling.y, scaling.z);
|
|
201
|
+
if (place.rotationQuaternion !== undefined) {
|
|
202
|
+
copy.rotationQuaternion = place.rotationQuaternion.clone();
|
|
203
|
+
} else if (place.rotation !== undefined) {
|
|
204
|
+
copy.rotationQuaternion = null;
|
|
205
|
+
copy.rotation.copyFromFloats(place.rotation.x, place.rotation.y, place.rotation.z);
|
|
206
|
+
} else {
|
|
207
|
+
copy.rotationQuaternion = rest.rotationQuaternion?.clone() ?? null;
|
|
208
|
+
copy.rotation.copyFrom(rest.rotation);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** Drops every trace of a copy, which was or is being disposed. */
|
|
213
|
+
private forget(copy: TransformNode): void {
|
|
214
|
+
for (const system of this.systemsOf.get(copy) ?? []) {
|
|
146
215
|
system.removeEventListener('finished', this.onSystemFinished);
|
|
147
216
|
this.options.renderer.deleteSystem(system);
|
|
148
217
|
system.dispose();
|
|
149
218
|
}
|
|
150
219
|
this.systemsOf.delete(copy);
|
|
151
|
-
|
|
220
|
+
this.restOf.delete(copy);
|
|
221
|
+
this.playing.delete(copy);
|
|
222
|
+
const index = this.idle.indexOf(copy);
|
|
223
|
+
if (index !== -1) {
|
|
224
|
+
this.idle.splice(index, 1);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
private disposeCopy(copy: TransformNode): void {
|
|
229
|
+
this.forget(copy);
|
|
230
|
+
// Lets go of the textures and materials the copy shares with the template.
|
|
152
231
|
disposeEffect(copy);
|
|
153
232
|
}
|
|
154
233
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {VertexBuffer} from '@babylonjs/core/Buffers/buffer';
|
|
2
2
|
import {Mesh} from '@babylonjs/core/Meshes/mesh';
|
|
3
3
|
import {EmitterShape, IParticleSystem, Particle, Plugin, random, ShapeJSON, ShapeTransform, Vector3} from 'quarks.core';
|
|
4
|
+
import {ensureTriangleIndices} from './geometryUtil';
|
|
4
5
|
import {quarksNodeId} from './nodeId';
|
|
5
6
|
|
|
6
7
|
/**
|
|
@@ -8,8 +9,9 @@ import {quarksNodeId} from './nodeId';
|
|
|
8
9
|
* Location): a point picked at random over the triangles, weighted by their area, moving along the
|
|
9
10
|
* triangle's normal at the particle's start speed. The mesh's vertices are read once, when it is
|
|
10
11
|
* set, in the mesh's own space, and laid in the emitter's space as they are, moved by
|
|
11
|
-
* {@link transform}; moving the mesh node afterwards does not move the shape. A
|
|
12
|
-
* mesh emits from the origin
|
|
12
|
+
* {@link transform}; moving the mesh node afterwards does not move the shape. A mesh without an
|
|
13
|
+
* index buffer is read as a plain triangle list. A shape without a mesh emits from the origin
|
|
14
|
+
* along +Z.
|
|
13
15
|
*/
|
|
14
16
|
export class MeshSurfaceEmitter implements EmitterShape {
|
|
15
17
|
/** The shape's type in effect files. */
|
|
@@ -23,7 +25,11 @@ export class MeshSurfaceEmitter implements EmitterShape {
|
|
|
23
25
|
/** Offset, rotation and scale of the mesh within the particle system. */
|
|
24
26
|
transform?: ShapeTransform;
|
|
25
27
|
|
|
26
|
-
/**
|
|
28
|
+
/**
|
|
29
|
+
* The mesh emitted from. Setting it reads its positions and indices; a mesh without indices is
|
|
30
|
+
* read as a triangle list (vertices 0-1-2, 3-4-5, ...), and a mesh without positions, or with
|
|
31
|
+
* no whole triangle, emits from the origin.
|
|
32
|
+
*/
|
|
27
33
|
get mesh(): Mesh | undefined {
|
|
28
34
|
return this._mesh;
|
|
29
35
|
}
|
|
@@ -36,20 +42,26 @@ export class MeshSurfaceEmitter implements EmitterShape {
|
|
|
36
42
|
if (!mesh) return;
|
|
37
43
|
|
|
38
44
|
const positions = mesh.getVerticesData(VertexBuffer.PositionKind);
|
|
39
|
-
|
|
40
|
-
|
|
45
|
+
if (!positions || positions.length < 9) return;
|
|
46
|
+
const vertexPositions = new Float32Array(positions);
|
|
47
|
+
const read = mesh.getIndices();
|
|
48
|
+
const indices = ensureTriangleIndices(
|
|
49
|
+
vertexPositions,
|
|
50
|
+
read && read.length > 0 ? Uint32Array.from(read) : undefined
|
|
51
|
+
);
|
|
52
|
+
const triCount = Math.floor(indices.length / 3);
|
|
53
|
+
if (triCount === 0) return;
|
|
54
|
+
|
|
55
|
+
this._positions = vertexPositions;
|
|
56
|
+
this._indices = new Int32Array(indices.subarray(0, triCount * 3));
|
|
41
57
|
|
|
42
|
-
this._positions = new Float32Array(positions);
|
|
43
|
-
this._indices = new Int32Array(indices);
|
|
44
|
-
|
|
45
|
-
this._triangleIndexToArea.length = 0;
|
|
46
58
|
let area = 0;
|
|
47
|
-
const triCount = indices.length / 3;
|
|
48
59
|
this._triangleIndexToArea.push(0);
|
|
49
60
|
|
|
50
61
|
const a = new Vector3();
|
|
51
62
|
const b = new Vector3();
|
|
52
63
|
const c = new Vector3();
|
|
64
|
+
const cross = new Vector3();
|
|
53
65
|
|
|
54
66
|
for (let i = 0; i < triCount; i++) {
|
|
55
67
|
const i0 = indices[i * 3];
|
|
@@ -62,7 +74,6 @@ export class MeshSurfaceEmitter implements EmitterShape {
|
|
|
62
74
|
|
|
63
75
|
b.sub(a);
|
|
64
76
|
c.sub(a);
|
|
65
|
-
const cross = new Vector3();
|
|
66
77
|
cross.crossVectors(b, c);
|
|
67
78
|
area += cross.length() * 0.5;
|
|
68
79
|
this._triangleIndexToArea.push(area);
|
|
@@ -144,7 +155,7 @@ export class MeshSurfaceEmitter implements EmitterShape {
|
|
|
144
155
|
}
|
|
145
156
|
|
|
146
157
|
/** Reads {@link toJSON} output; the mesh stays an id until `QuarksLoader` links it. */
|
|
147
|
-
static fromJSON(json:
|
|
158
|
+
static fromJSON(json: ShapeJSON | undefined): MeshSurfaceEmitter {
|
|
148
159
|
return new MeshSurfaceEmitter(undefined, json?.mesh, ShapeTransform.fromJSON(json?.transform));
|
|
149
160
|
}
|
|
150
161
|
|
|
@@ -166,7 +177,7 @@ export const MeshSurfaceEmitterPlugin: Plugin = {
|
|
|
166
177
|
type: 'mesh_surface',
|
|
167
178
|
params: [['mesh', ['mesh']]],
|
|
168
179
|
constructor: MeshSurfaceEmitter,
|
|
169
|
-
loadJSON: (json:
|
|
180
|
+
loadJSON: (json: ShapeJSON) => MeshSurfaceEmitter.fromJSON(json),
|
|
170
181
|
},
|
|
171
182
|
],
|
|
172
183
|
behaviors: [],
|