babylon.quarks 0.24.1 → 0.26.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 +124 -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 +501 -9
- package/package.json +1 -1
- package/src/BatchedRenderer.ts +34 -1
- package/src/ParticleSystem.ts +465 -13
- package/src/QuarksLoader.ts +50 -2
- package/src/QuarksUtil.ts +97 -1
- package/src/SpriteBatch.ts +101 -10
- package/src/TrailBatch.ts +132 -20
- package/src/VFXBatch.ts +465 -4
- package/src/index.ts +38 -2
- package/src/shaders/chunks/surface.ts +268 -0
- package/src/shaders/local_particle_physics_vert.glsl.ts +15 -0
- package/src/shaders/local_particle_physics_vert.wgsl.ts +15 -0
- package/src/shaders/particle_frag.glsl.ts +6 -11
- package/src/shaders/particle_frag.wgsl.ts +6 -11
- package/src/shaders/particle_physics_frag.glsl.ts +13 -15
- package/src/shaders/particle_physics_frag.wgsl.ts +13 -15
- package/src/shaders/particle_vert.glsl.ts +7 -0
- package/src/shaders/particle_vert.wgsl.ts +7 -0
- package/src/shaders/stretched_bb_particle_vert.glsl.ts +27 -7
- package/src/shaders/stretched_bb_particle_vert.wgsl.ts +32 -9
- package/src/shaders/trail_frag.glsl.ts +6 -7
- package/src/shaders/trail_frag.wgsl.ts +6 -7
- package/src/shaders/trail_vert.glsl.ts +8 -0
- package/src/shaders/trail_vert.wgsl.ts +8 -0
package/src/QuarksLoader.ts
CHANGED
|
@@ -75,6 +75,7 @@ interface ParsedGeometry {
|
|
|
75
75
|
positions: Float32Array;
|
|
76
76
|
indices: Uint32Array | Uint16Array;
|
|
77
77
|
uvs?: Float32Array;
|
|
78
|
+
uvs2?: Float32Array;
|
|
78
79
|
normals?: Float32Array;
|
|
79
80
|
}
|
|
80
81
|
|
|
@@ -101,6 +102,7 @@ function geometryKey(geometry: ParsedGeometry): string {
|
|
|
101
102
|
geometry.positions,
|
|
102
103
|
geometry.indices,
|
|
103
104
|
geometry.uvs,
|
|
105
|
+
geometry.uvs2,
|
|
104
106
|
geometry.normals,
|
|
105
107
|
];
|
|
106
108
|
let lengths = '';
|
|
@@ -313,6 +315,7 @@ export class QuarksLoader {
|
|
|
313
315
|
positions: new Float32Array(geom.positions ?? []),
|
|
314
316
|
indices: geom.indices ? new Uint32Array(geom.indices) : new Uint32Array(0),
|
|
315
317
|
uvs: geom.uvs ? new Float32Array(geom.uvs) : undefined,
|
|
318
|
+
uvs2: geom.uvs2 ? new Float32Array(geom.uvs2) : undefined,
|
|
316
319
|
normals: geom.normals ? new Float32Array(geom.normals) : undefined,
|
|
317
320
|
});
|
|
318
321
|
}
|
|
@@ -427,7 +430,10 @@ export class QuarksLoader {
|
|
|
427
430
|
let positions = new Float32Array(0);
|
|
428
431
|
let indices: Uint32Array | Uint16Array = new Uint32Array(0);
|
|
429
432
|
let uvs: Float32Array | undefined;
|
|
433
|
+
let uvs2: Float32Array | undefined;
|
|
430
434
|
let normals: Float32Array | undefined;
|
|
435
|
+
// three.js names the second UV set `uv1` (r151 on), `uv2` before.
|
|
436
|
+
const uv2Attribute = data.attributes?.uv1 ?? data.attributes?.uv2;
|
|
431
437
|
|
|
432
438
|
if (data.interleavedBuffers && data.arrayBuffers) {
|
|
433
439
|
const buffers: {[uuid: string]: {data: Float32Array; stride: number}} = {};
|
|
@@ -453,6 +459,16 @@ export class QuarksLoader {
|
|
|
453
459
|
const buf = buffers[attr.data];
|
|
454
460
|
if (buf) uvs = this.extractInterleavedAttribute(buf.data, buf.stride, attr.offset, attr.itemSize);
|
|
455
461
|
}
|
|
462
|
+
if (uv2Attribute?.isInterleavedBufferAttribute) {
|
|
463
|
+
const buf = buffers[uv2Attribute.data];
|
|
464
|
+
if (buf)
|
|
465
|
+
uvs2 = this.extractInterleavedAttribute(
|
|
466
|
+
buf.data,
|
|
467
|
+
buf.stride,
|
|
468
|
+
uv2Attribute.offset,
|
|
469
|
+
uv2Attribute.itemSize
|
|
470
|
+
);
|
|
471
|
+
}
|
|
456
472
|
if (data.attributes.normal?.isInterleavedBufferAttribute) {
|
|
457
473
|
const attr = data.attributes.normal;
|
|
458
474
|
const buf = buffers[attr.data];
|
|
@@ -467,6 +483,9 @@ export class QuarksLoader {
|
|
|
467
483
|
if (data.attributes.uv?.array) {
|
|
468
484
|
uvs = new Float32Array(data.attributes.uv.array);
|
|
469
485
|
}
|
|
486
|
+
if (uv2Attribute?.array) {
|
|
487
|
+
uvs2 = new Float32Array(uv2Attribute.array);
|
|
488
|
+
}
|
|
470
489
|
if (data.attributes.normal?.array) {
|
|
471
490
|
normals = new Float32Array(data.attributes.normal.array);
|
|
472
491
|
}
|
|
@@ -482,7 +501,7 @@ export class QuarksLoader {
|
|
|
482
501
|
|
|
483
502
|
indices = ensureTriangleIndices(positions, indices);
|
|
484
503
|
|
|
485
|
-
return {positions, indices, uvs, normals};
|
|
504
|
+
return {positions, indices, uvs, uvs2, normals};
|
|
486
505
|
}
|
|
487
506
|
|
|
488
507
|
private extractInterleavedAttribute(buffer: Float32Array, stride: number, offset: number, itemSize: number): any {
|
|
@@ -666,7 +685,7 @@ export class QuarksLoader {
|
|
|
666
685
|
const data = await loadGeometry(geom.url);
|
|
667
686
|
// Copy arrays so a shared fetch-cache document is not aliased across
|
|
668
687
|
// effects — mutating one effect's inlined geometry must not corrupt others.
|
|
669
|
-
for (const key of ['positions', 'indices', 'uvs', 'normals']) {
|
|
688
|
+
for (const key of ['positions', 'indices', 'uvs', 'uvs2', 'normals']) {
|
|
670
689
|
const value = data?.[key];
|
|
671
690
|
if (value === undefined) {
|
|
672
691
|
continue;
|
|
@@ -824,6 +843,32 @@ export class QuarksLoader {
|
|
|
824
843
|
if (matDef.rim && typeof matDef.rim === 'object') {
|
|
825
844
|
matInfo.rim = matDef.rim;
|
|
826
845
|
}
|
|
846
|
+
// Texture effects (QuarksMaterial): mapUV {channel, tiling, offset, scroll},
|
|
847
|
+
// gradientMap {ramp (texture uuid), channel},
|
|
848
|
+
// erosion {mask (texture uuid), maskUV {channel, tiling, offset, scroll}, softness, edgeWidth,
|
|
849
|
+
// edgeColor}.
|
|
850
|
+
if (matDef.mapUV && typeof matDef.mapUV === 'object') {
|
|
851
|
+
matInfo.mapUV = matDef.mapUV;
|
|
852
|
+
}
|
|
853
|
+
if (matDef.gradientMap && typeof matDef.gradientMap === 'object') {
|
|
854
|
+
matInfo.gradientMap = {
|
|
855
|
+
...matDef.gradientMap,
|
|
856
|
+
ramp: matDef.gradientMap.ramp ? meta.textures[matDef.gradientMap.ramp] || null : null,
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
if (matDef.erosion && typeof matDef.erosion === 'object') {
|
|
860
|
+
matInfo.erosion = {
|
|
861
|
+
...matDef.erosion,
|
|
862
|
+
mask: matDef.erosion.mask ? meta.textures[matDef.erosion.mask] || null : null,
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
// uvDistortion {map (texture uuid), mapUV {channel, tiling, offset, scroll}, strength}.
|
|
866
|
+
if (matDef.uvDistortion && typeof matDef.uvDistortion === 'object') {
|
|
867
|
+
matInfo.uvDistortion = {
|
|
868
|
+
...matDef.uvDistortion,
|
|
869
|
+
map: matDef.uvDistortion.map ? meta.textures[matDef.uvDistortion.map] || null : null,
|
|
870
|
+
};
|
|
871
|
+
}
|
|
827
872
|
|
|
828
873
|
// QuarksMaterial (what ParticleSystem.ensureMaterialMeta emits) writes `alphaMode`
|
|
829
874
|
// directly as a Babylon alpha-mode constant; three.js materials (MeshBasicMaterial
|
|
@@ -873,6 +918,9 @@ export class QuarksLoader {
|
|
|
873
918
|
if (geometry.uvs && geometry.uvs.length > 0) {
|
|
874
919
|
vertexData.uvs = geometry.uvs;
|
|
875
920
|
}
|
|
921
|
+
if (geometry.uvs2 && geometry.uvs2.length > 0) {
|
|
922
|
+
vertexData.uvs2 = geometry.uvs2;
|
|
923
|
+
}
|
|
876
924
|
if (geometry.normals && geometry.normals.length > 0) {
|
|
877
925
|
vertexData.normals = geometry.normals;
|
|
878
926
|
}
|
package/src/QuarksUtil.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {TransformNode} from '@babylonjs/core/Meshes/transformNode';
|
|
2
2
|
import {Node} from '@babylonjs/core/node';
|
|
3
|
-
import {EmitSubParticleSystem} from 'quarks.core';
|
|
3
|
+
import {ConstantValue, EmitSubParticleSystem, FunctionValueGenerator, IntervalValue, ValueGenerator} from 'quarks.core';
|
|
4
4
|
import {BatchedRenderer} from './BatchedRenderer';
|
|
5
5
|
import {ParticleEmitter} from './ParticleEmitter';
|
|
6
6
|
import {ParticleSystem} from './ParticleSystem';
|
|
@@ -120,6 +120,102 @@ export class QuarksUtil {
|
|
|
120
120
|
return copy;
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
+
/**
|
|
124
|
+
* Seconds until the effect under `root` is over when it plays from the
|
|
125
|
+
* start: for each system its start delay, its duration and its longest
|
|
126
|
+
* particle life, divided by its playback speed; a sub emitter target goes
|
|
127
|
+
* on emitting for its own duration after the last of its parents' particles
|
|
128
|
+
* and ends a particle life after that, wherever it sits in the scene.
|
|
129
|
+
* Infinity when a system loops, since such an effect only ends on
|
|
130
|
+
* {@link endEmit}, or when one is frozen at playback speed 0.
|
|
131
|
+
*
|
|
132
|
+
* Random values count at their maximum, so this is an upper bound; curves
|
|
133
|
+
* are sampled. Particles cut short by collisions or culling only make the
|
|
134
|
+
* real effect shorter.
|
|
135
|
+
*/
|
|
136
|
+
static getDuration(root: Node): number {
|
|
137
|
+
const systems: ParticleSystem[] = [];
|
|
138
|
+
QuarksUtil.runOnAllParticleEmitters(root, (emitter) => {
|
|
139
|
+
systems.push(emitter.system as unknown as ParticleSystem);
|
|
140
|
+
});
|
|
141
|
+
const parents = new Map<ParticleSystem, ParticleSystem[]>();
|
|
142
|
+
// Grows while it is walked: a target outside `root` is part of the effect too.
|
|
143
|
+
for (let i = 0; i < systems.length; i++) {
|
|
144
|
+
const system = systems[i];
|
|
145
|
+
for (const behavior of system.behaviors) {
|
|
146
|
+
if (!(behavior instanceof EmitSubParticleSystem)) {
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
const target = (behavior.subParticleSystem as unknown as ParticleEmitter | undefined)?.system as
|
|
150
|
+
| ParticleSystem
|
|
151
|
+
| undefined;
|
|
152
|
+
if (target !== undefined && typeof target === 'object') {
|
|
153
|
+
if (!systems.includes(target)) {
|
|
154
|
+
systems.push(target);
|
|
155
|
+
}
|
|
156
|
+
const list = parents.get(target) ?? [];
|
|
157
|
+
list.push(system);
|
|
158
|
+
parents.set(target, list);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
const ends = new Map<ParticleSystem, number>();
|
|
163
|
+
const endOf = (system: ParticleSystem): number => {
|
|
164
|
+
const known = ends.get(system);
|
|
165
|
+
if (known !== undefined) {
|
|
166
|
+
return known;
|
|
167
|
+
}
|
|
168
|
+
// Marks the system while it is resolved, so a sub emitter cycle loops forever rather than the stack.
|
|
169
|
+
ends.set(system, Infinity);
|
|
170
|
+
const speed = system.playbackSpeed;
|
|
171
|
+
const life = QuarksUtil.maxOf(system.startLife) / speed;
|
|
172
|
+
let end: number;
|
|
173
|
+
if (!(speed > 0)) {
|
|
174
|
+
end = Infinity;
|
|
175
|
+
} else if (system.onlyUsedByOther) {
|
|
176
|
+
end = 0;
|
|
177
|
+
for (const parent of parents.get(system) ?? []) {
|
|
178
|
+
end = Math.max(end, endOf(parent));
|
|
179
|
+
}
|
|
180
|
+
end += system.duration / speed + life;
|
|
181
|
+
} else if (system.looping) {
|
|
182
|
+
end = Infinity;
|
|
183
|
+
} else {
|
|
184
|
+
// The delay is drawn once, at the start.
|
|
185
|
+
end = (QuarksUtil.maxOf(system.startDelay, 0) + system.duration) / speed + life;
|
|
186
|
+
}
|
|
187
|
+
ends.set(system, end);
|
|
188
|
+
return end;
|
|
189
|
+
};
|
|
190
|
+
let duration = 0;
|
|
191
|
+
for (const system of systems) {
|
|
192
|
+
duration = Math.max(duration, endOf(system));
|
|
193
|
+
}
|
|
194
|
+
return duration;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** The largest value `generator` gives: exact for constants and intervals, sampled otherwise. */
|
|
198
|
+
private static maxOf(generator: ValueGenerator | FunctionValueGenerator, at?: number): number {
|
|
199
|
+
if (generator instanceof ConstantValue) {
|
|
200
|
+
return generator.value;
|
|
201
|
+
}
|
|
202
|
+
if (generator instanceof IntervalValue) {
|
|
203
|
+
return Math.max(generator.a, generator.b);
|
|
204
|
+
}
|
|
205
|
+
let max = -Infinity;
|
|
206
|
+
const samples = 32;
|
|
207
|
+
for (let i = 0; i <= samples; i++) {
|
|
208
|
+
const memory: number[] = [];
|
|
209
|
+
generator.startGen(memory);
|
|
210
|
+
const value =
|
|
211
|
+
generator.type === 'function'
|
|
212
|
+
? generator.genValue(memory, at ?? i / samples)
|
|
213
|
+
: generator.genValue(memory);
|
|
214
|
+
max = Math.max(max, value);
|
|
215
|
+
}
|
|
216
|
+
return max;
|
|
217
|
+
}
|
|
218
|
+
|
|
123
219
|
private static traverseNode(node: Node, callback: (node: Node) => void): void {
|
|
124
220
|
callback(node);
|
|
125
221
|
const children = node.getChildren();
|
package/src/SpriteBatch.ts
CHANGED
|
@@ -15,9 +15,11 @@ import {
|
|
|
15
15
|
SpriteParticle,
|
|
16
16
|
StretchedBillBoardSettings,
|
|
17
17
|
Vector3,
|
|
18
|
+
Vector4,
|
|
18
19
|
} from 'quarks.core';
|
|
19
20
|
import {VFXBatchSettings} from './BatchedRenderer';
|
|
20
21
|
import {gather, sortByKey, SortScratch} from './instanceSort';
|
|
22
|
+
import type {ParticleSystem} from './ParticleSystem';
|
|
21
23
|
import local_particle_physics_vert from './shaders/local_particle_physics_vert.glsl';
|
|
22
24
|
import local_particle_physics_vert_wgsl from './shaders/local_particle_physics_vert.wgsl';
|
|
23
25
|
import particle_frag from './shaders/particle_frag.glsl';
|
|
@@ -90,6 +92,9 @@ export class SpriteBatch extends VFXBatch {
|
|
|
90
92
|
} else {
|
|
91
93
|
vertexData.uvs = new Float32Array(vertexCount * 2);
|
|
92
94
|
}
|
|
95
|
+
if (this.meshUV2()) {
|
|
96
|
+
vertexData.uvs2 = this.settings.instancingUVs2!;
|
|
97
|
+
}
|
|
93
98
|
if (this.settings.instancingNormals) {
|
|
94
99
|
vertexData.normals = this.settings.instancingNormals;
|
|
95
100
|
}
|
|
@@ -98,6 +103,33 @@ export class SpriteBatch extends VFXBatch {
|
|
|
98
103
|
this.buildExpandableBuffers();
|
|
99
104
|
}
|
|
100
105
|
|
|
106
|
+
/**
|
|
107
|
+
* True when a mesh batch reads UV channel 1 and has a second UV set to
|
|
108
|
+
* read it from, two values per vertex. A set of any other length is left
|
|
109
|
+
* out with a warning, and channel 1 then reads the first set.
|
|
110
|
+
*/
|
|
111
|
+
private warnedUV2 = false;
|
|
112
|
+
|
|
113
|
+
private meshUV2(): boolean {
|
|
114
|
+
const uvs2 = this.settings.instancingUVs2;
|
|
115
|
+
if (this.settings.renderMode !== RenderMode.Mesh || !uvs2 || !this.usesUV1()) {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
const expected = (this.settings.instancingGeometry.length / 3) * 2;
|
|
119
|
+
if (uvs2.length !== expected) {
|
|
120
|
+
if (this.warnedUV2) {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
this.warnedUV2 = true;
|
|
124
|
+
console.warn(
|
|
125
|
+
`[babylon.quarks] instancingUVs2 has ${uvs2.length} values for ${expected / 2} vertices ` +
|
|
126
|
+
'(two per vertex expected); UV channel 1 reads the first UV set instead.'
|
|
127
|
+
);
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
|
|
101
133
|
/** Releases the instance vertex buffers so a resize does not leak GPU memory. */
|
|
102
134
|
private disposeExpandableBuffers(): void {
|
|
103
135
|
this.offsetVB?.dispose();
|
|
@@ -138,7 +170,7 @@ export class SpriteBatch extends VFXBatch {
|
|
|
138
170
|
}
|
|
139
171
|
this.mesh.setVerticesBuffer(this.rotationVB);
|
|
140
172
|
|
|
141
|
-
if (this.settings.renderMode
|
|
173
|
+
if (SpriteBatch.drawsAlongVelocity(this.settings.renderMode)) {
|
|
142
174
|
this.velocityBuffer = new Float32Array(this.maxParticles * 4);
|
|
143
175
|
this.velocityVB = new VertexBuffer(engine, this.velocityBuffer, 'velocity', true, false, 4, true);
|
|
144
176
|
this.mesh.setVerticesBuffer(this.velocityVB);
|
|
@@ -276,7 +308,7 @@ export class SpriteBatch extends VFXBatch {
|
|
|
276
308
|
if (this.settings.renderMode === RenderMode.Mesh) {
|
|
277
309
|
vertexShader = {glsl: local_particle_physics_vert, wgsl: local_particle_physics_vert_wgsl};
|
|
278
310
|
fragmentShader = {glsl: particle_physics_frag, wgsl: particle_physics_frag_wgsl};
|
|
279
|
-
} else if (this.settings.renderMode
|
|
311
|
+
} else if (SpriteBatch.drawsAlongVelocity(this.settings.renderMode)) {
|
|
280
312
|
vertexShader = {glsl: stretched_bb_particle_vert, wgsl: stretched_bb_particle_vert_wgsl};
|
|
281
313
|
fragmentShader = {glsl: particle_frag, wgsl: particle_frag_wgsl};
|
|
282
314
|
} else {
|
|
@@ -322,6 +354,9 @@ export class SpriteBatch extends VFXBatch {
|
|
|
322
354
|
if (this.settings.renderMode === RenderMode.HorizontalBillBoard) {
|
|
323
355
|
defines.push('HORIZONTAL');
|
|
324
356
|
}
|
|
357
|
+
if (this.settings.renderMode === RenderMode.Beam) {
|
|
358
|
+
defines.push('BEAM');
|
|
359
|
+
}
|
|
325
360
|
const pivot = SpriteBatch.takesPivotAndFlip(this.settings.renderMode) ? this.settings.pivot : undefined;
|
|
326
361
|
if (pivot) {
|
|
327
362
|
defines.push('PIVOT');
|
|
@@ -339,11 +374,14 @@ export class SpriteBatch extends VFXBatch {
|
|
|
339
374
|
const rim = isMesh ? this.settings.rim : undefined;
|
|
340
375
|
if (rim) {
|
|
341
376
|
defines.push('MESH_RIM');
|
|
342
|
-
if (rim.mode === '
|
|
377
|
+
if (rim.mode === 'fade') {
|
|
378
|
+
defines.push('MESH_RIM_FADE');
|
|
379
|
+
} else if (rim.mode === 'mix') {
|
|
343
380
|
defines.push('MESH_RIM_MIX');
|
|
344
381
|
}
|
|
345
382
|
}
|
|
346
383
|
const needsEyePosition = useEnvAtlas || !!rim;
|
|
384
|
+
defines.push(...this.surfaceDefines());
|
|
347
385
|
|
|
348
386
|
const shaderLanguage = shaderLanguageFor(this.scene.getEngine());
|
|
349
387
|
registerShaders(shaderName, vertexShader, fragmentShader, shaderLanguage);
|
|
@@ -352,7 +390,11 @@ export class SpriteBatch extends VFXBatch {
|
|
|
352
390
|
if (this.settings.renderMode === RenderMode.Mesh) {
|
|
353
391
|
attributes.push('normal');
|
|
354
392
|
}
|
|
355
|
-
if (this.
|
|
393
|
+
if (this.usesUV1() && this.meshUV2()) {
|
|
394
|
+
defines.push('MESH_UV2');
|
|
395
|
+
attributes.push('uv2');
|
|
396
|
+
}
|
|
397
|
+
if (SpriteBatch.drawsAlongVelocity(this.settings.renderMode)) {
|
|
356
398
|
attributes.push('velocity');
|
|
357
399
|
}
|
|
358
400
|
|
|
@@ -377,7 +419,8 @@ export class SpriteBatch extends VFXBatch {
|
|
|
377
419
|
uniforms.push('rimColor');
|
|
378
420
|
uniforms.push('rimParams');
|
|
379
421
|
}
|
|
380
|
-
|
|
422
|
+
this.surfaceUniforms(uniforms, samplers);
|
|
423
|
+
if (SpriteBatch.drawsAlongVelocity(this.settings.renderMode)) {
|
|
381
424
|
uniforms.push('speedFactor');
|
|
382
425
|
}
|
|
383
426
|
if (this.settings.softParticles) {
|
|
@@ -441,7 +484,7 @@ export class SpriteBatch extends VFXBatch {
|
|
|
441
484
|
mat.setFloat('tileCountX', this.settings.uTileCount);
|
|
442
485
|
mat.setFloat('tileCountY', this.settings.vTileCount);
|
|
443
486
|
}
|
|
444
|
-
if (this.settings.renderMode
|
|
487
|
+
if (SpriteBatch.drawsAlongVelocity(this.settings.renderMode)) {
|
|
445
488
|
mat.setFloat('speedFactor', this.settings.softNearFade > 0 ? this.settings.softNearFade : 1.0);
|
|
446
489
|
}
|
|
447
490
|
if (this.settings.softParticles) {
|
|
@@ -491,6 +534,7 @@ export class SpriteBatch extends VFXBatch {
|
|
|
491
534
|
}
|
|
492
535
|
|
|
493
536
|
this.bindFog(mat);
|
|
537
|
+
this.bindSurface(mat);
|
|
494
538
|
mat.backFaceCulling = false;
|
|
495
539
|
|
|
496
540
|
if (this.settings.materialTransparent) {
|
|
@@ -584,6 +628,8 @@ export class SpriteBatch extends VFXBatch {
|
|
|
584
628
|
const renderMode = this.settings.renderMode;
|
|
585
629
|
const isMeshRender = renderMode === RenderMode.Mesh;
|
|
586
630
|
const isStretchedRender = renderMode === RenderMode.StretchedBillBoard;
|
|
631
|
+
const isBeamRender = renderMode === RenderMode.Beam;
|
|
632
|
+
const usesVelocity = isStretchedRender || isBeamRender;
|
|
587
633
|
const flipsSprites = SpriteBatch.takesPivotAndFlip(renderMode);
|
|
588
634
|
|
|
589
635
|
const visibleSystems = this.getVisibleSystems();
|
|
@@ -904,6 +950,10 @@ export class SpriteBatch extends VFXBatch {
|
|
|
904
950
|
}
|
|
905
951
|
}
|
|
906
952
|
|
|
953
|
+
const tint = (system as ParticleSystem).tint;
|
|
954
|
+
if (tint !== undefined && index > firstIndex) {
|
|
955
|
+
SpriteBatch.applyTint(this.colorBuffer, firstIndex * 4, index - firstIndex, tint);
|
|
956
|
+
}
|
|
907
957
|
if (flipsSprites && system.flip !== undefined) {
|
|
908
958
|
if (store !== undefined) {
|
|
909
959
|
SpriteBatch.applyFlip(this.sizeBuffer, firstIndex * 3, store.flip, particleNum);
|
|
@@ -911,6 +961,9 @@ export class SpriteBatch extends VFXBatch {
|
|
|
911
961
|
SpriteBatch.applyFlipFromParticles(this.sizeBuffer, firstIndex * 3, particles, particleNum);
|
|
912
962
|
}
|
|
913
963
|
}
|
|
964
|
+
if (isBeamRender && this.velocityBuffer) {
|
|
965
|
+
this.writeBeams(system as ParticleSystem, firstIndex, index - firstIndex);
|
|
966
|
+
}
|
|
914
967
|
}
|
|
915
968
|
|
|
916
969
|
if (isStretchedRender && visibleSystems.length > 0) {
|
|
@@ -926,7 +979,7 @@ export class SpriteBatch extends VFXBatch {
|
|
|
926
979
|
// Slot 0 only needs clearing once per stretch of empty frames; re-uploading it every
|
|
927
980
|
// frame cost five buffer uploads per idle batch per frame.
|
|
928
981
|
if (!this.clearedInstanceUploaded) {
|
|
929
|
-
this.uploadClearedInstance(isMeshRender,
|
|
982
|
+
this.uploadClearedInstance(isMeshRender, usesVelocity);
|
|
930
983
|
this.clearedInstanceUploaded = true;
|
|
931
984
|
}
|
|
932
985
|
return;
|
|
@@ -934,7 +987,7 @@ export class SpriteBatch extends VFXBatch {
|
|
|
934
987
|
this.clearedInstanceUploaded = false;
|
|
935
988
|
|
|
936
989
|
if (this.settings.sortMode !== 'none') {
|
|
937
|
-
this.sortInstances(index, isMeshRender,
|
|
990
|
+
this.sortInstances(index, isMeshRender, usesVelocity);
|
|
938
991
|
}
|
|
939
992
|
|
|
940
993
|
// Upload only the slots actually filled this frame: the buffers are sized
|
|
@@ -944,7 +997,7 @@ export class SpriteBatch extends VFXBatch {
|
|
|
944
997
|
this.colorVB.update(this.colorBuffer.subarray(0, index * 4));
|
|
945
998
|
this.uvTileVB.update(this.uvTileBuffer.subarray(0, index));
|
|
946
999
|
this.rotationVB.update(this.rotationBuffer.subarray(0, isMeshRender ? index * 4 : index));
|
|
947
|
-
if (
|
|
1000
|
+
if (usesVelocity && this.velocityVB && this.velocityBuffer) {
|
|
948
1001
|
this.velocityVB.update(this.velocityBuffer.subarray(0, index * 4));
|
|
949
1002
|
}
|
|
950
1003
|
}
|
|
@@ -970,7 +1023,35 @@ export class SpriteBatch extends VFXBatch {
|
|
|
970
1023
|
}
|
|
971
1024
|
}
|
|
972
1025
|
|
|
973
|
-
/**
|
|
1026
|
+
/** Stretched billboards and beams: drawn along a per-instance vector, by the stretched shader. */
|
|
1027
|
+
private static drawsAlongVelocity(renderMode: RenderMode): boolean {
|
|
1028
|
+
return renderMode === RenderMode.StretchedBillBoard || renderMode === RenderMode.Beam;
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
private readonly beamEnd_ = new Vector3();
|
|
1032
|
+
|
|
1033
|
+
/**
|
|
1034
|
+
* Writes, for the `count` instances from `first`, the world vector from
|
|
1035
|
+
* each one's drawn position to the system's beam end into the velocity
|
|
1036
|
+
* buffer, which the stretched shader under `BEAM` lays the quad along. A
|
|
1037
|
+
* beam without an end gets no length.
|
|
1038
|
+
*/
|
|
1039
|
+
private writeBeams(system: ParticleSystem, first: number, count: number): void {
|
|
1040
|
+
const velocities = this.velocityBuffer!;
|
|
1041
|
+
const offsets = this.offsetBuffer;
|
|
1042
|
+
const end = this.beamEnd_;
|
|
1043
|
+
const hasEnd = typeof system.getBeamEnd === 'function' && system.getBeamEnd(end);
|
|
1044
|
+
for (let i = first; i < first + count; i++) {
|
|
1045
|
+
const o = i * 3;
|
|
1046
|
+
const v = i * 4;
|
|
1047
|
+
velocities[v] = hasEnd ? end.x - offsets[o] : 0;
|
|
1048
|
+
velocities[v + 1] = hasEnd ? end.y - offsets[o + 1] : 0;
|
|
1049
|
+
velocities[v + 2] = hasEnd ? end.z - offsets[o + 2] : 0;
|
|
1050
|
+
velocities[v + 3] = 0;
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
/** The quads that take a pivot and flip: every billboard kind, stretched included, but not meshes or beams. */
|
|
974
1055
|
private static takesPivotAndFlip(renderMode: RenderMode): boolean {
|
|
975
1056
|
return (
|
|
976
1057
|
renderMode === RenderMode.BillBoard ||
|
|
@@ -980,6 +1061,16 @@ export class SpriteBatch extends VFXBatch {
|
|
|
980
1061
|
);
|
|
981
1062
|
}
|
|
982
1063
|
|
|
1064
|
+
/** Multiplies `count` RGBA colours starting at `base` by a system's {@link ParticleSystem.tint}. */
|
|
1065
|
+
private static applyTint(colors: Float32Array, base: number, count: number, tint: Vector4): void {
|
|
1066
|
+
for (let j = 0, o = base; j < count; j++, o += 4) {
|
|
1067
|
+
colors[o] *= tint.x;
|
|
1068
|
+
colors[o + 1] *= tint.y;
|
|
1069
|
+
colors[o + 2] *= tint.z;
|
|
1070
|
+
colors[o + 3] *= tint.w;
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
|
|
983
1074
|
/**
|
|
984
1075
|
* Negates the size of each flipped particle along the axes it flips on,
|
|
985
1076
|
* starting at `base` in `sizes`. The billboard shader scales the quad by
|