babylon.quarks 0.24.0 → 0.25.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 +52 -5
- 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 +273 -7
- package/package.json +1 -1
- package/src/BatchedRenderer.ts +28 -1
- package/src/ParticleSystem.ts +256 -6
- package/src/QuarksLoader.ts +14 -0
- package/src/QuarksUtil.ts +97 -1
- package/src/SpriteBatch.ts +49 -9
- package/src/TrailBatch.ts +3 -0
- package/src/VFXBatch.ts +198 -1
- package/src/index.ts +21 -2
- package/src/shaders/chunks/surface.ts +188 -0
- package/src/shaders/particle_frag.glsl.ts +3 -11
- package/src/shaders/particle_frag.wgsl.ts +3 -11
- package/src/shaders/particle_physics_frag.glsl.ts +3 -11
- package/src/shaders/particle_physics_frag.wgsl.ts +3 -11
- package/src/shaders/stretched_bb_particle_vert.glsl.ts +12 -0
- package/src/shaders/stretched_bb_particle_vert.wgsl.ts +15 -0
- package/src/shaders/trail_frag.glsl.ts +3 -7
- package/src/shaders/trail_frag.wgsl.ts +3 -7
package/src/SpriteBatch.ts
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
} from 'quarks.core';
|
|
19
19
|
import {VFXBatchSettings} from './BatchedRenderer';
|
|
20
20
|
import {gather, sortByKey, SortScratch} from './instanceSort';
|
|
21
|
+
import type {ParticleSystem} from './ParticleSystem';
|
|
21
22
|
import local_particle_physics_vert from './shaders/local_particle_physics_vert.glsl';
|
|
22
23
|
import local_particle_physics_vert_wgsl from './shaders/local_particle_physics_vert.wgsl';
|
|
23
24
|
import particle_frag from './shaders/particle_frag.glsl';
|
|
@@ -138,7 +139,7 @@ export class SpriteBatch extends VFXBatch {
|
|
|
138
139
|
}
|
|
139
140
|
this.mesh.setVerticesBuffer(this.rotationVB);
|
|
140
141
|
|
|
141
|
-
if (this.settings.renderMode
|
|
142
|
+
if (SpriteBatch.drawsAlongVelocity(this.settings.renderMode)) {
|
|
142
143
|
this.velocityBuffer = new Float32Array(this.maxParticles * 4);
|
|
143
144
|
this.velocityVB = new VertexBuffer(engine, this.velocityBuffer, 'velocity', true, false, 4, true);
|
|
144
145
|
this.mesh.setVerticesBuffer(this.velocityVB);
|
|
@@ -276,7 +277,7 @@ export class SpriteBatch extends VFXBatch {
|
|
|
276
277
|
if (this.settings.renderMode === RenderMode.Mesh) {
|
|
277
278
|
vertexShader = {glsl: local_particle_physics_vert, wgsl: local_particle_physics_vert_wgsl};
|
|
278
279
|
fragmentShader = {glsl: particle_physics_frag, wgsl: particle_physics_frag_wgsl};
|
|
279
|
-
} else if (this.settings.renderMode
|
|
280
|
+
} else if (SpriteBatch.drawsAlongVelocity(this.settings.renderMode)) {
|
|
280
281
|
vertexShader = {glsl: stretched_bb_particle_vert, wgsl: stretched_bb_particle_vert_wgsl};
|
|
281
282
|
fragmentShader = {glsl: particle_frag, wgsl: particle_frag_wgsl};
|
|
282
283
|
} else {
|
|
@@ -322,6 +323,9 @@ export class SpriteBatch extends VFXBatch {
|
|
|
322
323
|
if (this.settings.renderMode === RenderMode.HorizontalBillBoard) {
|
|
323
324
|
defines.push('HORIZONTAL');
|
|
324
325
|
}
|
|
326
|
+
if (this.settings.renderMode === RenderMode.Beam) {
|
|
327
|
+
defines.push('BEAM');
|
|
328
|
+
}
|
|
325
329
|
const pivot = SpriteBatch.takesPivotAndFlip(this.settings.renderMode) ? this.settings.pivot : undefined;
|
|
326
330
|
if (pivot) {
|
|
327
331
|
defines.push('PIVOT');
|
|
@@ -344,6 +348,7 @@ export class SpriteBatch extends VFXBatch {
|
|
|
344
348
|
}
|
|
345
349
|
}
|
|
346
350
|
const needsEyePosition = useEnvAtlas || !!rim;
|
|
351
|
+
defines.push(...this.surfaceDefines());
|
|
347
352
|
|
|
348
353
|
const shaderLanguage = shaderLanguageFor(this.scene.getEngine());
|
|
349
354
|
registerShaders(shaderName, vertexShader, fragmentShader, shaderLanguage);
|
|
@@ -352,7 +357,7 @@ export class SpriteBatch extends VFXBatch {
|
|
|
352
357
|
if (this.settings.renderMode === RenderMode.Mesh) {
|
|
353
358
|
attributes.push('normal');
|
|
354
359
|
}
|
|
355
|
-
if (this.settings.renderMode
|
|
360
|
+
if (SpriteBatch.drawsAlongVelocity(this.settings.renderMode)) {
|
|
356
361
|
attributes.push('velocity');
|
|
357
362
|
}
|
|
358
363
|
|
|
@@ -377,7 +382,8 @@ export class SpriteBatch extends VFXBatch {
|
|
|
377
382
|
uniforms.push('rimColor');
|
|
378
383
|
uniforms.push('rimParams');
|
|
379
384
|
}
|
|
380
|
-
|
|
385
|
+
this.surfaceUniforms(uniforms, samplers);
|
|
386
|
+
if (SpriteBatch.drawsAlongVelocity(this.settings.renderMode)) {
|
|
381
387
|
uniforms.push('speedFactor');
|
|
382
388
|
}
|
|
383
389
|
if (this.settings.softParticles) {
|
|
@@ -441,7 +447,7 @@ export class SpriteBatch extends VFXBatch {
|
|
|
441
447
|
mat.setFloat('tileCountX', this.settings.uTileCount);
|
|
442
448
|
mat.setFloat('tileCountY', this.settings.vTileCount);
|
|
443
449
|
}
|
|
444
|
-
if (this.settings.renderMode
|
|
450
|
+
if (SpriteBatch.drawsAlongVelocity(this.settings.renderMode)) {
|
|
445
451
|
mat.setFloat('speedFactor', this.settings.softNearFade > 0 ? this.settings.softNearFade : 1.0);
|
|
446
452
|
}
|
|
447
453
|
if (this.settings.softParticles) {
|
|
@@ -491,6 +497,7 @@ export class SpriteBatch extends VFXBatch {
|
|
|
491
497
|
}
|
|
492
498
|
|
|
493
499
|
this.bindFog(mat);
|
|
500
|
+
this.bindSurface(mat);
|
|
494
501
|
mat.backFaceCulling = false;
|
|
495
502
|
|
|
496
503
|
if (this.settings.materialTransparent) {
|
|
@@ -584,6 +591,8 @@ export class SpriteBatch extends VFXBatch {
|
|
|
584
591
|
const renderMode = this.settings.renderMode;
|
|
585
592
|
const isMeshRender = renderMode === RenderMode.Mesh;
|
|
586
593
|
const isStretchedRender = renderMode === RenderMode.StretchedBillBoard;
|
|
594
|
+
const isBeamRender = renderMode === RenderMode.Beam;
|
|
595
|
+
const usesVelocity = isStretchedRender || isBeamRender;
|
|
587
596
|
const flipsSprites = SpriteBatch.takesPivotAndFlip(renderMode);
|
|
588
597
|
|
|
589
598
|
const visibleSystems = this.getVisibleSystems();
|
|
@@ -911,6 +920,9 @@ export class SpriteBatch extends VFXBatch {
|
|
|
911
920
|
SpriteBatch.applyFlipFromParticles(this.sizeBuffer, firstIndex * 3, particles, particleNum);
|
|
912
921
|
}
|
|
913
922
|
}
|
|
923
|
+
if (isBeamRender && this.velocityBuffer) {
|
|
924
|
+
this.writeBeams(system as ParticleSystem, firstIndex, index - firstIndex);
|
|
925
|
+
}
|
|
914
926
|
}
|
|
915
927
|
|
|
916
928
|
if (isStretchedRender && visibleSystems.length > 0) {
|
|
@@ -926,7 +938,7 @@ export class SpriteBatch extends VFXBatch {
|
|
|
926
938
|
// Slot 0 only needs clearing once per stretch of empty frames; re-uploading it every
|
|
927
939
|
// frame cost five buffer uploads per idle batch per frame.
|
|
928
940
|
if (!this.clearedInstanceUploaded) {
|
|
929
|
-
this.uploadClearedInstance(isMeshRender,
|
|
941
|
+
this.uploadClearedInstance(isMeshRender, usesVelocity);
|
|
930
942
|
this.clearedInstanceUploaded = true;
|
|
931
943
|
}
|
|
932
944
|
return;
|
|
@@ -934,7 +946,7 @@ export class SpriteBatch extends VFXBatch {
|
|
|
934
946
|
this.clearedInstanceUploaded = false;
|
|
935
947
|
|
|
936
948
|
if (this.settings.sortMode !== 'none') {
|
|
937
|
-
this.sortInstances(index, isMeshRender,
|
|
949
|
+
this.sortInstances(index, isMeshRender, usesVelocity);
|
|
938
950
|
}
|
|
939
951
|
|
|
940
952
|
// Upload only the slots actually filled this frame: the buffers are sized
|
|
@@ -944,7 +956,7 @@ export class SpriteBatch extends VFXBatch {
|
|
|
944
956
|
this.colorVB.update(this.colorBuffer.subarray(0, index * 4));
|
|
945
957
|
this.uvTileVB.update(this.uvTileBuffer.subarray(0, index));
|
|
946
958
|
this.rotationVB.update(this.rotationBuffer.subarray(0, isMeshRender ? index * 4 : index));
|
|
947
|
-
if (
|
|
959
|
+
if (usesVelocity && this.velocityVB && this.velocityBuffer) {
|
|
948
960
|
this.velocityVB.update(this.velocityBuffer.subarray(0, index * 4));
|
|
949
961
|
}
|
|
950
962
|
}
|
|
@@ -970,7 +982,35 @@ export class SpriteBatch extends VFXBatch {
|
|
|
970
982
|
}
|
|
971
983
|
}
|
|
972
984
|
|
|
973
|
-
/**
|
|
985
|
+
/** Stretched billboards and beams: drawn along a per-instance vector, by the stretched shader. */
|
|
986
|
+
private static drawsAlongVelocity(renderMode: RenderMode): boolean {
|
|
987
|
+
return renderMode === RenderMode.StretchedBillBoard || renderMode === RenderMode.Beam;
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
private readonly beamEnd_ = new Vector3();
|
|
991
|
+
|
|
992
|
+
/**
|
|
993
|
+
* Writes, for the `count` instances from `first`, the world vector from
|
|
994
|
+
* each one's drawn position to the system's beam end into the velocity
|
|
995
|
+
* buffer, which the stretched shader under `BEAM` lays the quad along. A
|
|
996
|
+
* beam without an end gets no length.
|
|
997
|
+
*/
|
|
998
|
+
private writeBeams(system: ParticleSystem, first: number, count: number): void {
|
|
999
|
+
const velocities = this.velocityBuffer!;
|
|
1000
|
+
const offsets = this.offsetBuffer;
|
|
1001
|
+
const end = this.beamEnd_;
|
|
1002
|
+
const hasEnd = typeof system.getBeamEnd === 'function' && system.getBeamEnd(end);
|
|
1003
|
+
for (let i = first; i < first + count; i++) {
|
|
1004
|
+
const o = i * 3;
|
|
1005
|
+
const v = i * 4;
|
|
1006
|
+
velocities[v] = hasEnd ? end.x - offsets[o] : 0;
|
|
1007
|
+
velocities[v + 1] = hasEnd ? end.y - offsets[o + 1] : 0;
|
|
1008
|
+
velocities[v + 2] = hasEnd ? end.z - offsets[o + 2] : 0;
|
|
1009
|
+
velocities[v + 3] = 0;
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
/** The quads that take a pivot and flip: every billboard kind, stretched included, but not meshes or beams. */
|
|
974
1014
|
private static takesPivotAndFlip(renderMode: RenderMode): boolean {
|
|
975
1015
|
return (
|
|
976
1016
|
renderMode === RenderMode.BillBoard ||
|
package/src/TrailBatch.ts
CHANGED
|
@@ -139,6 +139,7 @@ export class TrailBatch extends VFXBatch {
|
|
|
139
139
|
if (this.settings.texture) {
|
|
140
140
|
defines.push('USE_MAP');
|
|
141
141
|
}
|
|
142
|
+
defines.push(...this.surfaceDefines());
|
|
142
143
|
|
|
143
144
|
const shaderLanguage = shaderLanguageFor(this.scene.getEngine());
|
|
144
145
|
registerShaders(
|
|
@@ -164,6 +165,7 @@ export class TrailBatch extends VFXBatch {
|
|
|
164
165
|
if (this.settings.texture) {
|
|
165
166
|
samplers.push('map');
|
|
166
167
|
}
|
|
168
|
+
this.surfaceUniforms(uniforms, samplers);
|
|
167
169
|
|
|
168
170
|
const mat = new ShaderMaterial(
|
|
169
171
|
shaderName,
|
|
@@ -200,6 +202,7 @@ export class TrailBatch extends VFXBatch {
|
|
|
200
202
|
});
|
|
201
203
|
|
|
202
204
|
this.bindFog(mat);
|
|
205
|
+
this.bindSurface(mat);
|
|
203
206
|
mat.backFaceCulling = false;
|
|
204
207
|
|
|
205
208
|
if (this.settings.materialTransparent) {
|
package/src/VFXBatch.ts
CHANGED
|
@@ -3,7 +3,7 @@ import {Constants} from '@babylonjs/core/Engines/constants';
|
|
|
3
3
|
import {BaseTexture} from '@babylonjs/core/Materials/Textures/baseTexture';
|
|
4
4
|
import {ShaderMaterial} from '@babylonjs/core/Materials/shaderMaterial';
|
|
5
5
|
import {Color3} from '@babylonjs/core/Maths/math.color';
|
|
6
|
-
import {Vector3 as BVector3} from '@babylonjs/core/Maths/math.vector';
|
|
6
|
+
import {Vector2 as BVector2, Vector3 as BVector3, Vector4 as BVector4} from '@babylonjs/core/Maths/math.vector';
|
|
7
7
|
import {Mesh} from '@babylonjs/core/Meshes/mesh';
|
|
8
8
|
import {Scene} from '@babylonjs/core/scene';
|
|
9
9
|
import {IParticleSystem, Vector3} from 'quarks.core';
|
|
@@ -30,6 +30,13 @@ export enum RenderMode {
|
|
|
30
30
|
Trail = 3,
|
|
31
31
|
HorizontalBillBoard = 4,
|
|
32
32
|
VerticalBillBoard = 5,
|
|
33
|
+
/**
|
|
34
|
+
* A quad laid from each particle to the system's {@link ParticleSystem.beamEnd}
|
|
35
|
+
* and turned about its length to face the camera (a line renderer): U runs
|
|
36
|
+
* from the particle (0) to the end (1), V across, the particle's size is
|
|
37
|
+
* the width. Lightning, tethers, lasers.
|
|
38
|
+
*/
|
|
39
|
+
Beam = 6,
|
|
33
40
|
}
|
|
34
41
|
|
|
35
42
|
/**
|
|
@@ -122,6 +129,87 @@ export function meshRimEqual(a: MeshRim | undefined, b: MeshRim | undefined): bo
|
|
|
122
129
|
);
|
|
123
130
|
}
|
|
124
131
|
|
|
132
|
+
/**
|
|
133
|
+
* Alpha erosion: the particle's alpha decides how much of its shape is left
|
|
134
|
+
* instead of how transparent it is, so `ColorOverLife` fading alpha out
|
|
135
|
+
* dissolves (noise mask) or wipes (gradient mask) the shape away. See
|
|
136
|
+
* {@link VFXBatchSettings.erosion}.
|
|
137
|
+
*/
|
|
138
|
+
export interface ParticleErosion {
|
|
139
|
+
/**
|
|
140
|
+
* Mask whose red channel orders what goes first, sampled with the
|
|
141
|
+
* particle's UV (the quad, mesh or trail UV, unscrolled). Null: the map's
|
|
142
|
+
* own alpha, so a soft shape shrinks from its edges.
|
|
143
|
+
*/
|
|
144
|
+
mask: BaseTexture | null;
|
|
145
|
+
/** Width of the soft transition at the cut, in mask units. Default 0.05. */
|
|
146
|
+
softness: number;
|
|
147
|
+
/** Width of the glowing band above the cut, in mask units; 0 (default) draws none. */
|
|
148
|
+
edgeWidth: number;
|
|
149
|
+
/** Colour added in the edge band, linear RGB, may exceed 1. Default `[1, 1, 1]`. */
|
|
150
|
+
edgeColor: [number, number, number];
|
|
151
|
+
/** Mask UV scroll, UV units per second. Default `[0, 0]`. */
|
|
152
|
+
maskScroll: [number, number];
|
|
153
|
+
}
|
|
154
|
+
// Limits worth knowing: the mask is sampled with the UV as drawn (on a sheet,
|
|
155
|
+
// the current tile's), erosion only changes alpha (one-one and premultiplied
|
|
156
|
+
// blending show a hard cut), and with neither a mask nor a map there is
|
|
157
|
+
// nothing to order the cut by, so the shape goes all at once near alpha 0.
|
|
158
|
+
// Scrolling runs on the renderer's clock, not a system's playback speed.
|
|
159
|
+
|
|
160
|
+
/** A complete erosion from a partial one, every field defaulted, or undefined when `erosion` is unset. */
|
|
161
|
+
export function normalizeErosion(erosion: Partial<ParticleErosion> | null | undefined): ParticleErosion | undefined {
|
|
162
|
+
if (!erosion || typeof erosion !== 'object') {
|
|
163
|
+
return undefined;
|
|
164
|
+
}
|
|
165
|
+
const num = (value: unknown, fallback: number) =>
|
|
166
|
+
typeof value === 'number' && Number.isFinite(value) ? value : fallback;
|
|
167
|
+
const color = Array.isArray(erosion.edgeColor) ? erosion.edgeColor : [1, 1, 1];
|
|
168
|
+
const scroll = Array.isArray(erosion.maskScroll) ? erosion.maskScroll : [0, 0];
|
|
169
|
+
return {
|
|
170
|
+
mask: erosion.mask ?? null,
|
|
171
|
+
softness: Math.max(0.0001, num(erosion.softness, 0.05)),
|
|
172
|
+
edgeWidth: Math.max(0, num(erosion.edgeWidth, 0)),
|
|
173
|
+
edgeColor: [num(color[0], 1), num(color[1], 1), num(color[2], 1)],
|
|
174
|
+
maskScroll: [num(scroll[0], 0), num(scroll[1], 0)],
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** True when both describe the same erosion (or both none). */
|
|
179
|
+
export function erosionEqual(a: ParticleErosion | undefined, b: ParticleErosion | undefined): boolean {
|
|
180
|
+
if (a === b) {
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
183
|
+
if (!a || !b) {
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
return (
|
|
187
|
+
a.mask === b.mask &&
|
|
188
|
+
a.softness === b.softness &&
|
|
189
|
+
a.edgeWidth === b.edgeWidth &&
|
|
190
|
+
a.edgeColor[0] === b.edgeColor[0] &&
|
|
191
|
+
a.edgeColor[1] === b.edgeColor[1] &&
|
|
192
|
+
a.edgeColor[2] === b.edgeColor[2] &&
|
|
193
|
+
a.maskScroll[0] === b.maskScroll[0] &&
|
|
194
|
+
a.maskScroll[1] === b.maskScroll[1]
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** A UV scroll speed, or undefined when unset or zero. */
|
|
199
|
+
export function normalizeUVScroll(scroll: ArrayLike<number> | null | undefined): [number, number] | undefined {
|
|
200
|
+
if (!scroll || typeof scroll !== 'object') {
|
|
201
|
+
return undefined;
|
|
202
|
+
}
|
|
203
|
+
const u = Number.isFinite(scroll[0]) ? scroll[0] : 0;
|
|
204
|
+
const v = Number.isFinite(scroll[1]) ? scroll[1] : 0;
|
|
205
|
+
return u === 0 && v === 0 ? undefined : [u, v];
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** True when both scroll the same way (or both not at all). */
|
|
209
|
+
export function uvScrollEqual(a: [number, number] | undefined, b: [number, number] | undefined): boolean {
|
|
210
|
+
return a === b || (!!a && !!b && a[0] === b[0] && a[1] === b[1]);
|
|
211
|
+
}
|
|
212
|
+
|
|
125
213
|
export interface StoredBatchSettings {
|
|
126
214
|
instancingGeometry: Float32Array;
|
|
127
215
|
instancingIndices: Uint32Array | Uint16Array;
|
|
@@ -162,6 +250,12 @@ export interface StoredBatchSettings {
|
|
|
162
250
|
lighting?: MeshLighting;
|
|
163
251
|
/** Mesh particle rim, see {@link VFXBatchSettings.rim}. */
|
|
164
252
|
rim?: MeshRim;
|
|
253
|
+
/** Map UV scroll, see {@link VFXBatchSettings.uvScroll}. */
|
|
254
|
+
uvScroll?: [number, number];
|
|
255
|
+
/** Colour ramp for the map, see {@link VFXBatchSettings.gradientMap}. */
|
|
256
|
+
gradientMap: BaseTexture | null;
|
|
257
|
+
/** Alpha erosion, see {@link VFXBatchSettings.erosion}. */
|
|
258
|
+
erosion?: ParticleErosion;
|
|
165
259
|
}
|
|
166
260
|
|
|
167
261
|
export abstract class VFXBatch {
|
|
@@ -208,6 +302,9 @@ export abstract class VFXBatch {
|
|
|
208
302
|
applyFog: settings.applyFog ?? true,
|
|
209
303
|
lighting: settings.lighting,
|
|
210
304
|
rim: normalizeMeshRim(settings.rim),
|
|
305
|
+
uvScroll: normalizeUVScroll(settings.uvScroll),
|
|
306
|
+
gradientMap: settings.gradientMap ?? null,
|
|
307
|
+
erosion: normalizeErosion(settings.erosion),
|
|
211
308
|
};
|
|
212
309
|
this.mesh = new Mesh('vfxBatch', scene);
|
|
213
310
|
this.configureMesh();
|
|
@@ -331,6 +428,106 @@ export abstract class VFXBatch {
|
|
|
331
428
|
});
|
|
332
429
|
}
|
|
333
430
|
|
|
431
|
+
/**
|
|
432
|
+
* Seconds of renderer time, set by the renderer before each fill; what UV
|
|
433
|
+
* and erosion mask scrolling advance by.
|
|
434
|
+
*/
|
|
435
|
+
time = 0;
|
|
436
|
+
|
|
437
|
+
/** Defines of the texture effects (see `shaders/chunks/surface.ts`) this batch uses. */
|
|
438
|
+
protected surfaceDefines(): string[] {
|
|
439
|
+
const defines: string[] = [];
|
|
440
|
+
if (this.settings.uvScroll) {
|
|
441
|
+
defines.push('UV_SCROLL');
|
|
442
|
+
}
|
|
443
|
+
if (this.settings.gradientMap) {
|
|
444
|
+
defines.push('GRADIENT_MAP');
|
|
445
|
+
}
|
|
446
|
+
const erosion = this.settings.erosion;
|
|
447
|
+
if (erosion) {
|
|
448
|
+
defines.push('EROSION');
|
|
449
|
+
if (erosion.mask) {
|
|
450
|
+
defines.push('EROSION_MASK');
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
return defines;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/** Adds the uniforms and samplers of the texture effects this batch uses. */
|
|
457
|
+
protected surfaceUniforms(uniforms: string[], samplers: string[]): void {
|
|
458
|
+
if (this.settings.uvScroll) {
|
|
459
|
+
uniforms.push('uvScrollOffset');
|
|
460
|
+
}
|
|
461
|
+
if (this.settings.gradientMap) {
|
|
462
|
+
samplers.push('gradientMap');
|
|
463
|
+
uniforms.push('gradientMapRange');
|
|
464
|
+
}
|
|
465
|
+
const erosion = this.settings.erosion;
|
|
466
|
+
if (erosion) {
|
|
467
|
+
uniforms.push('erosionParams', 'erosionEdgeColor');
|
|
468
|
+
if (erosion.mask) {
|
|
469
|
+
samplers.push('erosionMask');
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/** Sets the texture effects' textures and constants, and advances their scrolling on every bind. */
|
|
475
|
+
protected bindSurface(material: ShaderMaterial): void {
|
|
476
|
+
const {uvScroll, gradientMap, erosion} = this.settings;
|
|
477
|
+
if (gradientMap) {
|
|
478
|
+
material.setTexture('gradientMap', gradientMap);
|
|
479
|
+
}
|
|
480
|
+
if (erosion) {
|
|
481
|
+
if (erosion.mask) {
|
|
482
|
+
material.setTexture('erosionMask', erosion.mask);
|
|
483
|
+
}
|
|
484
|
+
material.setVector3(
|
|
485
|
+
'erosionEdgeColor',
|
|
486
|
+
new BVector3(erosion.edgeColor[0], erosion.edgeColor[1], erosion.edgeColor[2])
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
if (!uvScroll && !erosion && !gradientMap) {
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
// Wrapped on the CPU: a raw speed × time grows until float32 UVs lose their fraction.
|
|
493
|
+
const wrap = (value: number) => value - Math.floor(value);
|
|
494
|
+
const scrollOffset = new BVector2();
|
|
495
|
+
const erosionParams = new BVector4();
|
|
496
|
+
const gradientRange = new BVector2(0, 1);
|
|
497
|
+
// Written to the material for the next bind and to the bound effect for
|
|
498
|
+
// this one: ShaderMaterial uploads its values before the bind observers run.
|
|
499
|
+
const write = () => {
|
|
500
|
+
const time = this.time;
|
|
501
|
+
const effect = material.getEffect();
|
|
502
|
+
if (uvScroll) {
|
|
503
|
+
scrollOffset.set(wrap(uvScroll[0] * time), wrap(uvScroll[1] * time));
|
|
504
|
+
material.setVector2('uvScrollOffset', scrollOffset);
|
|
505
|
+
effect?.setVector2('uvScrollOffset', scrollOffset);
|
|
506
|
+
}
|
|
507
|
+
if (erosion) {
|
|
508
|
+
erosionParams.set(
|
|
509
|
+
erosion.softness,
|
|
510
|
+
erosion.edgeWidth,
|
|
511
|
+
wrap(erosion.maskScroll[0] * time),
|
|
512
|
+
wrap(erosion.maskScroll[1] * time)
|
|
513
|
+
);
|
|
514
|
+
material.setVector4('erosionParams', erosionParams);
|
|
515
|
+
effect?.setVector4('erosionParams', erosionParams);
|
|
516
|
+
}
|
|
517
|
+
if (gradientMap) {
|
|
518
|
+
// Texel centres of the first and last texel, so a repeating
|
|
519
|
+
// ramp does not blend its two ends at black and white.
|
|
520
|
+
const width = gradientMap.getSize().width;
|
|
521
|
+
const half = width > 0 ? 0.5 / width : 0;
|
|
522
|
+
gradientRange.set(half, 1 - half);
|
|
523
|
+
material.setVector2('gradientMapRange', gradientRange);
|
|
524
|
+
effect?.setVector2('gradientMapRange', gradientRange);
|
|
525
|
+
}
|
|
526
|
+
};
|
|
527
|
+
write();
|
|
528
|
+
material.onBindObservable.add(write);
|
|
529
|
+
}
|
|
530
|
+
|
|
334
531
|
addSystem(system: IParticleSystem) {
|
|
335
532
|
this.systems.add(system);
|
|
336
533
|
}
|
package/src/index.ts
CHANGED
|
@@ -21,11 +21,13 @@ export {ParticleEmitter} from './ParticleEmitter';
|
|
|
21
21
|
export {ParticleSystem} from './ParticleSystem';
|
|
22
22
|
export type {
|
|
23
23
|
BabylonMetaData,
|
|
24
|
+
BeamEnd,
|
|
24
25
|
BurstParameters,
|
|
25
26
|
BurstParametersJSON,
|
|
26
27
|
CullingMode,
|
|
27
28
|
ParticleSystemJSONParameters,
|
|
28
29
|
ParticleSystemParameters,
|
|
30
|
+
ScalingMode,
|
|
29
31
|
} from './ParticleSystem';
|
|
30
32
|
export {QuarksLoader} from './QuarksLoader';
|
|
31
33
|
export type {
|
|
@@ -41,8 +43,25 @@ export {QuarksUtil} from './QuarksUtil';
|
|
|
41
43
|
export * from './shaders/';
|
|
42
44
|
export {SpriteBatch} from './SpriteBatch';
|
|
43
45
|
export {TrailBatch} from './TrailBatch';
|
|
44
|
-
export {
|
|
45
|
-
|
|
46
|
+
export {
|
|
47
|
+
defaultMeshLighting,
|
|
48
|
+
erosionEqual,
|
|
49
|
+
meshRimEqual,
|
|
50
|
+
normalizeErosion,
|
|
51
|
+
normalizeMeshRim,
|
|
52
|
+
normalizeUVScroll,
|
|
53
|
+
RenderMode,
|
|
54
|
+
uvScrollEqual,
|
|
55
|
+
VFXBatch,
|
|
56
|
+
} from './VFXBatch';
|
|
57
|
+
export type {
|
|
58
|
+
MeshLighting,
|
|
59
|
+
MeshLightingSettings,
|
|
60
|
+
MeshRim,
|
|
61
|
+
ParticleErosion,
|
|
62
|
+
SortMode,
|
|
63
|
+
StoredBatchSettings,
|
|
64
|
+
} from './VFXBatch';
|
|
46
65
|
|
|
47
66
|
// Re-export quarks.core types (interfaces - type-only)
|
|
48
67
|
export type {
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Texture effects shared by the sprite, mesh and trail fragment shaders, in
|
|
3
|
+
* both shader languages: UV scrolling, a gradient map and alpha erosion.
|
|
4
|
+
*
|
|
5
|
+
* - `UV_SCROLL`: `uvScrollOffset` (already wrapped to 0…1 on the CPU, so it
|
|
6
|
+
* never loses precision) is added to the map's UV. The texture should wrap
|
|
7
|
+
* (repeat); scrolling a tile of a sheet runs into its neighbours.
|
|
8
|
+
* - `GRADIENT_MAP`: the map's red channel picks a colour along U of
|
|
9
|
+
* `gradientMap`, whose alpha multiplies the map's. A greyscale texture is
|
|
10
|
+
* then recoloured by a ramp, and the particle colour still tints it.
|
|
11
|
+
* `gradientMapRange` keeps the lookup between the first and last texel
|
|
12
|
+
* centres. Make the ramp without mipmaps: the lookup's UV jumps with the
|
|
13
|
+
* map's detail, and the hardware would pick a blurred mip for it.
|
|
14
|
+
* - `EROSION`: the particle's alpha stops being a transparency and becomes how
|
|
15
|
+
* much of the shape is left. A fragment shows where the mask (the red
|
|
16
|
+
* channel of `erosionMask` under `EROSION_MASK`, the map's alpha otherwise)
|
|
17
|
+
* is above the cut that alpha sets: alpha 1 shows everything the map shows,
|
|
18
|
+
* alpha 0 nothing, and on the way down the shape eats away from its lowest
|
|
19
|
+
* mask values first — a noise mask dissolves it, a gradient mask wipes it.
|
|
20
|
+
* `erosionParams` is (softness, edge width, mask offset u, mask offset v),
|
|
21
|
+
* the edge band just above the cut glows in `erosionEdgeColor`. The mask is
|
|
22
|
+
* sampled with the particle's UV as drawn, which on a sprite sheet is the
|
|
23
|
+
* current tile's. Erosion only changes alpha, so blend modes that ignore
|
|
24
|
+
* alpha (one-one, premultiplied) show a hard cut.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/** Fragment-side declarations, GLSL. */
|
|
28
|
+
export const surfaceFragmentDeclarationGLSL = /* glsl */ `
|
|
29
|
+
#ifdef UV_SCROLL
|
|
30
|
+
uniform vec2 uvScrollOffset;
|
|
31
|
+
#endif
|
|
32
|
+
#ifdef GRADIENT_MAP
|
|
33
|
+
uniform sampler2D gradientMap;
|
|
34
|
+
uniform vec2 gradientMapRange;
|
|
35
|
+
#endif
|
|
36
|
+
#ifdef EROSION
|
|
37
|
+
uniform vec4 erosionParams;
|
|
38
|
+
uniform vec3 erosionEdgeColor;
|
|
39
|
+
#ifdef EROSION_MASK
|
|
40
|
+
uniform sampler2D erosionMask;
|
|
41
|
+
#endif
|
|
42
|
+
|
|
43
|
+
vec4 quarksErode(vec4 color, float level, float mask) {
|
|
44
|
+
float softness = erosionParams.x;
|
|
45
|
+
float edgeWidth = erosionParams.y;
|
|
46
|
+
// At level 1 the cut sits below the lowest mask value by the soft band
|
|
47
|
+
// and the edge band, so nothing is cut and no edge shows; at level 0 above
|
|
48
|
+
// the highest. The edge then grows in as the cut rises, rather than popping.
|
|
49
|
+
float cut = mix(-(softness + edgeWidth), 1.0, 1.0 - level);
|
|
50
|
+
float visible = smoothstep(cut, cut + softness, mask);
|
|
51
|
+
if (edgeWidth > 0.0) {
|
|
52
|
+
float edge = 1.0 - smoothstep(cut + softness, cut + softness + edgeWidth, mask);
|
|
53
|
+
color.rgb += erosionEdgeColor * edge;
|
|
54
|
+
}
|
|
55
|
+
color.a *= visible;
|
|
56
|
+
return color;
|
|
57
|
+
}
|
|
58
|
+
#endif
|
|
59
|
+
`;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Declares `baseColor` from `vColor` and the map, with the effects applied,
|
|
63
|
+
* GLSL. `tileBlend` adds the second tile of `TILE_BLEND`.
|
|
64
|
+
*/
|
|
65
|
+
export function surfaceColorGLSL(tileBlend: boolean): string {
|
|
66
|
+
return /* glsl */ `
|
|
67
|
+
vec4 baseColor = vColor;
|
|
68
|
+
#ifdef UV_SCROLL
|
|
69
|
+
vec2 mapUV = vUV + uvScrollOffset;
|
|
70
|
+
#else
|
|
71
|
+
vec2 mapUV = vUV;
|
|
72
|
+
#endif
|
|
73
|
+
#ifdef EROSION
|
|
74
|
+
float erosionLevel = baseColor.a;
|
|
75
|
+
baseColor.a = 1.0;
|
|
76
|
+
float erosionMaskValue = 1.0;
|
|
77
|
+
#endif
|
|
78
|
+
#ifdef USE_MAP
|
|
79
|
+
vec4 texColor = texture2D(map, mapUV);
|
|
80
|
+
${
|
|
81
|
+
tileBlend
|
|
82
|
+
? `#ifdef TILE_BLEND
|
|
83
|
+
#ifdef UV_SCROLL
|
|
84
|
+
vec4 texColor2 = texture2D(map, vUV2 + uvScrollOffset);
|
|
85
|
+
#else
|
|
86
|
+
vec4 texColor2 = texture2D(map, vUV2);
|
|
87
|
+
#endif
|
|
88
|
+
texColor = mix(texColor, texColor2, vTileBlend);
|
|
89
|
+
#endif`
|
|
90
|
+
: ''
|
|
91
|
+
}
|
|
92
|
+
#ifdef GRADIENT_MAP
|
|
93
|
+
vec4 ramp = texture2D(gradientMap, vec2(mix(gradientMapRange.x, gradientMapRange.y, clamp(texColor.r, 0.0, 1.0)), 0.5));
|
|
94
|
+
texColor = vec4(ramp.rgb, ramp.a * texColor.a);
|
|
95
|
+
#endif
|
|
96
|
+
#ifdef EROSION
|
|
97
|
+
erosionMaskValue = texColor.a;
|
|
98
|
+
#endif
|
|
99
|
+
baseColor *= texColor;
|
|
100
|
+
#endif
|
|
101
|
+
#ifdef EROSION
|
|
102
|
+
#ifdef EROSION_MASK
|
|
103
|
+
erosionMaskValue = texture2D(erosionMask, vUV + erosionParams.zw).r;
|
|
104
|
+
#endif
|
|
105
|
+
baseColor = quarksErode(baseColor, erosionLevel, erosionMaskValue);
|
|
106
|
+
#endif
|
|
107
|
+
`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Fragment-side declarations, WGSL. */
|
|
111
|
+
export const surfaceFragmentDeclarationWGSL = /* wgsl */ `
|
|
112
|
+
#ifdef UV_SCROLL
|
|
113
|
+
uniform uvScrollOffset: vec2f;
|
|
114
|
+
#endif
|
|
115
|
+
#ifdef GRADIENT_MAP
|
|
116
|
+
var gradientMapSampler: sampler;
|
|
117
|
+
var gradientMap: texture_2d<f32>;
|
|
118
|
+
uniform gradientMapRange: vec2f;
|
|
119
|
+
#endif
|
|
120
|
+
#ifdef EROSION
|
|
121
|
+
uniform erosionParams: vec4f;
|
|
122
|
+
uniform erosionEdgeColor: vec3f;
|
|
123
|
+
#ifdef EROSION_MASK
|
|
124
|
+
var erosionMaskSampler: sampler;
|
|
125
|
+
var erosionMask: texture_2d<f32>;
|
|
126
|
+
#endif
|
|
127
|
+
|
|
128
|
+
fn quarksErode(colorIn: vec4f, level: f32, mask: f32) -> vec4f {
|
|
129
|
+
var color = colorIn;
|
|
130
|
+
let softness = uniforms.erosionParams.x;
|
|
131
|
+
let edgeWidth = uniforms.erosionParams.y;
|
|
132
|
+
let cut = mix(-(softness + edgeWidth), 1.0, 1.0 - level);
|
|
133
|
+
let visible = smoothstep(cut, cut + softness, mask);
|
|
134
|
+
if (edgeWidth > 0.0) {
|
|
135
|
+
let edge = 1.0 - smoothstep(cut + softness, cut + softness + edgeWidth, mask);
|
|
136
|
+
color = vec4f(color.rgb + uniforms.erosionEdgeColor * edge, color.a);
|
|
137
|
+
}
|
|
138
|
+
color.a *= visible;
|
|
139
|
+
return color;
|
|
140
|
+
}
|
|
141
|
+
#endif
|
|
142
|
+
`;
|
|
143
|
+
|
|
144
|
+
/** Declares `baseColor` from `vColor` and the map, with the effects applied, WGSL. */
|
|
145
|
+
export function surfaceColorWGSL(tileBlend: boolean): string {
|
|
146
|
+
return /* wgsl */ `
|
|
147
|
+
var baseColor = fragmentInputs.vColor;
|
|
148
|
+
#ifdef UV_SCROLL
|
|
149
|
+
let mapUV = fragmentInputs.vUV + uniforms.uvScrollOffset;
|
|
150
|
+
#else
|
|
151
|
+
let mapUV = fragmentInputs.vUV;
|
|
152
|
+
#endif
|
|
153
|
+
#ifdef EROSION
|
|
154
|
+
let erosionLevel = baseColor.a;
|
|
155
|
+
baseColor.a = 1.0;
|
|
156
|
+
var erosionMaskValue = 1.0;
|
|
157
|
+
#endif
|
|
158
|
+
#ifdef USE_MAP
|
|
159
|
+
var texColor = textureSample(map, mapSampler, mapUV);
|
|
160
|
+
${
|
|
161
|
+
tileBlend
|
|
162
|
+
? `#ifdef TILE_BLEND
|
|
163
|
+
#ifdef UV_SCROLL
|
|
164
|
+
let texColor2 = textureSample(map, mapSampler, fragmentInputs.vUV2 + uniforms.uvScrollOffset);
|
|
165
|
+
#else
|
|
166
|
+
let texColor2 = textureSample(map, mapSampler, fragmentInputs.vUV2);
|
|
167
|
+
#endif
|
|
168
|
+
texColor = mix(texColor, texColor2, fragmentInputs.vTileBlend);
|
|
169
|
+
#endif`
|
|
170
|
+
: ''
|
|
171
|
+
}
|
|
172
|
+
#ifdef GRADIENT_MAP
|
|
173
|
+
let ramp = textureSample(gradientMap, gradientMapSampler, vec2f(mix(uniforms.gradientMapRange.x, uniforms.gradientMapRange.y, clamp(texColor.r, 0.0, 1.0)), 0.5));
|
|
174
|
+
texColor = vec4f(ramp.rgb, ramp.a * texColor.a);
|
|
175
|
+
#endif
|
|
176
|
+
#ifdef EROSION
|
|
177
|
+
erosionMaskValue = texColor.a;
|
|
178
|
+
#endif
|
|
179
|
+
baseColor *= texColor;
|
|
180
|
+
#endif
|
|
181
|
+
#ifdef EROSION
|
|
182
|
+
#ifdef EROSION_MASK
|
|
183
|
+
erosionMaskValue = textureSample(erosionMask, erosionMaskSampler, fragmentInputs.vUV + uniforms.erosionParams.zw).r;
|
|
184
|
+
#endif
|
|
185
|
+
baseColor = quarksErode(baseColor, erosionLevel, erosionMaskValue);
|
|
186
|
+
#endif
|
|
187
|
+
`;
|
|
188
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {fogFragmentDeclarationGLSL, fogFragmentGLSL} from './chunks/fog';
|
|
2
|
+
import {surfaceColorGLSL, surfaceFragmentDeclarationGLSL} from './chunks/surface';
|
|
2
3
|
|
|
3
4
|
export default /* glsl */ `
|
|
4
5
|
varying vec2 vUV;
|
|
@@ -26,18 +27,9 @@ uniform float alphaTest;
|
|
|
26
27
|
#endif
|
|
27
28
|
|
|
28
29
|
${fogFragmentDeclarationGLSL}
|
|
30
|
+
${surfaceFragmentDeclarationGLSL}
|
|
29
31
|
void main() {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
#ifdef USE_MAP
|
|
33
|
-
vec4 texColor = texture2D(map, vUV);
|
|
34
|
-
#ifdef TILE_BLEND
|
|
35
|
-
vec4 texColor2 = texture2D(map, vUV2);
|
|
36
|
-
texColor = mix(texColor, texColor2, vTileBlend);
|
|
37
|
-
#endif
|
|
38
|
-
baseColor *= texColor;
|
|
39
|
-
#endif
|
|
40
|
-
|
|
32
|
+
${surfaceColorGLSL(true)}
|
|
41
33
|
#ifdef USE_COLOR_AS_ALPHA
|
|
42
34
|
baseColor.a *= (baseColor.r + baseColor.g + baseColor.b) / 3.0;
|
|
43
35
|
#endif
|