babylon.quarks 0.18.1 → 0.20.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 +57 -0
- package/README.md +38 -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 +2121 -1157
- package/package.json +67 -67
- package/src/BatchedRenderer.ts +228 -15
- package/src/MeshSurfaceEmitter.ts +34 -23
- package/src/ParticleEmitter.ts +34 -10
- package/src/ParticleSystem.ts +558 -197
- package/src/QuarksLoader.ts +204 -18
- package/src/QuarksPrefab.ts +8 -11
- package/src/QuarksUtil.ts +1 -3
- package/src/SpriteBatch.ts +520 -98
- package/src/TrailBatch.ts +374 -166
- package/src/VFXBatch.ts +24 -5
- 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.glsl.ts +20 -0
- package/src/shaders/particle_vert.wgsl.ts +107 -0
- package/src/shaders/shaderLanguageSupport.ts +54 -0
- package/src/shaders/stretched_bb_particle_vert.glsl.ts +18 -5
- package/src/shaders/stretched_bb_particle_vert.wgsl.ts +101 -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.20.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, Vector3} 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,7 +27,34 @@ 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;
|
|
50
|
+
/**
|
|
51
|
+
* Offset of billboard quads from their particle, in particle-size units
|
|
52
|
+
* (Unity's renderer Pivot). On billboards x/y move the quad in its own plane
|
|
53
|
+
* before it is rotated and z moves it toward the camera (up for horizontal
|
|
54
|
+
* billboards). On stretched billboards x runs from the head to the tail and
|
|
55
|
+
* y across, and z is unused. Absent or zero: no offset. Meshes ignore it.
|
|
56
|
+
*/
|
|
57
|
+
pivot?: Vector3;
|
|
31
58
|
}
|
|
32
59
|
|
|
33
60
|
export interface AdaptivePerformanceOptions {
|
|
@@ -38,6 +65,27 @@ export interface AdaptivePerformanceOptions {
|
|
|
38
65
|
increaseStep: number;
|
|
39
66
|
}
|
|
40
67
|
|
|
68
|
+
/** True when both pivots put the quad in the same place; a missing pivot is zero. */
|
|
69
|
+
function pivotEqual(a: Vector3 | undefined, b: Vector3 | undefined): boolean {
|
|
70
|
+
return (a?.x ?? 0) === (b?.x ?? 0) && (a?.y ?? 0) === (b?.y ?? 0) && (a?.z ?? 0) === (b?.z ?? 0);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** True when both face lists are the same six texture references (or both empty). */
|
|
74
|
+
function reflectionFacesEqual(a: BaseTexture[] | null | undefined, b: BaseTexture[] | null | undefined): boolean {
|
|
75
|
+
if (a === b) {
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
if (!a || !b || a.length !== b.length) {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
for (let i = 0; i < a.length; i++) {
|
|
82
|
+
if (a[i] !== b[i]) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
|
|
41
89
|
export interface AdaptivePerformanceState extends AdaptivePerformanceOptions {
|
|
42
90
|
enabled: boolean;
|
|
43
91
|
currentQuality: number;
|
|
@@ -48,6 +96,29 @@ export class BatchedRenderer extends TransformNode {
|
|
|
48
96
|
batches: Array<VFXBatch> = [];
|
|
49
97
|
systemToBatchIndex: Map<IParticleSystem, number> = new Map<IParticleSystem, number>();
|
|
50
98
|
depthTexture: BaseTexture | null = null;
|
|
99
|
+
/** Systems in insertion order — iterated every frame, unlike the map. */
|
|
100
|
+
private systems: Array<IParticleSystem> = [];
|
|
101
|
+
/**
|
|
102
|
+
* The same systems, ordered so that one emitting into another comes first.
|
|
103
|
+
*
|
|
104
|
+
* A sub emitter's particles are created by its parent's behaviors, part way
|
|
105
|
+
* through the parent's step. A sub system updated before its parent has
|
|
106
|
+
* already fixed the list of particles it will run behaviors over, so those
|
|
107
|
+
* new particles are drawn that frame with nothing but their start values —
|
|
108
|
+
* white where a colour curve would have coloured them, at their start size,
|
|
109
|
+
* sitting at the point they were born. Rebuilt only when the set of systems
|
|
110
|
+
* or their behaviors changes, not per frame.
|
|
111
|
+
*/
|
|
112
|
+
private orderedSystems: Array<IParticleSystem> = [];
|
|
113
|
+
private orderedSystemsStale = true;
|
|
114
|
+
/** Consecutive frames each batch has held no system, parallel to `batches`. */
|
|
115
|
+
private batchEmptyFrames: Array<number> = [];
|
|
116
|
+
/**
|
|
117
|
+
* How long an empty batch is kept before it is disposed. Systems that toggle
|
|
118
|
+
* a setting back and forth reuse the batch within this window instead of
|
|
119
|
+
* rebuilding a mesh and material every time.
|
|
120
|
+
*/
|
|
121
|
+
private static readonly EMPTY_BATCH_GRACE_FRAMES = 120;
|
|
51
122
|
private adaptivePerformanceState: AdaptivePerformanceState = {
|
|
52
123
|
enabled: false,
|
|
53
124
|
targetFrameMs: 16.7,
|
|
@@ -76,6 +147,10 @@ export class BatchedRenderer extends TransformNode {
|
|
|
76
147
|
a.materialDepthWrite === b.materialDepthWrite &&
|
|
77
148
|
a.materialAlphaTest === b.materialAlphaTest &&
|
|
78
149
|
a.texture === b.texture &&
|
|
150
|
+
a.reflectionTexture === b.reflectionTexture &&
|
|
151
|
+
a.reflectionLevel === b.reflectionLevel &&
|
|
152
|
+
reflectionFacesEqual(a.reflectionFaces, b.reflectionFaces) &&
|
|
153
|
+
a.reflectionAtlas === b.reflectionAtlas &&
|
|
79
154
|
a.renderMode === b.renderMode &&
|
|
80
155
|
a.blendTiles === b.blendTiles &&
|
|
81
156
|
a.softParticles === b.softParticles &&
|
|
@@ -85,16 +160,27 @@ export class BatchedRenderer extends TransformNode {
|
|
|
85
160
|
a.vTileCount === b.vTileCount &&
|
|
86
161
|
a.instancingGeometry === b.instancingGeometry &&
|
|
87
162
|
a.renderOrder === b.renderOrder &&
|
|
88
|
-
a.layerMask === b.layerMask
|
|
163
|
+
a.layerMask === b.layerMask &&
|
|
164
|
+
pivotEqual(a.pivot, b.pivot)
|
|
89
165
|
);
|
|
90
166
|
}
|
|
91
167
|
|
|
92
168
|
addSystem(system: IParticleSystem) {
|
|
93
169
|
const particleSystem = system as ParticleSystem;
|
|
94
170
|
particleSystem._renderer = this;
|
|
171
|
+
if (!this.systemToBatchIndex.has(system) && this.systems.indexOf(system) === -1) {
|
|
172
|
+
this.systems.push(system);
|
|
173
|
+
this.orderedSystemsStale = true;
|
|
174
|
+
}
|
|
95
175
|
if (this.adaptivePerformanceState.enabled) {
|
|
96
176
|
particleSystem.setQualityFactor(this.adaptivePerformanceState.currentQuality);
|
|
97
177
|
}
|
|
178
|
+
this.assignToBatch(particleSystem);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Places a system into a compatible batch, creating one when none matches. */
|
|
182
|
+
private assignToBatch(particleSystem: ParticleSystem) {
|
|
183
|
+
const system = particleSystem as IParticleSystem;
|
|
98
184
|
const settings = particleSystem.getRendererSettings();
|
|
99
185
|
for (let i = 0; i < this.batches.length; i++) {
|
|
100
186
|
if (BatchedRenderer.equals(this.batches[i].settings, settings)) {
|
|
@@ -125,19 +211,44 @@ export class BatchedRenderer extends TransformNode {
|
|
|
125
211
|
}
|
|
126
212
|
batch.addSystem(system);
|
|
127
213
|
this.batches.push(batch);
|
|
214
|
+
this.batchEmptyFrames.push(0);
|
|
128
215
|
this.systemToBatchIndex.set(system, this.batches.length - 1);
|
|
129
216
|
}
|
|
130
217
|
|
|
131
218
|
deleteSystem(system: IParticleSystem) {
|
|
219
|
+
const removed = this.removeFromBatch(system);
|
|
220
|
+
const index = this.systems.indexOf(system);
|
|
221
|
+
if (index !== -1) {
|
|
222
|
+
this.systems.splice(index, 1);
|
|
223
|
+
this.orderedSystemsStale = true;
|
|
224
|
+
}
|
|
225
|
+
// Also out of the ordered copy, which update() may be iterating right
|
|
226
|
+
// now: a system that disposes itself from inside its own update leaves
|
|
227
|
+
// the loop expecting the slot it vacated to hold the next one.
|
|
228
|
+
const ordered = this.orderedSystems.indexOf(system);
|
|
229
|
+
if (ordered !== -1) {
|
|
230
|
+
this.orderedSystems.splice(ordered, 1);
|
|
231
|
+
}
|
|
232
|
+
return removed;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** Detaches a system from its batch, leaving the renderer's system list alone. */
|
|
236
|
+
private removeFromBatch(system: IParticleSystem): boolean {
|
|
132
237
|
const batchIndex = this.systemToBatchIndex.get(system);
|
|
133
|
-
if (batchIndex
|
|
134
|
-
|
|
135
|
-
this.systemToBatchIndex.delete(system);
|
|
238
|
+
if (batchIndex === undefined) {
|
|
239
|
+
return false;
|
|
136
240
|
}
|
|
241
|
+
this.batches[batchIndex].removeSystem(system);
|
|
242
|
+
this.systemToBatchIndex.delete(system);
|
|
243
|
+
return true;
|
|
137
244
|
}
|
|
138
245
|
|
|
246
|
+
/**
|
|
247
|
+
* Re-buckets a system whose render settings changed. The system keeps its
|
|
248
|
+
* place in the update order, so this is safe to call mid-frame.
|
|
249
|
+
*/
|
|
139
250
|
updateSystem(system: IParticleSystem) {
|
|
140
|
-
this.
|
|
251
|
+
this.removeFromBatch(system);
|
|
141
252
|
this.addSystem(system);
|
|
142
253
|
}
|
|
143
254
|
|
|
@@ -179,20 +290,90 @@ export class BatchedRenderer extends TransformNode {
|
|
|
179
290
|
if (quality === this.lastAppliedQuality) {
|
|
180
291
|
return;
|
|
181
292
|
}
|
|
182
|
-
for (
|
|
183
|
-
(
|
|
293
|
+
for (let i = 0; i < this.systems.length; i++) {
|
|
294
|
+
(this.systems[i] as ParticleSystem).setQualityFactor(quality);
|
|
184
295
|
}
|
|
185
296
|
this.lastAppliedQuality = quality;
|
|
186
297
|
}
|
|
187
298
|
|
|
299
|
+
/** @internal Called when a system's behaviors change, which can add a sub emitter. */
|
|
300
|
+
_invalidateUpdateOrder(): void {
|
|
301
|
+
this.orderedSystemsStale = true;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Orders the systems so a system emitting into another runs first.
|
|
306
|
+
*
|
|
307
|
+
* Depth first over "emits into" edges, recording each system once all the
|
|
308
|
+
* systems it feeds have been recorded, then reversed — a system therefore
|
|
309
|
+
* lands ahead of everything it emits into, however deep the chain. A cycle
|
|
310
|
+
* cannot be satisfied by any order; it is left as found rather than dropped.
|
|
311
|
+
*/
|
|
312
|
+
private orderSystems(): Array<IParticleSystem> {
|
|
313
|
+
if (!this.orderedSystemsStale) {
|
|
314
|
+
return this.orderedSystems;
|
|
315
|
+
}
|
|
316
|
+
const ordered: Array<IParticleSystem> = [];
|
|
317
|
+
const visiting = new Set<IParticleSystem>();
|
|
318
|
+
const done = new Set<IParticleSystem>();
|
|
319
|
+
const known = new Set<IParticleSystem>(this.systems);
|
|
320
|
+
|
|
321
|
+
const subTargetsOf = (system: IParticleSystem): Array<IParticleSystem> => {
|
|
322
|
+
const targets: Array<IParticleSystem> = [];
|
|
323
|
+
for (const behavior of (system as ParticleSystem).behaviors ?? []) {
|
|
324
|
+
if (behavior.type !== 'EmitSubParticleSystem') {
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
const target = (behavior as EmitSubParticleSystem).subParticleSystem?.system;
|
|
328
|
+
if (target !== undefined && known.has(target)) {
|
|
329
|
+
targets.push(target);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return targets;
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
const visit = (system: IParticleSystem): void => {
|
|
336
|
+
if (done.has(system) || visiting.has(system)) {
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
visiting.add(system);
|
|
340
|
+
for (const target of subTargetsOf(system)) {
|
|
341
|
+
visit(target);
|
|
342
|
+
}
|
|
343
|
+
visiting.delete(system);
|
|
344
|
+
done.add(system);
|
|
345
|
+
ordered.push(system);
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
for (const system of this.systems) {
|
|
349
|
+
visit(system);
|
|
350
|
+
}
|
|
351
|
+
ordered.reverse();
|
|
352
|
+
this.orderedSystems = ordered;
|
|
353
|
+
this.orderedSystemsStale = false;
|
|
354
|
+
return ordered;
|
|
355
|
+
}
|
|
356
|
+
|
|
188
357
|
update(delta: number) {
|
|
189
358
|
const adaptiveState = this.adaptivePerformanceState;
|
|
190
359
|
const frameStart = adaptiveState.enabled ? performance.now() : 0;
|
|
191
360
|
if (adaptiveState.enabled) {
|
|
192
361
|
this.applyAdaptiveQuality();
|
|
193
362
|
}
|
|
194
|
-
|
|
195
|
-
|
|
363
|
+
const systems = this.orderSystems();
|
|
364
|
+
for (let i = 0; i < systems.length; i++) {
|
|
365
|
+
const system = systems[i];
|
|
366
|
+
// A disabled emitter holds its particles frozen instead of simulating
|
|
367
|
+
// them unseen, matching how Unity treats a disabled ParticleSystem.
|
|
368
|
+
// Pooled effects parked with setEnabled(false) then cost nothing.
|
|
369
|
+
if (system.emitter.visible) {
|
|
370
|
+
(system as ParticleSystem).update(delta);
|
|
371
|
+
}
|
|
372
|
+
// An autoDestroy system disposes itself from inside update and drops
|
|
373
|
+
// out of the list; stay on the slot it vacated.
|
|
374
|
+
if (systems[i] !== system) {
|
|
375
|
+
i--;
|
|
376
|
+
}
|
|
196
377
|
}
|
|
197
378
|
this.refreshBatches();
|
|
198
379
|
if (!adaptiveState.enabled) {
|
|
@@ -217,8 +398,38 @@ export class BatchedRenderer extends TransformNode {
|
|
|
217
398
|
|
|
218
399
|
/** Rebuild instance buffers from current particle state without advancing simulation. */
|
|
219
400
|
refreshBatches(): void {
|
|
401
|
+
const emptyFrames = this.batchEmptyFrames;
|
|
220
402
|
for (let i = 0; i < this.batches.length; i++) {
|
|
221
|
-
this.batches[i]
|
|
403
|
+
const batch = this.batches[i];
|
|
404
|
+
if (batch.systems.size > 0) {
|
|
405
|
+
emptyFrames[i] = 0;
|
|
406
|
+
batch.update();
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
409
|
+
const framesEmpty = (emptyFrames[i] ?? 0) + 1;
|
|
410
|
+
emptyFrames[i] = framesEmpty;
|
|
411
|
+
if (framesEmpty === 1) {
|
|
412
|
+
// One last update clears the instance data left on screen.
|
|
413
|
+
batch.update();
|
|
414
|
+
} else if (framesEmpty > BatchedRenderer.EMPTY_BATCH_GRACE_FRAMES) {
|
|
415
|
+
// Nothing has used these settings for a while: give the mesh,
|
|
416
|
+
// material and GPU buffers back instead of re-uploading empty
|
|
417
|
+
// instance data forever.
|
|
418
|
+
this.removeBatch(i);
|
|
419
|
+
i--;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/** Disposes the batch at `index` and repairs the system→batch mapping. */
|
|
425
|
+
private removeBatch(index: number): void {
|
|
426
|
+
this.batches[index].dispose();
|
|
427
|
+
this.batches.splice(index, 1);
|
|
428
|
+
this.batchEmptyFrames.splice(index, 1);
|
|
429
|
+
for (const [system, batchIndex] of this.systemToBatchIndex) {
|
|
430
|
+
if (batchIndex > index) {
|
|
431
|
+
this.systemToBatchIndex.set(system, batchIndex - 1);
|
|
432
|
+
}
|
|
222
433
|
}
|
|
223
434
|
}
|
|
224
435
|
|
|
@@ -253,6 +464,8 @@ export class BatchedRenderer extends TransformNode {
|
|
|
253
464
|
batch.dispose();
|
|
254
465
|
}
|
|
255
466
|
this.batches = [];
|
|
467
|
+
this.batchEmptyFrames.length = 0;
|
|
468
|
+
this.systems.length = 0;
|
|
256
469
|
this.systemToBatchIndex.clear();
|
|
257
470
|
super.dispose();
|
|
258
471
|
}
|
|
@@ -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, ShapeTransform, Vector3} from 'quarks.core';
|
|
11
4
|
|
|
12
5
|
export class MeshSurfaceEmitter implements EmitterShape {
|
|
13
6
|
type = 'mesh_surface';
|
|
@@ -17,6 +10,8 @@ export class MeshSurfaceEmitter implements EmitterShape {
|
|
|
17
10
|
private _positions?: Float32Array;
|
|
18
11
|
private _indices?: Uint32Array | Int32Array;
|
|
19
12
|
private _meshReferenceId?: string;
|
|
13
|
+
/** Offset, rotation and scale of the mesh within the particle system. */
|
|
14
|
+
transform?: ShapeTransform;
|
|
20
15
|
|
|
21
16
|
get mesh() {
|
|
22
17
|
return this._mesh;
|
|
@@ -60,8 +55,9 @@ export class MeshSurfaceEmitter implements EmitterShape {
|
|
|
60
55
|
}
|
|
61
56
|
}
|
|
62
57
|
|
|
63
|
-
constructor(mesh?: Mesh, meshReferenceId?: string) {
|
|
58
|
+
constructor(mesh?: Mesh, meshReferenceId?: string, transform?: ShapeTransform) {
|
|
64
59
|
this._meshReferenceId = meshReferenceId;
|
|
60
|
+
this.transform = transform;
|
|
65
61
|
if (mesh) this.mesh = mesh;
|
|
66
62
|
}
|
|
67
63
|
|
|
@@ -69,11 +65,13 @@ export class MeshSurfaceEmitter implements EmitterShape {
|
|
|
69
65
|
if (!this._positions || !this._indices) {
|
|
70
66
|
p.position.set(0, 0, 0);
|
|
71
67
|
p.velocity.set(0, 0, 1).multiplyScalar(p.startSpeed);
|
|
68
|
+
this.transform?.apply(p);
|
|
72
69
|
return;
|
|
73
70
|
}
|
|
74
71
|
|
|
75
72
|
const triCount = this._triangleIndexToArea.length - 1;
|
|
76
|
-
let left = 0,
|
|
73
|
+
let left = 0,
|
|
74
|
+
right = triCount;
|
|
77
75
|
const target = Math.random() * this._triangleIndexToArea[triCount];
|
|
78
76
|
|
|
79
77
|
while (left + 1 < right) {
|
|
@@ -96,9 +94,15 @@ export class MeshSurfaceEmitter implements EmitterShape {
|
|
|
96
94
|
const idx1 = this._indices[left * 3 + 1];
|
|
97
95
|
const idx2 = this._indices[left * 3 + 2];
|
|
98
96
|
|
|
99
|
-
const ax = this._positions[idx0 * 3],
|
|
100
|
-
|
|
101
|
-
|
|
97
|
+
const ax = this._positions[idx0 * 3],
|
|
98
|
+
ay = this._positions[idx0 * 3 + 1],
|
|
99
|
+
az = this._positions[idx0 * 3 + 2];
|
|
100
|
+
const bx = this._positions[idx1 * 3] - ax,
|
|
101
|
+
by = this._positions[idx1 * 3 + 1] - ay,
|
|
102
|
+
bz = this._positions[idx1 * 3 + 2] - az;
|
|
103
|
+
const cx = this._positions[idx2 * 3] - ax,
|
|
104
|
+
cy = this._positions[idx2 * 3 + 1] - ay,
|
|
105
|
+
cz = this._positions[idx2 * 3 + 2] - az;
|
|
102
106
|
|
|
103
107
|
p.position.set(ax + bx * u1 + cx * u2, ay + by * u1 + cy * u2, az + bz * u1 + cz * u2);
|
|
104
108
|
|
|
@@ -107,18 +111,23 @@ export class MeshSurfaceEmitter implements EmitterShape {
|
|
|
107
111
|
const nz = bx * cy - by * cx;
|
|
108
112
|
const len = Math.sqrt(nx * nx + ny * ny + nz * nz) || 1;
|
|
109
113
|
p.velocity.set(nx / len, ny / len, nz / len).multiplyScalar(p.startSpeed);
|
|
114
|
+
this.transform?.apply(p);
|
|
110
115
|
}
|
|
111
116
|
|
|
112
117
|
toJSON(): ShapeJSON {
|
|
113
|
-
return {
|
|
118
|
+
return {
|
|
119
|
+
type: 'mesh_surface',
|
|
120
|
+
mesh: this._mesh ? (this._mesh as any).uniqueId?.toString() : (this._meshReferenceId ?? ''),
|
|
121
|
+
...ShapeTransform.toShapeJSON(this.transform),
|
|
122
|
+
};
|
|
114
123
|
}
|
|
115
124
|
|
|
116
125
|
static fromJSON(json: any): MeshSurfaceEmitter {
|
|
117
|
-
return new MeshSurfaceEmitter(undefined, json?.mesh);
|
|
126
|
+
return new MeshSurfaceEmitter(undefined, json?.mesh, ShapeTransform.fromJSON(json?.transform));
|
|
118
127
|
}
|
|
119
128
|
|
|
120
129
|
clone(): EmitterShape {
|
|
121
|
-
return new MeshSurfaceEmitter(this._mesh, this._meshReferenceId);
|
|
130
|
+
return new MeshSurfaceEmitter(this._mesh, this._meshReferenceId, this.transform?.clone());
|
|
122
131
|
}
|
|
123
132
|
|
|
124
133
|
update(_system: IParticleSystem, _delta: number): void {}
|
|
@@ -127,11 +136,13 @@ export class MeshSurfaceEmitter implements EmitterShape {
|
|
|
127
136
|
export const MeshSurfaceEmitterPlugin: Plugin = {
|
|
128
137
|
id: 'babylon.quarks',
|
|
129
138
|
initialize: () => {},
|
|
130
|
-
emitterShapes: [
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
139
|
+
emitterShapes: [
|
|
140
|
+
{
|
|
141
|
+
type: 'mesh_surface',
|
|
142
|
+
params: [['mesh', ['mesh']]],
|
|
143
|
+
constructor: MeshSurfaceEmitter,
|
|
144
|
+
loadJSON: (json: any) => MeshSurfaceEmitter.fromJSON(json),
|
|
145
|
+
},
|
|
146
|
+
],
|
|
136
147
|
behaviors: [],
|
|
137
148
|
};
|
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(),
|