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.
@@ -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 === RenderMode.StretchedBillBoard) {
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 === RenderMode.StretchedBillBoard) {
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 === RenderMode.StretchedBillBoard) {
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
- if (this.settings.renderMode === RenderMode.StretchedBillBoard) {
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 === RenderMode.StretchedBillBoard) {
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, isStretchedRender);
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, isStretchedRender);
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 (isStretchedRender && this.velocityVB && this.velocityBuffer) {
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
- /** The quads that take a pivot and flip: every billboard kind, stretched included, but not meshes. */
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 {defaultMeshLighting, meshRimEqual, normalizeMeshRim, RenderMode, VFXBatch} from './VFXBatch';
45
- export type {MeshLighting, MeshLightingSettings, MeshRim, SortMode, StoredBatchSettings} from './VFXBatch';
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
- vec4 baseColor = vColor;
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