babylon.quarks 0.23.0 → 0.24.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 +101 -0
- 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 +529 -20
- package/package.json +2 -2
- package/src/BatchedRenderer.ts +146 -2
- package/src/EffectPool.ts +165 -0
- package/src/MeshSurfaceEmitter.ts +4 -4
- package/src/ParticleEmitter.ts +2 -1
- package/src/ParticleSystem.ts +592 -15
- package/src/QuarksUtil.ts +55 -7
- package/src/SpriteBatch.ts +127 -3
- package/src/TrailBatch.ts +49 -12
- package/src/VFXBatch.ts +154 -3
- package/src/index.ts +4 -1
- package/src/instanceSort.ts +121 -0
- package/src/shaders/chunks/fog.ts +127 -0
- package/src/shaders/local_particle_physics_vert.glsl.ts +4 -0
- package/src/shaders/local_particle_physics_vert.wgsl.ts +4 -0
- package/src/shaders/particle_frag.glsl.ts +4 -0
- package/src/shaders/particle_frag.wgsl.ts +4 -0
- package/src/shaders/particle_physics_frag.glsl.ts +4 -0
- package/src/shaders/particle_physics_frag.wgsl.ts +4 -0
- package/src/shaders/particle_vert.glsl.ts +23 -4
- package/src/shaders/particle_vert.wgsl.ts +23 -4
- package/src/shaders/stretched_bb_particle_vert.glsl.ts +17 -0
- package/src/shaders/stretched_bb_particle_vert.wgsl.ts +18 -1
- package/src/shaders/trail_frag.glsl.ts +4 -0
- package/src/shaders/trail_frag.wgsl.ts +4 -0
- package/src/shaders/trail_vert.glsl.ts +4 -0
- package/src/shaders/trail_vert.wgsl.ts +4 -0
package/src/QuarksUtil.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import {TransformNode} from '@babylonjs/core/Meshes/transformNode';
|
|
1
2
|
import {Node} from '@babylonjs/core/node';
|
|
3
|
+
import {EmitSubParticleSystem} from 'quarks.core';
|
|
2
4
|
import {BatchedRenderer} from './BatchedRenderer';
|
|
3
5
|
import {ParticleEmitter} from './ParticleEmitter';
|
|
4
6
|
import {ParticleSystem} from './ParticleSystem';
|
|
@@ -54,24 +56,70 @@ export class QuarksUtil {
|
|
|
54
56
|
});
|
|
55
57
|
}
|
|
56
58
|
|
|
57
|
-
/**
|
|
59
|
+
/**
|
|
60
|
+
* True when every system under `root` is finished: non-looping ones that
|
|
61
|
+
* ran their course, looping ones whose emission was ended (see
|
|
62
|
+
* {@link endEmit}) and whose particles are gone. A looping system still
|
|
63
|
+
* emitting keeps the effect unfinished.
|
|
64
|
+
*/
|
|
58
65
|
static isEffectFinished(root: Node): boolean {
|
|
59
66
|
let found = false;
|
|
60
67
|
let finished = true;
|
|
61
68
|
QuarksUtil.runOnAllParticleEmitters(root, (emitter) => {
|
|
62
69
|
found = true;
|
|
63
|
-
|
|
64
|
-
if (system.looping) {
|
|
65
|
-
finished = false;
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
68
|
-
if (!system.isFinished()) {
|
|
70
|
+
if (!(emitter.system as unknown as ParticleSystem).isFinished()) {
|
|
69
71
|
finished = false;
|
|
70
72
|
}
|
|
71
73
|
});
|
|
72
74
|
return found && finished;
|
|
73
75
|
}
|
|
74
76
|
|
|
77
|
+
/**
|
|
78
|
+
* A deep copy of an effect: the node tree under `root` with every particle
|
|
79
|
+
* system cloned, sub emitter links pointing at the copies, and the same
|
|
80
|
+
* transforms and enabled state. The copy is not added to any renderer.
|
|
81
|
+
* Nodes that are neither transform nodes nor emitters are skipped.
|
|
82
|
+
*/
|
|
83
|
+
static instantiate(root: TransformNode, name?: string): TransformNode {
|
|
84
|
+
const copies = new Map<ParticleEmitter, ParticleEmitter>();
|
|
85
|
+
const copyNode = (node: Node, parent: TransformNode | null, copyName?: string): TransformNode | null => {
|
|
86
|
+
let copy: TransformNode;
|
|
87
|
+
if (node instanceof ParticleEmitter) {
|
|
88
|
+
copy = node.clone(copyName);
|
|
89
|
+
copies.set(node, copy as ParticleEmitter);
|
|
90
|
+
} else if (node instanceof TransformNode) {
|
|
91
|
+
copy = new TransformNode(copyName ?? node.name, node.getScene());
|
|
92
|
+
copy.position.copyFrom(node.position);
|
|
93
|
+
copy.rotation.copyFrom(node.rotation);
|
|
94
|
+
copy.scaling.copyFrom(node.scaling);
|
|
95
|
+
if (node.rotationQuaternion) {
|
|
96
|
+
copy.rotationQuaternion = node.rotationQuaternion.clone();
|
|
97
|
+
}
|
|
98
|
+
copy.setEnabled(node.isEnabled(false));
|
|
99
|
+
} else {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
copy.parent = parent;
|
|
103
|
+
for (const child of node.getChildren()) {
|
|
104
|
+
copyNode(child, copy);
|
|
105
|
+
}
|
|
106
|
+
return copy;
|
|
107
|
+
};
|
|
108
|
+
const copy = copyNode(root, null, name) as TransformNode;
|
|
109
|
+
// Sub emitter behaviors on the copies still target the original emitters.
|
|
110
|
+
for (const emitter of copies.values()) {
|
|
111
|
+
for (const behavior of (emitter.system as unknown as ParticleSystem).behaviors) {
|
|
112
|
+
if (behavior instanceof EmitSubParticleSystem) {
|
|
113
|
+
const target = copies.get(behavior.subParticleSystem as unknown as ParticleEmitter);
|
|
114
|
+
if (target !== undefined) {
|
|
115
|
+
behavior.subParticleSystem = target as unknown as typeof behavior.subParticleSystem;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return copy;
|
|
121
|
+
}
|
|
122
|
+
|
|
75
123
|
private static traverseNode(node: Node, callback: (node: Node) => void): void {
|
|
76
124
|
callback(node);
|
|
77
125
|
const children = node.getChildren();
|
package/src/SpriteBatch.ts
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
Vector3,
|
|
18
18
|
} from 'quarks.core';
|
|
19
19
|
import {VFXBatchSettings} from './BatchedRenderer';
|
|
20
|
+
import {gather, sortByKey, SortScratch} from './instanceSort';
|
|
20
21
|
import local_particle_physics_vert from './shaders/local_particle_physics_vert.glsl';
|
|
21
22
|
import local_particle_physics_vert_wgsl from './shaders/local_particle_physics_vert.wgsl';
|
|
22
23
|
import particle_frag from './shaders/particle_frag.glsl';
|
|
@@ -75,7 +76,7 @@ export class SpriteBatch extends VFXBatch {
|
|
|
75
76
|
setupBuffers(): void {
|
|
76
77
|
this.mesh.dispose();
|
|
77
78
|
this.mesh = new Mesh('spriteBatch', this.scene);
|
|
78
|
-
this.
|
|
79
|
+
this.configureMesh();
|
|
79
80
|
|
|
80
81
|
const vertexData = new VertexData();
|
|
81
82
|
vertexData.positions = this.settings.instancingGeometry;
|
|
@@ -153,6 +154,113 @@ export class SpriteBatch extends VFXBatch {
|
|
|
153
154
|
}
|
|
154
155
|
this.disposeExpandableBuffers();
|
|
155
156
|
this.buildExpandableBuffers();
|
|
157
|
+
this.sortScratch = undefined;
|
|
158
|
+
this.gatherScratch.length = 0;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** The screen-size limits as a uniform value, or null when the batch has none that apply. */
|
|
162
|
+
private sizeLimits(): BVector2 | null {
|
|
163
|
+
const {minParticleSize, maxParticleSize} = this.settings;
|
|
164
|
+
if (!SpriteBatch.takesPivotAndFlip(this.settings.renderMode)) {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
if (minParticleSize <= 0 && maxParticleSize === Infinity) {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
// The shader clamps; an unlimited maximum is just a very large one.
|
|
171
|
+
return new BVector2(Math.max(0, minParticleSize), Math.min(maxParticleSize, 1e9));
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
private sortScratch?: SortScratch;
|
|
175
|
+
/** One spare array per attribute buffer, gathered into and swapped in; indexed by buffer slot. */
|
|
176
|
+
private readonly gatherScratch: Float32Array[] = [];
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Reorders the `count` filled instances by the batch's sort mode, so that
|
|
180
|
+
* the later (covering) draws are the nearer or the chosen-age particles.
|
|
181
|
+
* The order is computed once as a permutation and every attribute buffer
|
|
182
|
+
* is gathered through it into a spare array that then takes its place.
|
|
183
|
+
*/
|
|
184
|
+
private sortInstances(count: number, isMeshRender: boolean, isStretchedRender: boolean): void {
|
|
185
|
+
if (count < 2) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
const scratch = (this.sortScratch ??= new SortScratch(this.maxParticles));
|
|
189
|
+
scratch.ensure(count);
|
|
190
|
+
const keys = scratch.keys;
|
|
191
|
+
const mode = this.settings.sortMode;
|
|
192
|
+
if (mode === 'distance') {
|
|
193
|
+
const camera = this.scene.activeCameras?.[0] ?? this.scene.activeCamera;
|
|
194
|
+
if (!camera) {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
const eye = camera.globalPosition;
|
|
198
|
+
const ex = eye.x;
|
|
199
|
+
const ey = eye.y;
|
|
200
|
+
const ez = eye.z;
|
|
201
|
+
const offsets = this.offsetBuffer;
|
|
202
|
+
// Farthest first: the nearer particle is drawn last and covers.
|
|
203
|
+
for (let i = 0, o = 0; i < count; i++, o += 3) {
|
|
204
|
+
const dx = offsets[o] - ex;
|
|
205
|
+
const dy = offsets[o + 1] - ey;
|
|
206
|
+
const dz = offsets[o + 2] - ez;
|
|
207
|
+
keys[i] = scratch.floatKey(dx * dx + dy * dy + dz * dz, true);
|
|
208
|
+
}
|
|
209
|
+
} else {
|
|
210
|
+
// Ages in the order the systems were filled, which is the instance order.
|
|
211
|
+
const youngestInFront = mode === 'youngestInFront';
|
|
212
|
+
const systems = this.visibleSystems;
|
|
213
|
+
let i = 0;
|
|
214
|
+
for (let s = 0; s < systems.length; s++) {
|
|
215
|
+
const system = systems[s];
|
|
216
|
+
const particleNum = system.particleNum;
|
|
217
|
+
const store = system.store;
|
|
218
|
+
if (particleNum === 0) {
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
if (store === undefined) {
|
|
222
|
+
const particles = system.particles;
|
|
223
|
+
for (let j = 0; j < particleNum; j++) {
|
|
224
|
+
keys[i++] = scratch.floatKey(particles[j].age, youngestInFront);
|
|
225
|
+
}
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
const scalars = store.scalars;
|
|
229
|
+
const stride = ParticleStore.SCALAR_STRIDE;
|
|
230
|
+
for (let j = 0, a = ParticleStore.AGE; j < particleNum; j++, a += stride) {
|
|
231
|
+
keys[i++] = scratch.floatKey(scalars[a], youngestInFront);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
if (i !== count) {
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
const order = sortByKey(scratch, count);
|
|
239
|
+
this.offsetBuffer = this.gatherInto(0, this.offsetBuffer, order, count, 3);
|
|
240
|
+
this.sizeBuffer = this.gatherInto(1, this.sizeBuffer, order, count, 3);
|
|
241
|
+
this.colorBuffer = this.gatherInto(2, this.colorBuffer, order, count, 4);
|
|
242
|
+
this.uvTileBuffer = this.gatherInto(3, this.uvTileBuffer, order, count, 1);
|
|
243
|
+
this.rotationBuffer = this.gatherInto(4, this.rotationBuffer, order, count, isMeshRender ? 4 : 1);
|
|
244
|
+
if (isStretchedRender && this.velocityBuffer) {
|
|
245
|
+
this.velocityBuffer = this.gatherInto(5, this.velocityBuffer, order, count, 4);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** Gathers `source` in `order` into the spare array of `slot` and returns it; the source becomes the spare. */
|
|
250
|
+
private gatherInto(
|
|
251
|
+
slot: number,
|
|
252
|
+
source: Float32Array,
|
|
253
|
+
order: Uint32Array,
|
|
254
|
+
count: number,
|
|
255
|
+
stride: number
|
|
256
|
+
): Float32Array {
|
|
257
|
+
let spare = this.gatherScratch[slot];
|
|
258
|
+
if (spare === undefined || spare.length !== source.length) {
|
|
259
|
+
spare = new Float32Array(source.length);
|
|
260
|
+
}
|
|
261
|
+
gather(source, spare, order, count, stride);
|
|
262
|
+
this.gatherScratch[slot] = source;
|
|
263
|
+
return spare;
|
|
156
264
|
}
|
|
157
265
|
|
|
158
266
|
rebuildMaterial(): void {
|
|
@@ -161,7 +269,7 @@ export class SpriteBatch extends VFXBatch {
|
|
|
161
269
|
// Effect.ShadersStore and defeat Babylon's compiled-effect cache.
|
|
162
270
|
const shaderName = `quarksParticle_${this.settings.renderMode}`;
|
|
163
271
|
this.lastStretchedSpeedFactor = Number.NaN;
|
|
164
|
-
const defines: string[] =
|
|
272
|
+
const defines: string[] = this.fogDefines();
|
|
165
273
|
|
|
166
274
|
let vertexShader: ShaderSources;
|
|
167
275
|
let fragmentShader: ShaderSources;
|
|
@@ -218,6 +326,10 @@ export class SpriteBatch extends VFXBatch {
|
|
|
218
326
|
if (pivot) {
|
|
219
327
|
defines.push('PIVOT');
|
|
220
328
|
}
|
|
329
|
+
const sizeLimits = this.sizeLimits();
|
|
330
|
+
if (sizeLimits) {
|
|
331
|
+
defines.push('SIZE_LIMITS');
|
|
332
|
+
}
|
|
221
333
|
const isMesh = this.settings.renderMode === RenderMode.Mesh;
|
|
222
334
|
const lighting = this.settings.lighting ?? 'fixed';
|
|
223
335
|
const lit = isMesh && lighting !== 'unlit';
|
|
@@ -244,7 +356,7 @@ export class SpriteBatch extends VFXBatch {
|
|
|
244
356
|
attributes.push('velocity');
|
|
245
357
|
}
|
|
246
358
|
|
|
247
|
-
const uniforms = ['world', 'view', 'projection', 'worldView', 'worldViewProjection'];
|
|
359
|
+
const uniforms = ['world', 'view', 'projection', 'worldView', 'worldViewProjection', ...VFXBatch.FOG_UNIFORMS];
|
|
248
360
|
const samplers: string[] = [];
|
|
249
361
|
|
|
250
362
|
if (this.settings.uTileCount > 1 || this.settings.vTileCount > 1) {
|
|
@@ -279,6 +391,9 @@ export class SpriteBatch extends VFXBatch {
|
|
|
279
391
|
if (pivot) {
|
|
280
392
|
uniforms.push('pivot');
|
|
281
393
|
}
|
|
394
|
+
if (sizeLimits) {
|
|
395
|
+
uniforms.push('sizeLimits');
|
|
396
|
+
}
|
|
282
397
|
if (lit) {
|
|
283
398
|
uniforms.push('lightDirection');
|
|
284
399
|
uniforms.push('lightColor');
|
|
@@ -354,6 +469,9 @@ export class SpriteBatch extends VFXBatch {
|
|
|
354
469
|
if (pivot) {
|
|
355
470
|
mat.setVector3('pivot', new BVector3(pivot.x, pivot.y, pivot.z));
|
|
356
471
|
}
|
|
472
|
+
if (sizeLimits) {
|
|
473
|
+
mat.setVector2('sizeLimits', sizeLimits);
|
|
474
|
+
}
|
|
357
475
|
if (lit) {
|
|
358
476
|
this.writeFixedLighting(mat);
|
|
359
477
|
if (lighting === 'scene') {
|
|
@@ -372,6 +490,7 @@ export class SpriteBatch extends VFXBatch {
|
|
|
372
490
|
}
|
|
373
491
|
}
|
|
374
492
|
|
|
493
|
+
this.bindFog(mat);
|
|
375
494
|
mat.backFaceCulling = false;
|
|
376
495
|
|
|
377
496
|
if (this.settings.materialTransparent) {
|
|
@@ -801,6 +920,7 @@ export class SpriteBatch extends VFXBatch {
|
|
|
801
920
|
// Keep the mesh enabled even when count is 0 — toggling visibility off for a
|
|
802
921
|
// single frame (common with short lifetimes + low emission overlap) causes flicker.
|
|
803
922
|
this.mesh.forcedInstanceCount = index;
|
|
923
|
+
this.refreshBoundingInfo();
|
|
804
924
|
|
|
805
925
|
if (index === 0) {
|
|
806
926
|
// Slot 0 only needs clearing once per stretch of empty frames; re-uploading it every
|
|
@@ -813,6 +933,10 @@ export class SpriteBatch extends VFXBatch {
|
|
|
813
933
|
}
|
|
814
934
|
this.clearedInstanceUploaded = false;
|
|
815
935
|
|
|
936
|
+
if (this.settings.sortMode !== 'none') {
|
|
937
|
+
this.sortInstances(index, isMeshRender, isStretchedRender);
|
|
938
|
+
}
|
|
939
|
+
|
|
816
940
|
// Upload only the slots actually filled this frame: the buffers are sized
|
|
817
941
|
// for the high-water mark and are usually far larger than the live count.
|
|
818
942
|
this.offsetVB.update(this.offsetBuffer.subarray(0, index * 3));
|
package/src/TrailBatch.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import {VertexBuffer} from '@babylonjs/core/Buffers/buffer';
|
|
2
|
-
import {BoundingInfo} from '@babylonjs/core/Culling/boundingInfo';
|
|
3
2
|
import {Constants} from '@babylonjs/core/Engines/constants';
|
|
4
3
|
import {ShaderMaterial} from '@babylonjs/core/Materials/shaderMaterial';
|
|
5
|
-
import {Vector2 as BVector2
|
|
4
|
+
import {Vector2 as BVector2} from '@babylonjs/core/Maths/math.vector';
|
|
6
5
|
import {Mesh} from '@babylonjs/core/Meshes/mesh';
|
|
7
6
|
import {Scene} from '@babylonjs/core/scene';
|
|
8
7
|
import {IParticleSystem, Matrix4, Quaternion, TrailParticle, TrailSettings, Vector3} from 'quarks.core';
|
|
9
8
|
import {VFXBatchSettings} from './BatchedRenderer';
|
|
9
|
+
import type {ParticleSystem} from './ParticleSystem';
|
|
10
10
|
import {registerShaders, shaderLanguageFor} from './shaders/shaderLanguageSupport';
|
|
11
11
|
import trail_frag from './shaders/trail_frag.glsl';
|
|
12
12
|
import trail_frag_wgsl from './shaders/trail_frag.wgsl';
|
|
@@ -43,7 +43,7 @@ export class TrailBatch extends VFXBatch {
|
|
|
43
43
|
setupBuffers(): void {
|
|
44
44
|
this.mesh.dispose();
|
|
45
45
|
this.mesh = new Mesh('trailBatch', this.scene);
|
|
46
|
-
this.
|
|
46
|
+
this.configureMesh();
|
|
47
47
|
this.buildGeometryBuffers();
|
|
48
48
|
}
|
|
49
49
|
|
|
@@ -110,15 +110,10 @@ export class TrailBatch extends VFXBatch {
|
|
|
110
110
|
this.mesh.setVerticesBuffer(this.sideVB);
|
|
111
111
|
this.mesh.setVerticesBuffer(this.widthVB);
|
|
112
112
|
|
|
113
|
-
//
|
|
114
|
-
|
|
115
|
-
const min = new BVector3(-10000, -10000, -10000);
|
|
116
|
-
const max = new BVector3(10000, 10000, 10000);
|
|
117
|
-
this.mesh.setBoundingInfo(new BoundingInfo(min, max));
|
|
113
|
+
// The bounds come from the ribbons each fill writes (configureMesh set
|
|
114
|
+
// the placeholder); the sub meshes have to share them.
|
|
118
115
|
if (this.mesh.subMeshes) {
|
|
119
|
-
|
|
120
|
-
(sub as any)._boundingInfo = this.mesh.getBoundingInfo();
|
|
121
|
-
}
|
|
116
|
+
this.adoptMeshBounds();
|
|
122
117
|
}
|
|
123
118
|
}
|
|
124
119
|
|
|
@@ -139,7 +134,7 @@ export class TrailBatch extends VFXBatch {
|
|
|
139
134
|
// Stable name: the trail shader source never varies, so registering it once
|
|
140
135
|
// keeps Effect.ShadersStore bounded and lets Babylon reuse compiled effects.
|
|
141
136
|
const shaderName = 'quarksTrail';
|
|
142
|
-
const defines: string[] =
|
|
137
|
+
const defines: string[] = this.fogDefines();
|
|
143
138
|
|
|
144
139
|
if (this.settings.texture) {
|
|
145
140
|
defines.push('USE_MAP');
|
|
@@ -162,6 +157,7 @@ export class TrailBatch extends VFXBatch {
|
|
|
162
157
|
'lineWidth',
|
|
163
158
|
'resolution',
|
|
164
159
|
'sizeAttenuation',
|
|
160
|
+
...VFXBatch.FOG_UNIFORMS,
|
|
165
161
|
];
|
|
166
162
|
const samplers: string[] = [];
|
|
167
163
|
|
|
@@ -203,6 +199,7 @@ export class TrailBatch extends VFXBatch {
|
|
|
203
199
|
}
|
|
204
200
|
});
|
|
205
201
|
|
|
202
|
+
this.bindFog(mat);
|
|
206
203
|
mat.backFaceCulling = false;
|
|
207
204
|
|
|
208
205
|
if (this.settings.materialTransparent) {
|
|
@@ -266,6 +263,7 @@ export class TrailBatch extends VFXBatch {
|
|
|
266
263
|
|
|
267
264
|
for (let s = 0; s < visibleSystems.length; s++) {
|
|
268
265
|
const system = visibleSystems[s];
|
|
266
|
+
const firstVertex = index;
|
|
269
267
|
TrailBatch.ensureWorldMatrix(system);
|
|
270
268
|
const rotation = this.quaternion_;
|
|
271
269
|
const translation = this.vector2_;
|
|
@@ -554,11 +552,50 @@ export class TrailBatch extends VFXBatch {
|
|
|
554
552
|
index += 2;
|
|
555
553
|
}
|
|
556
554
|
}
|
|
555
|
+
this.measureSystemBounds(system as ParticleSystem, firstVertex, index);
|
|
557
556
|
}
|
|
558
557
|
|
|
558
|
+
this.refreshBoundingInfo();
|
|
559
559
|
this.uploadGeometry(index, triangles);
|
|
560
560
|
}
|
|
561
561
|
|
|
562
|
+
/**
|
|
563
|
+
* Records a system's world bounds from the ribbon vertices just written for
|
|
564
|
+
* it, padded by the widest ribbon. The store only holds each trail's head,
|
|
565
|
+
* so this is where a trail's extent is known.
|
|
566
|
+
*/
|
|
567
|
+
private measureSystemBounds(system: ParticleSystem, fromVertex: number, toVertex: number): void {
|
|
568
|
+
if (toVertex <= fromVertex || typeof system.setWorldBounds !== 'function') {
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
const positions = this.positionBuffer;
|
|
572
|
+
let minX = Infinity;
|
|
573
|
+
let minY = Infinity;
|
|
574
|
+
let minZ = Infinity;
|
|
575
|
+
let maxX = -Infinity;
|
|
576
|
+
let maxY = -Infinity;
|
|
577
|
+
let maxZ = -Infinity;
|
|
578
|
+
// Each sample writes its position twice (both ribbon edges); one of the pair is enough.
|
|
579
|
+
for (let o = fromVertex * 3, end = toVertex * 3; o < end; o += 6) {
|
|
580
|
+
const x = positions[o];
|
|
581
|
+
const y = positions[o + 1];
|
|
582
|
+
const z = positions[o + 2];
|
|
583
|
+
if (x < minX) minX = x;
|
|
584
|
+
if (x > maxX) maxX = x;
|
|
585
|
+
if (y < minY) minY = y;
|
|
586
|
+
if (y > maxY) maxY = y;
|
|
587
|
+
if (z < minZ) minZ = z;
|
|
588
|
+
if (z > maxZ) maxZ = z;
|
|
589
|
+
}
|
|
590
|
+
const widths = this.widthBuffer;
|
|
591
|
+
let maxWidth = 0;
|
|
592
|
+
for (let v = fromVertex; v < toVertex; v += 2) {
|
|
593
|
+
const w = widths[v];
|
|
594
|
+
if (w > maxWidth) maxWidth = w;
|
|
595
|
+
}
|
|
596
|
+
system.setWorldBounds(minX, minY, minZ, maxX, maxY, maxZ, maxWidth);
|
|
597
|
+
}
|
|
598
|
+
|
|
562
599
|
/**
|
|
563
600
|
* Uploads the vertex data written by `update` and resizes the sub mesh.
|
|
564
601
|
*
|
package/src/VFXBatch.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import {BoundingInfo} from '@babylonjs/core/Culling/boundingInfo';
|
|
2
|
+
import {Constants} from '@babylonjs/core/Engines/constants';
|
|
1
3
|
import {BaseTexture} from '@babylonjs/core/Materials/Textures/baseTexture';
|
|
2
4
|
import {ShaderMaterial} from '@babylonjs/core/Materials/shaderMaterial';
|
|
3
5
|
import {Color3} from '@babylonjs/core/Maths/math.color';
|
|
@@ -6,6 +8,20 @@ import {Mesh} from '@babylonjs/core/Meshes/mesh';
|
|
|
6
8
|
import {Scene} from '@babylonjs/core/scene';
|
|
7
9
|
import {IParticleSystem, Vector3} from 'quarks.core';
|
|
8
10
|
import {VFXBatchSettings} from './BatchedRenderer';
|
|
11
|
+
import type {ParticleSystem} from './ParticleSystem';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The order the particles of a batch are drawn in (Unity's Sort Mode). Alpha
|
|
15
|
+
* blended particles cover what was drawn before them, so the order shows.
|
|
16
|
+
*
|
|
17
|
+
* - `'none'`: as they are, system by system. Right for additive blending.
|
|
18
|
+
* - `'distance'`: farthest from the active camera first, so nearer particles
|
|
19
|
+
* cover farther ones.
|
|
20
|
+
* - `'oldestInFront'` / `'youngestInFront'`: by age.
|
|
21
|
+
*
|
|
22
|
+
* Sorting runs over the whole batch after each fill. Trails are not sorted.
|
|
23
|
+
*/
|
|
24
|
+
export type SortMode = 'none' | 'distance' | 'oldestInFront' | 'youngestInFront';
|
|
9
25
|
|
|
10
26
|
export enum RenderMode {
|
|
11
27
|
BillBoard = 0,
|
|
@@ -130,8 +146,18 @@ export interface StoredBatchSettings {
|
|
|
130
146
|
reflectionFaces: BaseTexture[] | null;
|
|
131
147
|
reflectionAtlas: BaseTexture | null;
|
|
132
148
|
layerMask: number;
|
|
149
|
+
/** Babylon rendering group the batch mesh draws in, see {@link VFXBatchSettings.renderingGroupId}. */
|
|
150
|
+
renderingGroupId: number;
|
|
151
|
+
/** Whether the scene's fog is applied, see {@link VFXBatchSettings.applyFog}. */
|
|
152
|
+
applyFog: boolean;
|
|
133
153
|
/** Billboard pivot in particle-size units, see {@link VFXBatchSettings.pivot}. */
|
|
134
154
|
pivot?: Vector3;
|
|
155
|
+
/** Draw order within the batch, see {@link SortMode}. */
|
|
156
|
+
sortMode: SortMode;
|
|
157
|
+
/** Smallest size a particle may have on screen, see {@link VFXBatchSettings.minParticleSize}. */
|
|
158
|
+
minParticleSize: number;
|
|
159
|
+
/** Largest size a particle may have on screen, see {@link VFXBatchSettings.maxParticleSize}. */
|
|
160
|
+
maxParticleSize: number;
|
|
135
161
|
/** Mesh particle lighting, see {@link VFXBatchSettings.lighting}. */
|
|
136
162
|
lighting?: MeshLighting;
|
|
137
163
|
/** Mesh particle rim, see {@link VFXBatchSettings.rim}. */
|
|
@@ -144,7 +170,7 @@ export abstract class VFXBatch {
|
|
|
144
170
|
settings: StoredBatchSettings;
|
|
145
171
|
protected maxParticles: number;
|
|
146
172
|
protected scene: Scene;
|
|
147
|
-
|
|
173
|
+
protected readonly visibleSystems: IParticleSystem[] = [];
|
|
148
174
|
|
|
149
175
|
protected constructor(settings: VFXBatchSettings, scene: Scene) {
|
|
150
176
|
this.scene = scene;
|
|
@@ -174,12 +200,135 @@ export abstract class VFXBatch {
|
|
|
174
200
|
reflectionFaces: settings.reflectionFaces ?? null,
|
|
175
201
|
reflectionAtlas: settings.reflectionAtlas ?? null,
|
|
176
202
|
pivot: settings.pivot?.clone(),
|
|
203
|
+
sortMode: settings.sortMode ?? 'none',
|
|
204
|
+
minParticleSize: settings.minParticleSize ?? 0,
|
|
205
|
+
maxParticleSize: settings.maxParticleSize ?? Infinity,
|
|
177
206
|
layerMask: settings.layerMask,
|
|
207
|
+
renderingGroupId: settings.renderingGroupId ?? 0,
|
|
208
|
+
applyFog: settings.applyFog ?? true,
|
|
178
209
|
lighting: settings.lighting,
|
|
179
210
|
rim: normalizeMeshRim(settings.rim),
|
|
180
211
|
};
|
|
181
212
|
this.mesh = new Mesh('vfxBatch', scene);
|
|
182
|
-
this.
|
|
213
|
+
this.configureMesh();
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Applies the batch-level scene settings to {@link mesh}. Called on every
|
|
218
|
+
* mesh the batch creates, since the buffers rebuild it.
|
|
219
|
+
*
|
|
220
|
+
* The bounds of a batch mesh are not kept, so Babylon cannot sort it among
|
|
221
|
+
* the scene's transparent meshes by distance; `alphaIndex` from the
|
|
222
|
+
* system's render order puts it where the author said, and the rendering
|
|
223
|
+
* group and layer mask reach the mesh at all (they used to stop at the
|
|
224
|
+
* settings).
|
|
225
|
+
*/
|
|
226
|
+
protected configureMesh(): void {
|
|
227
|
+
const mesh = this.mesh;
|
|
228
|
+
// Until the first fill measures the particles, the batch has no bounds
|
|
229
|
+
// to cull by and is drawn regardless. The instance data is already in
|
|
230
|
+
// world space, so the bounds never need the mesh's own matrix.
|
|
231
|
+
mesh.alwaysSelectAsActiveMesh = true;
|
|
232
|
+
mesh.doNotSyncBoundingInfo = true;
|
|
233
|
+
mesh.setBoundingInfo(new BoundingInfo(BVector3.Zero(), BVector3.Zero()));
|
|
234
|
+
mesh.layerMask = this.settings.layerMask;
|
|
235
|
+
mesh.renderingGroupId = this.settings.renderingGroupId;
|
|
236
|
+
mesh.alphaIndex = this.settings.renderOrder;
|
|
237
|
+
// The shaders fog themselves; Babylon's own fog pass does not apply to a ShaderMaterial.
|
|
238
|
+
mesh.applyFog = false;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
private readonly boundsMin_ = new Vector3();
|
|
242
|
+
private readonly boundsMax_ = new Vector3();
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Sets the batch mesh's bounds to the union of its filled systems' world
|
|
246
|
+
* bounds, so Babylon skips the draw when none of them is in view. Called
|
|
247
|
+
* after each fill, over the systems {@link getVisibleSystems} last returned.
|
|
248
|
+
* A batch whose systems have no measured bounds stays always drawn.
|
|
249
|
+
*/
|
|
250
|
+
protected refreshBoundingInfo(): void {
|
|
251
|
+
const min = this.boundsMin_;
|
|
252
|
+
const max = this.boundsMax_;
|
|
253
|
+
let minX = Infinity;
|
|
254
|
+
let minY = Infinity;
|
|
255
|
+
let minZ = Infinity;
|
|
256
|
+
let maxX = -Infinity;
|
|
257
|
+
let maxY = -Infinity;
|
|
258
|
+
let maxZ = -Infinity;
|
|
259
|
+
let any = false;
|
|
260
|
+
const systems = this.visibleSystems;
|
|
261
|
+
for (let i = 0; i < systems.length; i++) {
|
|
262
|
+
const system = systems[i] as ParticleSystem;
|
|
263
|
+
if (system.particleNum === 0 || typeof system.getWorldBounds !== 'function') {
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
if (!system.getWorldBounds(min, max)) {
|
|
267
|
+
// Drawn but not measured: the batch cannot be culled this frame.
|
|
268
|
+
this.mesh.alwaysSelectAsActiveMesh = true;
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
any = true;
|
|
272
|
+
if (min.x < minX) minX = min.x;
|
|
273
|
+
if (min.y < minY) minY = min.y;
|
|
274
|
+
if (min.z < minZ) minZ = min.z;
|
|
275
|
+
if (max.x > maxX) maxX = max.x;
|
|
276
|
+
if (max.y > maxY) maxY = max.y;
|
|
277
|
+
if (max.z > maxZ) maxZ = max.z;
|
|
278
|
+
}
|
|
279
|
+
if (!any) {
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
const info = this.mesh.getBoundingInfo();
|
|
283
|
+
const bmin = info.minimum;
|
|
284
|
+
const bmax = info.maximum;
|
|
285
|
+
if (
|
|
286
|
+
bmin.x !== minX ||
|
|
287
|
+
bmin.y !== minY ||
|
|
288
|
+
bmin.z !== minZ ||
|
|
289
|
+
bmax.x !== maxX ||
|
|
290
|
+
bmax.y !== maxY ||
|
|
291
|
+
bmax.z !== maxZ
|
|
292
|
+
) {
|
|
293
|
+
bmin.set(minX, minY, minZ);
|
|
294
|
+
bmax.set(maxX, maxY, maxZ);
|
|
295
|
+
info.reConstruct(bmin, bmax);
|
|
296
|
+
}
|
|
297
|
+
this.mesh.alwaysSelectAsActiveMesh = false;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Fog defines and uniforms for a material of this batch, and the per-bind
|
|
302
|
+
* upload of the scene's current fog. Mode 0 (none) when the scene has fog
|
|
303
|
+
* disabled, so toggling fog at runtime needs no recompile.
|
|
304
|
+
*/
|
|
305
|
+
protected static readonly FOG_UNIFORMS = ['vFogInfos', 'vFogColor'];
|
|
306
|
+
|
|
307
|
+
protected fogDefines(): string[] {
|
|
308
|
+
if (!this.settings.applyFog) {
|
|
309
|
+
return [];
|
|
310
|
+
}
|
|
311
|
+
const additive =
|
|
312
|
+
this.settings.materialTransparent &&
|
|
313
|
+
(this.settings.materialBlendMode === Constants.ALPHA_ADD ||
|
|
314
|
+
this.settings.materialBlendMode === Constants.ALPHA_ONEONE);
|
|
315
|
+
return additive ? ['FOG', 'FOG_ADDITIVE'] : ['FOG'];
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
protected bindFog(material: ShaderMaterial): void {
|
|
319
|
+
if (!this.settings.applyFog) {
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
const scene = this.scene;
|
|
323
|
+
material.onBindObservable.add(() => {
|
|
324
|
+
const effect = material.getEffect();
|
|
325
|
+
if (!effect) {
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
const mode = scene.fogEnabled ? scene.fogMode : Scene.FOGMODE_NONE;
|
|
329
|
+
effect.setFloat4('vFogInfos', mode, scene.fogStart, scene.fogEnd, scene.fogDensity);
|
|
330
|
+
effect.setColor3('vFogColor', scene.fogColor);
|
|
331
|
+
});
|
|
183
332
|
}
|
|
184
333
|
|
|
185
334
|
addSystem(system: IParticleSystem) {
|
|
@@ -199,7 +348,9 @@ export abstract class VFXBatch {
|
|
|
199
348
|
const visibleSystems = this.visibleSystems;
|
|
200
349
|
let count = 0;
|
|
201
350
|
for (const system of this.systems) {
|
|
202
|
-
|
|
351
|
+
// A culled system's particles are off every camera's screen; filling
|
|
352
|
+
// them in would only cost the copy and widen the batch's bounds.
|
|
353
|
+
if (system.emitter.visible && (system as ParticleSystem).culled !== true) {
|
|
203
354
|
visibleSystems[count++] = system;
|
|
204
355
|
}
|
|
205
356
|
}
|
package/src/index.ts
CHANGED
|
@@ -6,6 +6,8 @@ export * from 'quarks.core';
|
|
|
6
6
|
export {BatchedParticleRenderer} from './BatchedParticleRenderer';
|
|
7
7
|
export {BatchedRenderer} from './BatchedRenderer';
|
|
8
8
|
export type {AdaptivePerformanceOptions, AdaptivePerformanceState, VFXBatchSettings} from './BatchedRenderer';
|
|
9
|
+
export {EffectPool} from './EffectPool';
|
|
10
|
+
export type {EffectPoolOptions} from './EffectPool';
|
|
9
11
|
export {
|
|
10
12
|
cacheEnvAtlas,
|
|
11
13
|
createEnvAtlasFromFaceUrls,
|
|
@@ -21,6 +23,7 @@ export type {
|
|
|
21
23
|
BabylonMetaData,
|
|
22
24
|
BurstParameters,
|
|
23
25
|
BurstParametersJSON,
|
|
26
|
+
CullingMode,
|
|
24
27
|
ParticleSystemJSONParameters,
|
|
25
28
|
ParticleSystemParameters,
|
|
26
29
|
} from './ParticleSystem';
|
|
@@ -39,7 +42,7 @@ export * from './shaders/';
|
|
|
39
42
|
export {SpriteBatch} from './SpriteBatch';
|
|
40
43
|
export {TrailBatch} from './TrailBatch';
|
|
41
44
|
export {defaultMeshLighting, meshRimEqual, normalizeMeshRim, RenderMode, VFXBatch} from './VFXBatch';
|
|
42
|
-
export type {MeshLighting, MeshLightingSettings, MeshRim, StoredBatchSettings} from './VFXBatch';
|
|
45
|
+
export type {MeshLighting, MeshLightingSettings, MeshRim, SortMode, StoredBatchSettings} from './VFXBatch';
|
|
43
46
|
|
|
44
47
|
// Re-export quarks.core types (interfaces - type-only)
|
|
45
48
|
export type {
|