babylon.quarks 0.18.1 → 0.19.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/README.md +16 -16
- 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.ts +1834 -1181
- package/package.json +67 -67
- package/src/BatchedRenderer.ts +213 -14
- package/src/MeshSurfaceEmitter.ts +25 -20
- package/src/ParticleEmitter.ts +34 -10
- package/src/ParticleSystem.ts +436 -198
- package/src/QuarksLoader.ts +54 -14
- package/src/QuarksPrefab.ts +8 -11
- package/src/QuarksUtil.ts +1 -3
- package/src/SpriteBatch.ts +447 -96
- package/src/TrailBatch.ts +361 -164
- package/src/VFXBatch.ts +20 -4
- package/src/envAtlas.ts +136 -0
- package/src/index.ts +34 -29
- package/src/materials/ParticleMaterials.ts +1 -1
- package/src/shaders/index.ts +4 -4
- package/src/shaders/local_particle_physics_vert.wgsl.ts +81 -0
- package/src/shaders/particle_frag.wgsl.ts +64 -0
- package/src/shaders/particle_physics_frag.glsl.ts +56 -1
- package/src/shaders/particle_physics_frag.wgsl.ts +125 -0
- package/src/shaders/particle_vert.wgsl.ts +87 -0
- package/src/shaders/shaderLanguageSupport.ts +54 -0
- package/src/shaders/stretched_bb_particle_vert.wgsl.ts +88 -0
- package/src/shaders/trail_frag.wgsl.ts +23 -0
- package/src/shaders/trail_vert.wgsl.ts +63 -0
package/package.json
CHANGED
|
@@ -1,70 +1,70 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
2
|
+
"name": "babylon.quarks",
|
|
3
|
+
"version": "0.19.0",
|
|
4
|
+
"description": "A High-Performance Particle System for Babylon.js",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"types": "./dist/types/index.d.ts",
|
|
7
|
+
"main": "./dist/babylon.quarks.cjs",
|
|
8
|
+
"module": "./dist/babylon.quarks.esm.js",
|
|
9
|
+
"unpkg": "./dist/babylon.quarks.umd.min.js",
|
|
10
|
+
"jsdelivr": "./dist/babylon.quarks.umd.min.js",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"import": "./dist/babylon.quarks.esm.js",
|
|
14
|
+
"require": "./dist/babylon.quarks.cjs",
|
|
15
|
+
"types": "./dist/types/index.d.ts"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist",
|
|
20
|
+
"src",
|
|
21
|
+
"LICENSE",
|
|
22
|
+
"README.md",
|
|
23
|
+
"CHANGELOG.md"
|
|
24
|
+
],
|
|
25
|
+
"scripts": {
|
|
26
|
+
"clean": "rimraf dist dist-types tsconfig.tsbuildinfo",
|
|
27
|
+
"build:types": "tsc --emitDeclarationOnly",
|
|
28
|
+
"build:js": "cross-env NODE_ENV=production rollup -c",
|
|
29
|
+
"build": "npm run clean && npm run build:types && npm run build:js",
|
|
30
|
+
"test": "jest",
|
|
31
|
+
"docs": "typedoc",
|
|
32
|
+
"test:coverage": "jest --coverage",
|
|
33
|
+
"prettier": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\""
|
|
34
|
+
},
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "git+https://github.com/Soullnik/babylon.quarks-standalone.git"
|
|
38
|
+
},
|
|
39
|
+
"keywords": [
|
|
40
|
+
"babylonjs",
|
|
41
|
+
"babylon.js",
|
|
42
|
+
"particles",
|
|
43
|
+
"particle-system",
|
|
44
|
+
"particle",
|
|
45
|
+
"vfx",
|
|
46
|
+
"visual-effects",
|
|
47
|
+
"gpu-particles",
|
|
48
|
+
"webgl",
|
|
49
|
+
"webgpu",
|
|
50
|
+
"quarks",
|
|
51
|
+
"three.quarks",
|
|
52
|
+
"3d",
|
|
53
|
+
"game",
|
|
54
|
+
"performance"
|
|
55
|
+
],
|
|
56
|
+
"author": "soullnik <soullnik96@gmail.com>",
|
|
57
|
+
"license": "MIT",
|
|
58
|
+
"bugs": {
|
|
59
|
+
"url": "https://github.com/Soullnik/babylon.quarks-standalone/issues"
|
|
60
|
+
},
|
|
61
|
+
"homepage": "https://github.com/Soullnik/babylon.quarks-standalone#readme",
|
|
62
|
+
"peerDependencies": {
|
|
63
|
+
"@babylonjs/core": ">=9.0.0"
|
|
64
|
+
},
|
|
65
|
+
"devDependencies": {
|
|
66
|
+
"@babylonjs/core": "^9.0.0",
|
|
67
|
+
"@types/jest": "^30.0.0",
|
|
68
|
+
"quarks.core": "*"
|
|
16
69
|
}
|
|
17
|
-
},
|
|
18
|
-
"files": [
|
|
19
|
-
"dist",
|
|
20
|
-
"src",
|
|
21
|
-
"LICENSE",
|
|
22
|
-
"README.md",
|
|
23
|
-
"CHANGELOG.md"
|
|
24
|
-
],
|
|
25
|
-
"scripts": {
|
|
26
|
-
"clean": "rimraf dist dist-types tsconfig.tsbuildinfo",
|
|
27
|
-
"build:types": "tsc --emitDeclarationOnly",
|
|
28
|
-
"build:js": "cross-env NODE_ENV=production rollup -c",
|
|
29
|
-
"build": "npm run clean && npm run build:types && npm run build:js",
|
|
30
|
-
"test": "jest",
|
|
31
|
-
"docs": "typedoc",
|
|
32
|
-
"test:coverage": "jest --coverage",
|
|
33
|
-
"prettier": "prettier --write --ignore-unknown **/*.ts"
|
|
34
|
-
},
|
|
35
|
-
"repository": {
|
|
36
|
-
"type": "git",
|
|
37
|
-
"url": "git+https://github.com/Soullnik/babylon.quarks-standalone.git"
|
|
38
|
-
},
|
|
39
|
-
"keywords": [
|
|
40
|
-
"babylonjs",
|
|
41
|
-
"babylon.js",
|
|
42
|
-
"particles",
|
|
43
|
-
"particle-system",
|
|
44
|
-
"particle",
|
|
45
|
-
"vfx",
|
|
46
|
-
"visual-effects",
|
|
47
|
-
"gpu-particles",
|
|
48
|
-
"webgl",
|
|
49
|
-
"webgpu",
|
|
50
|
-
"quarks",
|
|
51
|
-
"three.quarks",
|
|
52
|
-
"3d",
|
|
53
|
-
"game",
|
|
54
|
-
"performance"
|
|
55
|
-
],
|
|
56
|
-
"author": "soullnik <soullnik96@gmail.com>",
|
|
57
|
-
"license": "MIT",
|
|
58
|
-
"bugs": {
|
|
59
|
-
"url": "https://github.com/Soullnik/babylon.quarks-standalone/issues"
|
|
60
|
-
},
|
|
61
|
-
"homepage": "https://github.com/Soullnik/babylon.quarks-standalone#readme",
|
|
62
|
-
"peerDependencies": {
|
|
63
|
-
"@babylonjs/core": ">=9.0.0"
|
|
64
|
-
},
|
|
65
|
-
"devDependencies": {
|
|
66
|
-
"@babylonjs/core": "^9.0.0",
|
|
67
|
-
"@types/jest": "^30.0.0",
|
|
68
|
-
"quarks.core": "*"
|
|
69
|
-
}
|
|
70
70
|
}
|
package/src/BatchedRenderer.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
+
import {BaseTexture} from '@babylonjs/core/Materials/Textures/baseTexture';
|
|
2
|
+
import {Texture} from '@babylonjs/core/Materials/Textures/texture';
|
|
1
3
|
import {TransformNode} from '@babylonjs/core/Meshes/transformNode';
|
|
2
4
|
import {Scene} from '@babylonjs/core/scene';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {IParticleSystem} from 'quarks.core';
|
|
6
|
-
import {VFXBatch, RenderMode, StoredBatchSettings} from './VFXBatch';
|
|
5
|
+
import {EmitSubParticleSystem, IParticleSystem} from 'quarks.core';
|
|
6
|
+
import {ParticleSystem} from './ParticleSystem';
|
|
7
7
|
import {SpriteBatch} from './SpriteBatch';
|
|
8
8
|
import {TrailBatch} from './TrailBatch';
|
|
9
|
-
import {
|
|
9
|
+
import {RenderMode, StoredBatchSettings, VFXBatch} from './VFXBatch';
|
|
10
10
|
|
|
11
11
|
export interface VFXBatchSettings {
|
|
12
12
|
instancingGeometry: Float32Array;
|
|
@@ -27,6 +27,25 @@ export interface VFXBatchSettings {
|
|
|
27
27
|
materialDepthWrite: boolean;
|
|
28
28
|
materialAlphaTest: number;
|
|
29
29
|
texture: Texture | null;
|
|
30
|
+
/**
|
|
31
|
+
* Cubemap (or other reflection texture) harvested from a Babylon material's
|
|
32
|
+
* `reflectionTexture`. Mesh batches sample it like StandardMaterial cubic
|
|
33
|
+
* reflection; other render modes ignore it.
|
|
34
|
+
*/
|
|
35
|
+
reflectionTexture: BaseTexture | null;
|
|
36
|
+
/** Multiplier for the reflection sample — mirrors `texture.level`. */
|
|
37
|
+
reflectionLevel: number;
|
|
38
|
+
/**
|
|
39
|
+
* Optional six cube-face 2D textures (px,py,pz,nx,ny,nz). Prefer
|
|
40
|
+
* `reflectionAtlas` on iOS — many simultaneous face samplers still trip
|
|
41
|
+
* GL_INVALID_OPERATION on WebKit.
|
|
42
|
+
*/
|
|
43
|
+
reflectionFaces: BaseTexture[] | null;
|
|
44
|
+
/**
|
|
45
|
+
* Single 3×2 atlas of cube faces (px py pz / nx ny nz). One sampler2D —
|
|
46
|
+
* the path that stays valid on iOS WebKit ShaderMaterial draws.
|
|
47
|
+
*/
|
|
48
|
+
reflectionAtlas: BaseTexture | null;
|
|
30
49
|
layerMask: number;
|
|
31
50
|
}
|
|
32
51
|
|
|
@@ -38,6 +57,22 @@ export interface AdaptivePerformanceOptions {
|
|
|
38
57
|
increaseStep: number;
|
|
39
58
|
}
|
|
40
59
|
|
|
60
|
+
/** True when both face lists are the same six texture references (or both empty). */
|
|
61
|
+
function reflectionFacesEqual(a: BaseTexture[] | null | undefined, b: BaseTexture[] | null | undefined): boolean {
|
|
62
|
+
if (a === b) {
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
if (!a || !b || a.length !== b.length) {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
for (let i = 0; i < a.length; i++) {
|
|
69
|
+
if (a[i] !== b[i]) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
|
|
41
76
|
export interface AdaptivePerformanceState extends AdaptivePerformanceOptions {
|
|
42
77
|
enabled: boolean;
|
|
43
78
|
currentQuality: number;
|
|
@@ -48,6 +83,29 @@ export class BatchedRenderer extends TransformNode {
|
|
|
48
83
|
batches: Array<VFXBatch> = [];
|
|
49
84
|
systemToBatchIndex: Map<IParticleSystem, number> = new Map<IParticleSystem, number>();
|
|
50
85
|
depthTexture: BaseTexture | null = null;
|
|
86
|
+
/** Systems in insertion order — iterated every frame, unlike the map. */
|
|
87
|
+
private systems: Array<IParticleSystem> = [];
|
|
88
|
+
/**
|
|
89
|
+
* The same systems, ordered so that one emitting into another comes first.
|
|
90
|
+
*
|
|
91
|
+
* A sub emitter's particles are created by its parent's behaviors, part way
|
|
92
|
+
* through the parent's step. A sub system updated before its parent has
|
|
93
|
+
* already fixed the list of particles it will run behaviors over, so those
|
|
94
|
+
* new particles are drawn that frame with nothing but their start values —
|
|
95
|
+
* white where a colour curve would have coloured them, at their start size,
|
|
96
|
+
* sitting at the point they were born. Rebuilt only when the set of systems
|
|
97
|
+
* or their behaviors changes, not per frame.
|
|
98
|
+
*/
|
|
99
|
+
private orderedSystems: Array<IParticleSystem> = [];
|
|
100
|
+
private orderedSystemsStale = true;
|
|
101
|
+
/** Consecutive frames each batch has held no system, parallel to `batches`. */
|
|
102
|
+
private batchEmptyFrames: Array<number> = [];
|
|
103
|
+
/**
|
|
104
|
+
* How long an empty batch is kept before it is disposed. Systems that toggle
|
|
105
|
+
* a setting back and forth reuse the batch within this window instead of
|
|
106
|
+
* rebuilding a mesh and material every time.
|
|
107
|
+
*/
|
|
108
|
+
private static readonly EMPTY_BATCH_GRACE_FRAMES = 120;
|
|
51
109
|
private adaptivePerformanceState: AdaptivePerformanceState = {
|
|
52
110
|
enabled: false,
|
|
53
111
|
targetFrameMs: 16.7,
|
|
@@ -76,6 +134,10 @@ export class BatchedRenderer extends TransformNode {
|
|
|
76
134
|
a.materialDepthWrite === b.materialDepthWrite &&
|
|
77
135
|
a.materialAlphaTest === b.materialAlphaTest &&
|
|
78
136
|
a.texture === b.texture &&
|
|
137
|
+
a.reflectionTexture === b.reflectionTexture &&
|
|
138
|
+
a.reflectionLevel === b.reflectionLevel &&
|
|
139
|
+
reflectionFacesEqual(a.reflectionFaces, b.reflectionFaces) &&
|
|
140
|
+
a.reflectionAtlas === b.reflectionAtlas &&
|
|
79
141
|
a.renderMode === b.renderMode &&
|
|
80
142
|
a.blendTiles === b.blendTiles &&
|
|
81
143
|
a.softParticles === b.softParticles &&
|
|
@@ -92,9 +154,19 @@ export class BatchedRenderer extends TransformNode {
|
|
|
92
154
|
addSystem(system: IParticleSystem) {
|
|
93
155
|
const particleSystem = system as ParticleSystem;
|
|
94
156
|
particleSystem._renderer = this;
|
|
157
|
+
if (!this.systemToBatchIndex.has(system) && this.systems.indexOf(system) === -1) {
|
|
158
|
+
this.systems.push(system);
|
|
159
|
+
this.orderedSystemsStale = true;
|
|
160
|
+
}
|
|
95
161
|
if (this.adaptivePerformanceState.enabled) {
|
|
96
162
|
particleSystem.setQualityFactor(this.adaptivePerformanceState.currentQuality);
|
|
97
163
|
}
|
|
164
|
+
this.assignToBatch(particleSystem);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Places a system into a compatible batch, creating one when none matches. */
|
|
168
|
+
private assignToBatch(particleSystem: ParticleSystem) {
|
|
169
|
+
const system = particleSystem as IParticleSystem;
|
|
98
170
|
const settings = particleSystem.getRendererSettings();
|
|
99
171
|
for (let i = 0; i < this.batches.length; i++) {
|
|
100
172
|
if (BatchedRenderer.equals(this.batches[i].settings, settings)) {
|
|
@@ -125,19 +197,44 @@ export class BatchedRenderer extends TransformNode {
|
|
|
125
197
|
}
|
|
126
198
|
batch.addSystem(system);
|
|
127
199
|
this.batches.push(batch);
|
|
200
|
+
this.batchEmptyFrames.push(0);
|
|
128
201
|
this.systemToBatchIndex.set(system, this.batches.length - 1);
|
|
129
202
|
}
|
|
130
203
|
|
|
131
204
|
deleteSystem(system: IParticleSystem) {
|
|
205
|
+
const removed = this.removeFromBatch(system);
|
|
206
|
+
const index = this.systems.indexOf(system);
|
|
207
|
+
if (index !== -1) {
|
|
208
|
+
this.systems.splice(index, 1);
|
|
209
|
+
this.orderedSystemsStale = true;
|
|
210
|
+
}
|
|
211
|
+
// Also out of the ordered copy, which update() may be iterating right
|
|
212
|
+
// now: a system that disposes itself from inside its own update leaves
|
|
213
|
+
// the loop expecting the slot it vacated to hold the next one.
|
|
214
|
+
const ordered = this.orderedSystems.indexOf(system);
|
|
215
|
+
if (ordered !== -1) {
|
|
216
|
+
this.orderedSystems.splice(ordered, 1);
|
|
217
|
+
}
|
|
218
|
+
return removed;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Detaches a system from its batch, leaving the renderer's system list alone. */
|
|
222
|
+
private removeFromBatch(system: IParticleSystem): boolean {
|
|
132
223
|
const batchIndex = this.systemToBatchIndex.get(system);
|
|
133
|
-
if (batchIndex
|
|
134
|
-
|
|
135
|
-
this.systemToBatchIndex.delete(system);
|
|
224
|
+
if (batchIndex === undefined) {
|
|
225
|
+
return false;
|
|
136
226
|
}
|
|
227
|
+
this.batches[batchIndex].removeSystem(system);
|
|
228
|
+
this.systemToBatchIndex.delete(system);
|
|
229
|
+
return true;
|
|
137
230
|
}
|
|
138
231
|
|
|
232
|
+
/**
|
|
233
|
+
* Re-buckets a system whose render settings changed. The system keeps its
|
|
234
|
+
* place in the update order, so this is safe to call mid-frame.
|
|
235
|
+
*/
|
|
139
236
|
updateSystem(system: IParticleSystem) {
|
|
140
|
-
this.
|
|
237
|
+
this.removeFromBatch(system);
|
|
141
238
|
this.addSystem(system);
|
|
142
239
|
}
|
|
143
240
|
|
|
@@ -179,20 +276,90 @@ export class BatchedRenderer extends TransformNode {
|
|
|
179
276
|
if (quality === this.lastAppliedQuality) {
|
|
180
277
|
return;
|
|
181
278
|
}
|
|
182
|
-
for (
|
|
183
|
-
(
|
|
279
|
+
for (let i = 0; i < this.systems.length; i++) {
|
|
280
|
+
(this.systems[i] as ParticleSystem).setQualityFactor(quality);
|
|
184
281
|
}
|
|
185
282
|
this.lastAppliedQuality = quality;
|
|
186
283
|
}
|
|
187
284
|
|
|
285
|
+
/** @internal Called when a system's behaviors change, which can add a sub emitter. */
|
|
286
|
+
_invalidateUpdateOrder(): void {
|
|
287
|
+
this.orderedSystemsStale = true;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Orders the systems so a system emitting into another runs first.
|
|
292
|
+
*
|
|
293
|
+
* Depth first over "emits into" edges, recording each system once all the
|
|
294
|
+
* systems it feeds have been recorded, then reversed — a system therefore
|
|
295
|
+
* lands ahead of everything it emits into, however deep the chain. A cycle
|
|
296
|
+
* cannot be satisfied by any order; it is left as found rather than dropped.
|
|
297
|
+
*/
|
|
298
|
+
private orderSystems(): Array<IParticleSystem> {
|
|
299
|
+
if (!this.orderedSystemsStale) {
|
|
300
|
+
return this.orderedSystems;
|
|
301
|
+
}
|
|
302
|
+
const ordered: Array<IParticleSystem> = [];
|
|
303
|
+
const visiting = new Set<IParticleSystem>();
|
|
304
|
+
const done = new Set<IParticleSystem>();
|
|
305
|
+
const known = new Set<IParticleSystem>(this.systems);
|
|
306
|
+
|
|
307
|
+
const subTargetsOf = (system: IParticleSystem): Array<IParticleSystem> => {
|
|
308
|
+
const targets: Array<IParticleSystem> = [];
|
|
309
|
+
for (const behavior of (system as ParticleSystem).behaviors ?? []) {
|
|
310
|
+
if (behavior.type !== 'EmitSubParticleSystem') {
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
const target = (behavior as EmitSubParticleSystem).subParticleSystem?.system;
|
|
314
|
+
if (target !== undefined && known.has(target)) {
|
|
315
|
+
targets.push(target);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return targets;
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
const visit = (system: IParticleSystem): void => {
|
|
322
|
+
if (done.has(system) || visiting.has(system)) {
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
visiting.add(system);
|
|
326
|
+
for (const target of subTargetsOf(system)) {
|
|
327
|
+
visit(target);
|
|
328
|
+
}
|
|
329
|
+
visiting.delete(system);
|
|
330
|
+
done.add(system);
|
|
331
|
+
ordered.push(system);
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
for (const system of this.systems) {
|
|
335
|
+
visit(system);
|
|
336
|
+
}
|
|
337
|
+
ordered.reverse();
|
|
338
|
+
this.orderedSystems = ordered;
|
|
339
|
+
this.orderedSystemsStale = false;
|
|
340
|
+
return ordered;
|
|
341
|
+
}
|
|
342
|
+
|
|
188
343
|
update(delta: number) {
|
|
189
344
|
const adaptiveState = this.adaptivePerformanceState;
|
|
190
345
|
const frameStart = adaptiveState.enabled ? performance.now() : 0;
|
|
191
346
|
if (adaptiveState.enabled) {
|
|
192
347
|
this.applyAdaptiveQuality();
|
|
193
348
|
}
|
|
194
|
-
|
|
195
|
-
|
|
349
|
+
const systems = this.orderSystems();
|
|
350
|
+
for (let i = 0; i < systems.length; i++) {
|
|
351
|
+
const system = systems[i];
|
|
352
|
+
// A disabled emitter holds its particles frozen instead of simulating
|
|
353
|
+
// them unseen, matching how Unity treats a disabled ParticleSystem.
|
|
354
|
+
// Pooled effects parked with setEnabled(false) then cost nothing.
|
|
355
|
+
if (system.emitter.visible) {
|
|
356
|
+
(system as ParticleSystem).update(delta);
|
|
357
|
+
}
|
|
358
|
+
// An autoDestroy system disposes itself from inside update and drops
|
|
359
|
+
// out of the list; stay on the slot it vacated.
|
|
360
|
+
if (systems[i] !== system) {
|
|
361
|
+
i--;
|
|
362
|
+
}
|
|
196
363
|
}
|
|
197
364
|
this.refreshBatches();
|
|
198
365
|
if (!adaptiveState.enabled) {
|
|
@@ -217,8 +384,38 @@ export class BatchedRenderer extends TransformNode {
|
|
|
217
384
|
|
|
218
385
|
/** Rebuild instance buffers from current particle state without advancing simulation. */
|
|
219
386
|
refreshBatches(): void {
|
|
387
|
+
const emptyFrames = this.batchEmptyFrames;
|
|
220
388
|
for (let i = 0; i < this.batches.length; i++) {
|
|
221
|
-
this.batches[i]
|
|
389
|
+
const batch = this.batches[i];
|
|
390
|
+
if (batch.systems.size > 0) {
|
|
391
|
+
emptyFrames[i] = 0;
|
|
392
|
+
batch.update();
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
const framesEmpty = (emptyFrames[i] ?? 0) + 1;
|
|
396
|
+
emptyFrames[i] = framesEmpty;
|
|
397
|
+
if (framesEmpty === 1) {
|
|
398
|
+
// One last update clears the instance data left on screen.
|
|
399
|
+
batch.update();
|
|
400
|
+
} else if (framesEmpty > BatchedRenderer.EMPTY_BATCH_GRACE_FRAMES) {
|
|
401
|
+
// Nothing has used these settings for a while: give the mesh,
|
|
402
|
+
// material and GPU buffers back instead of re-uploading empty
|
|
403
|
+
// instance data forever.
|
|
404
|
+
this.removeBatch(i);
|
|
405
|
+
i--;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/** Disposes the batch at `index` and repairs the system→batch mapping. */
|
|
411
|
+
private removeBatch(index: number): void {
|
|
412
|
+
this.batches[index].dispose();
|
|
413
|
+
this.batches.splice(index, 1);
|
|
414
|
+
this.batchEmptyFrames.splice(index, 1);
|
|
415
|
+
for (const [system, batchIndex] of this.systemToBatchIndex) {
|
|
416
|
+
if (batchIndex > index) {
|
|
417
|
+
this.systemToBatchIndex.set(system, batchIndex - 1);
|
|
418
|
+
}
|
|
222
419
|
}
|
|
223
420
|
}
|
|
224
421
|
|
|
@@ -253,6 +450,8 @@ export class BatchedRenderer extends TransformNode {
|
|
|
253
450
|
batch.dispose();
|
|
254
451
|
}
|
|
255
452
|
this.batches = [];
|
|
453
|
+
this.batchEmptyFrames.length = 0;
|
|
454
|
+
this.systems.length = 0;
|
|
256
455
|
this.systemToBatchIndex.clear();
|
|
257
456
|
super.dispose();
|
|
258
457
|
}
|
|
@@ -1,13 +1,6 @@
|
|
|
1
|
-
import {Mesh} from '@babylonjs/core/Meshes/mesh';
|
|
2
1
|
import {VertexBuffer} from '@babylonjs/core/Buffers/buffer';
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
EmitterShape,
|
|
6
|
-
ShapeJSON,
|
|
7
|
-
IParticleSystem,
|
|
8
|
-
Vector3,
|
|
9
|
-
Plugin,
|
|
10
|
-
} from 'quarks.core';
|
|
2
|
+
import {Mesh} from '@babylonjs/core/Meshes/mesh';
|
|
3
|
+
import {EmitterShape, IParticleSystem, Particle, Plugin, ShapeJSON, Vector3} from 'quarks.core';
|
|
11
4
|
|
|
12
5
|
export class MeshSurfaceEmitter implements EmitterShape {
|
|
13
6
|
type = 'mesh_surface';
|
|
@@ -73,7 +66,8 @@ export class MeshSurfaceEmitter implements EmitterShape {
|
|
|
73
66
|
}
|
|
74
67
|
|
|
75
68
|
const triCount = this._triangleIndexToArea.length - 1;
|
|
76
|
-
let left = 0,
|
|
69
|
+
let left = 0,
|
|
70
|
+
right = triCount;
|
|
77
71
|
const target = Math.random() * this._triangleIndexToArea[triCount];
|
|
78
72
|
|
|
79
73
|
while (left + 1 < right) {
|
|
@@ -96,9 +90,15 @@ export class MeshSurfaceEmitter implements EmitterShape {
|
|
|
96
90
|
const idx1 = this._indices[left * 3 + 1];
|
|
97
91
|
const idx2 = this._indices[left * 3 + 2];
|
|
98
92
|
|
|
99
|
-
const ax = this._positions[idx0 * 3],
|
|
100
|
-
|
|
101
|
-
|
|
93
|
+
const ax = this._positions[idx0 * 3],
|
|
94
|
+
ay = this._positions[idx0 * 3 + 1],
|
|
95
|
+
az = this._positions[idx0 * 3 + 2];
|
|
96
|
+
const bx = this._positions[idx1 * 3] - ax,
|
|
97
|
+
by = this._positions[idx1 * 3 + 1] - ay,
|
|
98
|
+
bz = this._positions[idx1 * 3 + 2] - az;
|
|
99
|
+
const cx = this._positions[idx2 * 3] - ax,
|
|
100
|
+
cy = this._positions[idx2 * 3 + 1] - ay,
|
|
101
|
+
cz = this._positions[idx2 * 3 + 2] - az;
|
|
102
102
|
|
|
103
103
|
p.position.set(ax + bx * u1 + cx * u2, ay + by * u1 + cy * u2, az + bz * u1 + cz * u2);
|
|
104
104
|
|
|
@@ -110,7 +110,10 @@ export class MeshSurfaceEmitter implements EmitterShape {
|
|
|
110
110
|
}
|
|
111
111
|
|
|
112
112
|
toJSON(): ShapeJSON {
|
|
113
|
-
return {
|
|
113
|
+
return {
|
|
114
|
+
type: 'mesh_surface',
|
|
115
|
+
mesh: this._mesh ? (this._mesh as any).uniqueId?.toString() : (this._meshReferenceId ?? ''),
|
|
116
|
+
};
|
|
114
117
|
}
|
|
115
118
|
|
|
116
119
|
static fromJSON(json: any): MeshSurfaceEmitter {
|
|
@@ -127,11 +130,13 @@ export class MeshSurfaceEmitter implements EmitterShape {
|
|
|
127
130
|
export const MeshSurfaceEmitterPlugin: Plugin = {
|
|
128
131
|
id: 'babylon.quarks',
|
|
129
132
|
initialize: () => {},
|
|
130
|
-
emitterShapes: [
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
133
|
+
emitterShapes: [
|
|
134
|
+
{
|
|
135
|
+
type: 'mesh_surface',
|
|
136
|
+
params: [['mesh', ['mesh']]],
|
|
137
|
+
constructor: MeshSurfaceEmitter,
|
|
138
|
+
loadJSON: (json: any) => MeshSurfaceEmitter.fromJSON(json),
|
|
139
|
+
},
|
|
140
|
+
],
|
|
136
141
|
behaviors: [],
|
|
137
142
|
};
|
package/src/ParticleEmitter.ts
CHANGED
|
@@ -1,18 +1,42 @@
|
|
|
1
1
|
import {TransformNode} from '@babylonjs/core/Meshes/transformNode';
|
|
2
2
|
import {Scene} from '@babylonjs/core/scene';
|
|
3
|
-
import {
|
|
3
|
+
import {IParticleSystem, Matrix4, SerializationOptions} from 'quarks.core';
|
|
4
4
|
|
|
5
5
|
export class ParticleEmitter extends TransformNode {
|
|
6
6
|
system: IParticleSystem;
|
|
7
7
|
private _matrixWorld: Matrix4 = new Matrix4();
|
|
8
|
+
private _matrixWorldFlag = -1;
|
|
8
9
|
|
|
10
|
+
/**
|
|
11
|
+
* The node's world matrix in quarks.core form. Mirrored from Babylon's
|
|
12
|
+
* matrix only when that matrix actually changed (tracked by its
|
|
13
|
+
* `updateFlag`), so repeated reads inside a frame are free.
|
|
14
|
+
*/
|
|
9
15
|
get matrixWorld(): Matrix4 {
|
|
10
16
|
const m = this.getWorldMatrix();
|
|
17
|
+
const flag = m.updateFlag;
|
|
18
|
+
if (flag === this._matrixWorldFlag) {
|
|
19
|
+
return this._matrixWorld;
|
|
20
|
+
}
|
|
21
|
+
this._matrixWorldFlag = flag;
|
|
11
22
|
const e = this._matrixWorld.elements;
|
|
12
|
-
|
|
13
|
-
e[
|
|
14
|
-
e[
|
|
15
|
-
e[
|
|
23
|
+
const s = m.m;
|
|
24
|
+
e[0] = s[0];
|
|
25
|
+
e[1] = s[1];
|
|
26
|
+
e[2] = s[2];
|
|
27
|
+
e[3] = s[3];
|
|
28
|
+
e[4] = s[4];
|
|
29
|
+
e[5] = s[5];
|
|
30
|
+
e[6] = s[6];
|
|
31
|
+
e[7] = s[7];
|
|
32
|
+
e[8] = s[8];
|
|
33
|
+
e[9] = s[9];
|
|
34
|
+
e[10] = s[10];
|
|
35
|
+
e[11] = s[11];
|
|
36
|
+
e[12] = s[12];
|
|
37
|
+
e[13] = s[13];
|
|
38
|
+
e[14] = s[14];
|
|
39
|
+
e[15] = s[15];
|
|
16
40
|
return this._matrixWorld;
|
|
17
41
|
}
|
|
18
42
|
|
|
@@ -72,11 +96,11 @@ export class ParticleEmitter extends TransformNode {
|
|
|
72
96
|
rotation: [this.rotation.x, this.rotation.y, this.rotation.z],
|
|
73
97
|
quaternion: this.rotationQuaternion
|
|
74
98
|
? [
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
99
|
+
this.rotationQuaternion.x,
|
|
100
|
+
this.rotationQuaternion.y,
|
|
101
|
+
this.rotationQuaternion.z,
|
|
102
|
+
this.rotationQuaternion.w,
|
|
103
|
+
]
|
|
80
104
|
: undefined,
|
|
81
105
|
scale: [this.scaling.x, this.scaling.y, this.scaling.z],
|
|
82
106
|
visible: this.isEnabled(),
|