babylon.quarks 0.19.0 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +57 -0
- package/README.md +22 -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 +324 -13
- package/package.json +1 -1
- package/src/BatchedRenderer.ts +16 -2
- package/src/MeshSurfaceEmitter.ts +10 -4
- package/src/ParticleSystem.ts +124 -1
- package/src/QuarksLoader.ts +150 -4
- package/src/SpriteBatch.ts +75 -4
- package/src/TrailBatch.ts +24 -13
- package/src/VFXBatch.ts +4 -1
- package/src/shaders/particle_vert.glsl.ts +20 -0
- package/src/shaders/particle_vert.wgsl.ts +21 -1
- package/src/shaders/stretched_bb_particle_vert.glsl.ts +18 -5
- package/src/shaders/stretched_bb_particle_vert.wgsl.ts +19 -6
package/src/SpriteBatch.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {Mesh} from '@babylonjs/core/Meshes/mesh';
|
|
|
7
7
|
import {VertexData} from '@babylonjs/core/Meshes/mesh.vertexData';
|
|
8
8
|
import {Scene} from '@babylonjs/core/scene';
|
|
9
9
|
import {
|
|
10
|
+
IParticle,
|
|
10
11
|
IParticleSystem,
|
|
11
12
|
Matrix3,
|
|
12
13
|
ParticleStore,
|
|
@@ -208,6 +209,10 @@ export class SpriteBatch extends VFXBatch {
|
|
|
208
209
|
if (this.settings.renderMode === RenderMode.HorizontalBillBoard) {
|
|
209
210
|
defines.push('HORIZONTAL');
|
|
210
211
|
}
|
|
212
|
+
const pivot = SpriteBatch.takesPivotAndFlip(this.settings.renderMode) ? this.settings.pivot : undefined;
|
|
213
|
+
if (pivot) {
|
|
214
|
+
defines.push('PIVOT');
|
|
215
|
+
}
|
|
211
216
|
|
|
212
217
|
const shaderLanguage = shaderLanguageFor(this.scene.getEngine());
|
|
213
218
|
registerShaders(shaderName, vertexShader, fragmentShader, shaderLanguage);
|
|
@@ -246,6 +251,9 @@ export class SpriteBatch extends VFXBatch {
|
|
|
246
251
|
if (this.settings.materialAlphaTest > 0) {
|
|
247
252
|
uniforms.push('alphaTest');
|
|
248
253
|
}
|
|
254
|
+
if (pivot) {
|
|
255
|
+
uniforms.push('pivot');
|
|
256
|
+
}
|
|
249
257
|
if (this.settings.renderMode === RenderMode.Mesh) {
|
|
250
258
|
uniforms.push('lightDirection');
|
|
251
259
|
uniforms.push('lightColor');
|
|
@@ -311,6 +319,9 @@ export class SpriteBatch extends VFXBatch {
|
|
|
311
319
|
if (this.settings.materialAlphaTest > 0) {
|
|
312
320
|
mat.setFloat('alphaTest', this.settings.materialAlphaTest);
|
|
313
321
|
}
|
|
322
|
+
if (pivot) {
|
|
323
|
+
mat.setVector3('pivot', new BVector3(pivot.x, pivot.y, pivot.z));
|
|
324
|
+
}
|
|
314
325
|
if (this.settings.renderMode === RenderMode.Mesh) {
|
|
315
326
|
mat.setVector3('lightDirection', new BVector3(0.4, -1, 0.6));
|
|
316
327
|
mat.setVector3('lightColor', new BVector3(1, 1, 1));
|
|
@@ -343,6 +354,8 @@ export class SpriteBatch extends VFXBatch {
|
|
|
343
354
|
private rotationMat_ = new Matrix3();
|
|
344
355
|
private rotationMat2_ = new Matrix3();
|
|
345
356
|
private lastStretchedSpeedFactor = Number.NaN;
|
|
357
|
+
/** Slot 0 holds the zero-alpha instance since the batch last went empty. */
|
|
358
|
+
private clearedInstanceUploaded = false;
|
|
346
359
|
|
|
347
360
|
update(): void {
|
|
348
361
|
let index = 0;
|
|
@@ -350,6 +363,7 @@ export class SpriteBatch extends VFXBatch {
|
|
|
350
363
|
const renderMode = this.settings.renderMode;
|
|
351
364
|
const isMeshRender = renderMode === RenderMode.Mesh;
|
|
352
365
|
const isStretchedRender = renderMode === RenderMode.StretchedBillBoard;
|
|
366
|
+
const flipsSprites = SpriteBatch.takesPivotAndFlip(renderMode);
|
|
353
367
|
|
|
354
368
|
const visibleSystems = this.getVisibleSystems();
|
|
355
369
|
for (let i = 0; i < visibleSystems.length; i++) {
|
|
@@ -361,15 +375,34 @@ export class SpriteBatch extends VFXBatch {
|
|
|
361
375
|
|
|
362
376
|
for (let s = 0; s < visibleSystems.length; s++) {
|
|
363
377
|
const system = visibleSystems[s];
|
|
364
|
-
const particles = system.particles;
|
|
365
378
|
const particleNum = system.particleNum;
|
|
379
|
+
// A finished or between-bursts system adds nothing to the batch. With many effects
|
|
380
|
+
// loaded most systems are idle at any moment, so skip their per-system setup.
|
|
381
|
+
if (particleNum === 0) {
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
const particles = system.particles;
|
|
366
385
|
const rotation = this.quaternion2_;
|
|
367
386
|
const translation = this.vector2_;
|
|
368
387
|
const scale = this.vector3_;
|
|
369
388
|
const systemWorldSpace = system.worldSpace;
|
|
370
389
|
const emitterMatrix = system.emitter.matrixWorld;
|
|
371
|
-
|
|
372
|
-
|
|
390
|
+
// Only a local-space mesh needs the emitter's turn as a quaternion, and only a
|
|
391
|
+
// local-space stretched billboard its rotation matrix. Everything else needs at most
|
|
392
|
+
// the scale, which the basis column lengths give without a full decompose.
|
|
393
|
+
if (isMeshRender && !systemWorldSpace) {
|
|
394
|
+
emitterMatrix.decompose(translation, rotation, scale);
|
|
395
|
+
} else {
|
|
396
|
+
const e = emitterMatrix.elements;
|
|
397
|
+
scale.set(
|
|
398
|
+
Math.sqrt(e[0] * e[0] + e[1] * e[1] + e[2] * e[2]),
|
|
399
|
+
Math.sqrt(e[4] * e[4] + e[5] * e[5] + e[6] * e[6]),
|
|
400
|
+
Math.sqrt(e[8] * e[8] + e[9] * e[9] + e[10] * e[10])
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
if (isStretchedRender && !systemWorldSpace) {
|
|
404
|
+
this.rotationMat_.setFromMatrix4(emitterMatrix);
|
|
405
|
+
}
|
|
373
406
|
const absScaleX = Math.abs(scale.x);
|
|
374
407
|
const absScaleY = Math.abs(scale.y);
|
|
375
408
|
const absScaleZ = Math.abs(scale.z);
|
|
@@ -487,6 +520,7 @@ export class SpriteBatch extends VFXBatch {
|
|
|
487
520
|
copiedPositionAndSize = true;
|
|
488
521
|
}
|
|
489
522
|
|
|
523
|
+
const firstIndex = index;
|
|
490
524
|
for (let j = 0; j < particleNum; j++, index++) {
|
|
491
525
|
const particle = particles[j] as SpriteParticle;
|
|
492
526
|
|
|
@@ -600,6 +634,10 @@ export class SpriteBatch extends VFXBatch {
|
|
|
600
634
|
this.velocityBuffer[vi + 3] = lengthFactor;
|
|
601
635
|
}
|
|
602
636
|
}
|
|
637
|
+
|
|
638
|
+
if (flipsSprites && system.flip !== undefined) {
|
|
639
|
+
SpriteBatch.applyFlip(this.sizeBuffer, firstIndex * 3, particles, particleNum);
|
|
640
|
+
}
|
|
603
641
|
}
|
|
604
642
|
|
|
605
643
|
if (isStretchedRender && visibleSystems.length > 0) {
|
|
@@ -611,9 +649,15 @@ export class SpriteBatch extends VFXBatch {
|
|
|
611
649
|
this.mesh.forcedInstanceCount = index;
|
|
612
650
|
|
|
613
651
|
if (index === 0) {
|
|
614
|
-
|
|
652
|
+
// Slot 0 only needs clearing once per stretch of empty frames; re-uploading it every
|
|
653
|
+
// frame cost five buffer uploads per idle batch per frame.
|
|
654
|
+
if (!this.clearedInstanceUploaded) {
|
|
655
|
+
this.uploadClearedInstance(isMeshRender, isStretchedRender);
|
|
656
|
+
this.clearedInstanceUploaded = true;
|
|
657
|
+
}
|
|
615
658
|
return;
|
|
616
659
|
}
|
|
660
|
+
this.clearedInstanceUploaded = false;
|
|
617
661
|
|
|
618
662
|
// Upload only the slots actually filled this frame: the buffers are sized
|
|
619
663
|
// for the high-water mark and are usually far larger than the live count.
|
|
@@ -648,6 +692,33 @@ export class SpriteBatch extends VFXBatch {
|
|
|
648
692
|
}
|
|
649
693
|
}
|
|
650
694
|
|
|
695
|
+
/** The quads that take a pivot and flip: every billboard kind, stretched included, but not meshes. */
|
|
696
|
+
private static takesPivotAndFlip(renderMode: RenderMode): boolean {
|
|
697
|
+
return (
|
|
698
|
+
renderMode === RenderMode.BillBoard ||
|
|
699
|
+
renderMode === RenderMode.StretchedBillBoard ||
|
|
700
|
+
renderMode === RenderMode.VerticalBillBoard ||
|
|
701
|
+
renderMode === RenderMode.HorizontalBillBoard
|
|
702
|
+
);
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
/**
|
|
706
|
+
* Negates the size of each flipped particle along the axes it flips on,
|
|
707
|
+
* starting at `base` in `sizes`. The billboard shader scales the quad by
|
|
708
|
+
* the size, so a negative one mirrors it — and with it the texture, the
|
|
709
|
+
* quad being centred — the way Unity's renderer Flip mirrors U and V. The
|
|
710
|
+
* stretched shader reads the sign and mirrors the texture coordinates.
|
|
711
|
+
*/
|
|
712
|
+
private static applyFlip(sizes: Float32Array, base: number, particles: IParticle[], count: number): void {
|
|
713
|
+
for (let j = 0, o = base; j < count; j++, o += 3) {
|
|
714
|
+
const flip = (particles[j] as SpriteParticle).flip;
|
|
715
|
+
if (flip !== 0) {
|
|
716
|
+
if (flip & 1) sizes[o] = -sizes[o];
|
|
717
|
+
if (flip & 2) sizes[o + 1] = -sizes[o + 1];
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
|
|
651
722
|
/**
|
|
652
723
|
* The fraction `f` of a turn, as a normalised lerp from no rotation toward
|
|
653
724
|
* `step`, written into `out`.
|
package/src/TrailBatch.ts
CHANGED
|
@@ -276,9 +276,13 @@ export class TrailBatch extends VFXBatch {
|
|
|
276
276
|
// body still and the tip moving without that pulse. A trail pinned
|
|
277
277
|
// to the emitter's origin is driven by the emitter's transform
|
|
278
278
|
// rather than by the particle's velocity, so it is left alone.
|
|
279
|
-
const
|
|
280
|
-
|
|
281
|
-
|
|
279
|
+
const trailSettings = system.rendererEmitterSettings as TrailSettings;
|
|
280
|
+
const uvFromHead = Boolean(trailSettings.uvFromHead);
|
|
281
|
+
const colorFromParticle = Boolean(trailSettings.colorFromParticle);
|
|
282
|
+
// Recorded samples already sit in world space; the live tip still comes
|
|
283
|
+
// from the particle's own (local) position.
|
|
284
|
+
const samplesInWorld = systemWorldSpace || Boolean(trailSettings.recordWorldSpace);
|
|
285
|
+
const residual = trailSettings.followLocalOrigin ? 0 : (system.simulationResidual ?? 0);
|
|
282
286
|
// Measured from the step the particle just took, not from its
|
|
283
287
|
// velocity: a trail pulled along by an orbit or by a plugin's own
|
|
284
288
|
// behavior has motion no velocity term describes.
|
|
@@ -293,7 +297,10 @@ export class TrailBatch extends VFXBatch {
|
|
|
293
297
|
const historyCapacity = particle.historyCapacity;
|
|
294
298
|
const historyPositions = particle.historyPositions;
|
|
295
299
|
const historySizes = particle.historySizes;
|
|
296
|
-
|
|
300
|
+
// Every sample takes its color from one place: its own recorded slot, or
|
|
301
|
+
// the particle's current row in the store.
|
|
302
|
+
const colors = colorFromParticle ? particle.store.color : particle.historyColors;
|
|
303
|
+
const particleColorIndex = particle.storeIndex * 4;
|
|
297
304
|
const col = particle.uvTile % vTileCount;
|
|
298
305
|
const row = Math.floor(particle.uvTile / vTileCount + 0.001);
|
|
299
306
|
const particleMatrix = particle.parentMatrix as unknown as Matrix4 | undefined;
|
|
@@ -314,7 +321,10 @@ export class TrailBatch extends VFXBatch {
|
|
|
314
321
|
m31 = me[13],
|
|
315
322
|
m32 = me[14],
|
|
316
323
|
m33 = me[15];
|
|
317
|
-
|
|
324
|
+
// The texture spans the ribbon end to end: up to the live tip when one is
|
|
325
|
+
// drawn, otherwise up to the newest recorded sample.
|
|
326
|
+
const invHistoryLength =
|
|
327
|
+
1 / (stepFraction !== 0 ? particleHistoryLength : Math.max(1, particleHistoryLength - 1));
|
|
318
328
|
|
|
319
329
|
// Walk the ring buffer oldest-first with hand-rolled wrapping so the
|
|
320
330
|
// inner loop stays free of modulo operations.
|
|
@@ -344,16 +354,16 @@ export class TrailBatch extends VFXBatch {
|
|
|
344
354
|
let nextZ = historyPositions[nextPosIndex + 2];
|
|
345
355
|
|
|
346
356
|
const currentSize = historySizes[currentSlot];
|
|
347
|
-
const currentColorIndex = currentSlot * 4;
|
|
348
|
-
const currentColorX =
|
|
349
|
-
const currentColorY =
|
|
350
|
-
const currentColorZ =
|
|
351
|
-
const currentColorW =
|
|
357
|
+
const currentColorIndex = colorFromParticle ? particleColorIndex : currentSlot * 4;
|
|
358
|
+
const currentColorX = colors[currentColorIndex];
|
|
359
|
+
const currentColorY = colors[currentColorIndex + 1];
|
|
360
|
+
const currentColorZ = colors[currentColorIndex + 2];
|
|
361
|
+
const currentColorW = colors[currentColorIndex + 3];
|
|
352
362
|
|
|
353
363
|
previousSlot = currentSlot;
|
|
354
364
|
currentSlot = nextSlot;
|
|
355
365
|
|
|
356
|
-
if (!
|
|
366
|
+
if (!samplesInWorld) {
|
|
357
367
|
// Inlined point transforms — three per trail sample.
|
|
358
368
|
let w = 1 / (m03 * currentX + m13 * currentY + m23 * currentZ + m33);
|
|
359
369
|
let tx = (m00 * currentX + m10 * currentY + m20 * currentZ + m30) * w;
|
|
@@ -411,7 +421,8 @@ export class TrailBatch extends VFXBatch {
|
|
|
411
421
|
}
|
|
412
422
|
|
|
413
423
|
const ui = index * 2;
|
|
414
|
-
const
|
|
424
|
+
const along = i * invHistoryLength;
|
|
425
|
+
const u = ((uvFromHead ? 1 - along : along) + col) * tileWidth;
|
|
415
426
|
this.uvBuffer[ui] = u;
|
|
416
427
|
this.uvBuffer[ui + 1] = (vTileCount - row - 1) * tileHeight;
|
|
417
428
|
this.uvBuffer[ui + 2] = u;
|
|
@@ -507,7 +518,7 @@ export class TrailBatch extends VFXBatch {
|
|
|
507
518
|
}
|
|
508
519
|
|
|
509
520
|
const tipUi = index * 2;
|
|
510
|
-
const tipU = (1 + col) * tileWidth;
|
|
521
|
+
const tipU = ((uvFromHead ? 0 : 1) + col) * tileWidth;
|
|
511
522
|
this.uvBuffer[tipUi] = tipU;
|
|
512
523
|
this.uvBuffer[tipUi + 1] = (vTileCount - row - 1) * tileHeight;
|
|
513
524
|
this.uvBuffer[tipUi + 2] = tipU;
|
package/src/VFXBatch.ts
CHANGED
|
@@ -2,7 +2,7 @@ import {BaseTexture} from '@babylonjs/core/Materials/Textures/baseTexture';
|
|
|
2
2
|
import {ShaderMaterial} from '@babylonjs/core/Materials/shaderMaterial';
|
|
3
3
|
import {Mesh} from '@babylonjs/core/Meshes/mesh';
|
|
4
4
|
import {Scene} from '@babylonjs/core/scene';
|
|
5
|
-
import {IParticleSystem} from 'quarks.core';
|
|
5
|
+
import {IParticleSystem, Vector3} from 'quarks.core';
|
|
6
6
|
import {VFXBatchSettings} from './BatchedRenderer';
|
|
7
7
|
|
|
8
8
|
export enum RenderMode {
|
|
@@ -38,6 +38,8 @@ export interface StoredBatchSettings {
|
|
|
38
38
|
reflectionFaces: BaseTexture[] | null;
|
|
39
39
|
reflectionAtlas: BaseTexture | null;
|
|
40
40
|
layerMask: number;
|
|
41
|
+
/** Billboard pivot in particle-size units, see {@link VFXBatchSettings.pivot}. */
|
|
42
|
+
pivot?: Vector3;
|
|
41
43
|
}
|
|
42
44
|
|
|
43
45
|
export abstract class VFXBatch {
|
|
@@ -75,6 +77,7 @@ export abstract class VFXBatch {
|
|
|
75
77
|
reflectionLevel: settings.reflectionLevel ?? 1,
|
|
76
78
|
reflectionFaces: settings.reflectionFaces ?? null,
|
|
77
79
|
reflectionAtlas: settings.reflectionAtlas ?? null,
|
|
80
|
+
pivot: settings.pivot?.clone(),
|
|
78
81
|
layerMask: settings.layerMask,
|
|
79
82
|
};
|
|
80
83
|
this.mesh = new Mesh('vfxBatch', scene);
|
|
@@ -21,6 +21,11 @@ uniform float tileCountX;
|
|
|
21
21
|
uniform float tileCountY;
|
|
22
22
|
#endif
|
|
23
23
|
|
|
24
|
+
#ifdef PIVOT
|
|
25
|
+
// Quad offset in particle-size units: xy in the quad's plane, z toward the camera.
|
|
26
|
+
uniform vec3 pivot;
|
|
27
|
+
#endif
|
|
28
|
+
|
|
24
29
|
// Varyings
|
|
25
30
|
varying vec2 vUV;
|
|
26
31
|
varying vec4 vColor;
|
|
@@ -37,7 +42,12 @@ void main() {
|
|
|
37
42
|
float c = cos(rotation);
|
|
38
43
|
float s = sin(rotation);
|
|
39
44
|
|
|
45
|
+
// A negative size mirrors the quad, which is how flipped particles arrive.
|
|
40
46
|
vec2 alignedPosition = position.xy * size.xy;
|
|
47
|
+
#ifdef PIVOT
|
|
48
|
+
// The pivot turns with the particle but is not mirrored with it.
|
|
49
|
+
alignedPosition += pivot.xy * abs(size.xy);
|
|
50
|
+
#endif
|
|
41
51
|
vec2 rotatedPosition;
|
|
42
52
|
rotatedPosition.x = c * alignedPosition.x - s * alignedPosition.y;
|
|
43
53
|
rotatedPosition.y = s * alignedPosition.x + c * alignedPosition.y;
|
|
@@ -46,6 +56,9 @@ void main() {
|
|
|
46
56
|
vec4 mvPosition = world * vec4(offset, 1.0);
|
|
47
57
|
mvPosition.x += rotatedPosition.x;
|
|
48
58
|
mvPosition.z -= rotatedPosition.y;
|
|
59
|
+
#ifdef PIVOT
|
|
60
|
+
mvPosition.y += pivot.z * abs(size.x);
|
|
61
|
+
#endif
|
|
49
62
|
mvPosition = view * mvPosition;
|
|
50
63
|
#elif defined(VERTICAL)
|
|
51
64
|
vec4 mvPosition = world * vec4(offset, 1.0);
|
|
@@ -55,6 +68,13 @@ void main() {
|
|
|
55
68
|
#else
|
|
56
69
|
vec4 mvPosition = view * world * vec4(offset, 1.0);
|
|
57
70
|
mvPosition.xy += rotatedPosition;
|
|
71
|
+
#endif
|
|
72
|
+
#ifdef PIVOT
|
|
73
|
+
#ifndef HORIZONTAL
|
|
74
|
+
// Toward the camera along its view axis, whichever way the view space
|
|
75
|
+
// faces: anything in front of the camera has the sign of the far side.
|
|
76
|
+
mvPosition.z -= sign(mvPosition.z) * pivot.z * abs(size.x);
|
|
77
|
+
#endif
|
|
58
78
|
#endif
|
|
59
79
|
|
|
60
80
|
gl_Position = projection * mvPosition;
|
|
@@ -20,6 +20,11 @@ uniform tileCountX: f32;
|
|
|
20
20
|
uniform tileCountY: f32;
|
|
21
21
|
#endif
|
|
22
22
|
|
|
23
|
+
#ifdef PIVOT
|
|
24
|
+
// Quad offset in particle-size units: xy in the quad's plane, z toward the camera.
|
|
25
|
+
uniform pivot: vec3f;
|
|
26
|
+
#endif
|
|
27
|
+
|
|
23
28
|
// Varyings
|
|
24
29
|
varying vUV: vec2f;
|
|
25
30
|
varying vColor: vec4f;
|
|
@@ -37,7 +42,12 @@ fn main(input: VertexInputs) -> FragmentInputs {
|
|
|
37
42
|
let c = cos(vertexInputs.rotation);
|
|
38
43
|
let s = sin(vertexInputs.rotation);
|
|
39
44
|
|
|
40
|
-
|
|
45
|
+
// A negative size mirrors the quad, which is how flipped particles arrive.
|
|
46
|
+
var alignedPosition = vertexInputs.position.xy * vertexInputs.size.xy;
|
|
47
|
+
#ifdef PIVOT
|
|
48
|
+
// The pivot turns with the particle but is not mirrored with it.
|
|
49
|
+
alignedPosition += uniforms.pivot.xy * abs(vertexInputs.size.xy);
|
|
50
|
+
#endif
|
|
41
51
|
let rotatedPosition = vec2f(
|
|
42
52
|
c * alignedPosition.x - s * alignedPosition.y,
|
|
43
53
|
s * alignedPosition.x + c * alignedPosition.y);
|
|
@@ -46,6 +56,9 @@ fn main(input: VertexInputs) -> FragmentInputs {
|
|
|
46
56
|
var mvPosition = uniforms.world * vec4f(vertexInputs.offset, 1.0);
|
|
47
57
|
mvPosition.x += rotatedPosition.x;
|
|
48
58
|
mvPosition.z -= rotatedPosition.y;
|
|
59
|
+
#ifdef PIVOT
|
|
60
|
+
mvPosition.y += uniforms.pivot.z * abs(vertexInputs.size.x);
|
|
61
|
+
#endif
|
|
49
62
|
mvPosition = uniforms.view * mvPosition;
|
|
50
63
|
#elif defined(VERTICAL)
|
|
51
64
|
var mvPosition = uniforms.world * vec4f(vertexInputs.offset, 1.0);
|
|
@@ -56,6 +69,13 @@ fn main(input: VertexInputs) -> FragmentInputs {
|
|
|
56
69
|
var mvPosition = uniforms.view * uniforms.world * vec4f(vertexInputs.offset, 1.0);
|
|
57
70
|
mvPosition.x += rotatedPosition.x;
|
|
58
71
|
mvPosition.y += rotatedPosition.y;
|
|
72
|
+
#endif
|
|
73
|
+
#ifdef PIVOT
|
|
74
|
+
#ifndef HORIZONTAL
|
|
75
|
+
// Toward the camera along its view axis, whichever way the view space
|
|
76
|
+
// faces: anything in front of the camera has the sign of the far side.
|
|
77
|
+
mvPosition.z -= sign(mvPosition.z) * uniforms.pivot.z * abs(vertexInputs.size.x);
|
|
78
|
+
#endif
|
|
59
79
|
#endif
|
|
60
80
|
|
|
61
81
|
let clipPosition = uniforms.projection * mvPosition;
|
|
@@ -22,6 +22,11 @@ uniform float tileCountX;
|
|
|
22
22
|
uniform float tileCountY;
|
|
23
23
|
#endif
|
|
24
24
|
|
|
25
|
+
#ifdef PIVOT
|
|
26
|
+
// Streak offset in particle-size units: x from head to tail, y across.
|
|
27
|
+
uniform vec3 pivot;
|
|
28
|
+
#endif
|
|
29
|
+
|
|
25
30
|
// Varyings
|
|
26
31
|
varying vec2 vUV;
|
|
27
32
|
varying vec4 vColor;
|
|
@@ -36,7 +41,9 @@ varying float linearDepth;
|
|
|
36
41
|
|
|
37
42
|
void main() {
|
|
38
43
|
float lengthFactor = velocity.w;
|
|
39
|
-
|
|
44
|
+
// A flipped particle arrives with a negative size: the streak keeps its
|
|
45
|
+
// extent and mirrors its texture instead (see quadUV below).
|
|
46
|
+
float avgSize = (abs(size.x) + abs(size.y)) * 0.5;
|
|
40
47
|
vec4 mvPosition = view * world * vec4(offset, 1.0);
|
|
41
48
|
vec3 viewVelocity = mat3(view * world) * velocity.xyz;
|
|
42
49
|
float vlength = length(viewVelocity);
|
|
@@ -44,11 +51,17 @@ void main() {
|
|
|
44
51
|
// Deriving it via normalize keeps the stretch aligned even when the speed contribution is ~0
|
|
45
52
|
// (e.g. speedFactor 0), instead of collapsing the whole burst onto a fixed screen axis.
|
|
46
53
|
vec3 vdir = vlength > 0.000001 ? viewVelocity / vlength : vec3(0.0, 0.0, 1.0);
|
|
47
|
-
|
|
54
|
+
vec3 side = normalize(cross(mvPosition.xyz, vdir));
|
|
55
|
+
mvPosition.xyz += position.y * side * avgSize;
|
|
48
56
|
// Equivalent to viewVelocity * (1.0 + lengthFactor / vlength) for moving particles, but the
|
|
49
57
|
// size-based term (vdir * lengthFactor) survives when the velocity contribution vanishes.
|
|
50
58
|
mvPosition.xyz -= (position.x + 0.5) * (viewVelocity + vdir * lengthFactor) * avgSize;
|
|
59
|
+
#ifdef PIVOT
|
|
60
|
+
// In particle-size units along the quad's own axes: x from head to tail, y across.
|
|
61
|
+
mvPosition.xyz += (pivot.y * side - pivot.x * vdir) * avgSize;
|
|
62
|
+
#endif
|
|
51
63
|
gl_Position = projection * mvPosition;
|
|
64
|
+
vec2 quadUV = vec2(size.x < 0.0 ? 1.0 - uv.x : uv.x, size.y < 0.0 ? 1.0 - uv.y : uv.y);
|
|
52
65
|
#ifdef SOFT_PARTICLES
|
|
53
66
|
projPosition = gl_Position;
|
|
54
67
|
linearDepth = -mvPosition.z;
|
|
@@ -59,16 +72,16 @@ void main() {
|
|
|
59
72
|
float baseTile = floor(uvTile);
|
|
60
73
|
float tileU = mod(baseTile, tc.x) / tc.x;
|
|
61
74
|
float tileV = 1.0 - floor(baseTile / tc.x) / tc.y - 1.0 / tc.y;
|
|
62
|
-
vUV =
|
|
75
|
+
vUV = quadUV / tc + vec2(tileU, tileV);
|
|
63
76
|
#ifdef TILE_BLEND
|
|
64
77
|
float nextTile = ceil(uvTile);
|
|
65
78
|
float nextU = mod(nextTile, tc.x) / tc.x;
|
|
66
79
|
float nextV = 1.0 - floor(nextTile / tc.x) / tc.y - 1.0 / tc.y;
|
|
67
|
-
vUV2 =
|
|
80
|
+
vUV2 = quadUV / tc + vec2(nextU, nextV);
|
|
68
81
|
vTileBlend = fract(uvTile);
|
|
69
82
|
#endif
|
|
70
83
|
#else
|
|
71
|
-
vUV =
|
|
84
|
+
vUV = quadUV;
|
|
72
85
|
#endif
|
|
73
86
|
|
|
74
87
|
vColor = color;
|
|
@@ -22,6 +22,11 @@ uniform tileCountX: f32;
|
|
|
22
22
|
uniform tileCountY: f32;
|
|
23
23
|
#endif
|
|
24
24
|
|
|
25
|
+
#ifdef PIVOT
|
|
26
|
+
// Streak offset in particle-size units: x from head to tail, y across.
|
|
27
|
+
uniform pivot: vec3f;
|
|
28
|
+
#endif
|
|
29
|
+
|
|
25
30
|
// Varyings
|
|
26
31
|
varying vUV: vec2f;
|
|
27
32
|
varying vColor: vec4f;
|
|
@@ -37,7 +42,9 @@ varying linearDepth: f32;
|
|
|
37
42
|
@vertex
|
|
38
43
|
fn main(input: VertexInputs) -> FragmentInputs {
|
|
39
44
|
let lengthFactor = vertexInputs.velocity.w;
|
|
40
|
-
|
|
45
|
+
// A flipped particle arrives with a negative size: the streak keeps its
|
|
46
|
+
// extent and mirrors its texture instead (see quadUV below).
|
|
47
|
+
let avgSize = (abs(vertexInputs.size.x) + abs(vertexInputs.size.y)) * 0.5;
|
|
41
48
|
var mvPosition = uniforms.view * uniforms.world * vec4f(vertexInputs.offset, 1.0);
|
|
42
49
|
let viewWorld = uniforms.view * uniforms.world;
|
|
43
50
|
let viewWorld3 = mat3x3f(viewWorld[0].xyz, viewWorld[1].xyz, viewWorld[2].xyz);
|
|
@@ -50,17 +57,23 @@ fn main(input: VertexInputs) -> FragmentInputs {
|
|
|
50
57
|
if (vlength > 0.000001) {
|
|
51
58
|
vdir = viewVelocity / vlength;
|
|
52
59
|
}
|
|
53
|
-
let
|
|
60
|
+
let side = normalize(cross(mvPosition.xyz, vdir));
|
|
61
|
+
let widthOffset = vertexInputs.position.y * side * avgSize;
|
|
54
62
|
// Equivalent to viewVelocity * (1.0 + lengthFactor / vlength) for moving particles, but the
|
|
55
63
|
// size-based term (vdir * lengthFactor) survives when the velocity contribution vanishes.
|
|
56
64
|
let lengthOffset = (vertexInputs.position.x + 0.5) * (viewVelocity + vdir * lengthFactor) * avgSize;
|
|
57
|
-
|
|
65
|
+
var stretched = mvPosition.xyz + widthOffset - lengthOffset;
|
|
66
|
+
#ifdef PIVOT
|
|
67
|
+
// In particle-size units along the quad's own axes: x from head to tail, y across.
|
|
68
|
+
stretched += (uniforms.pivot.y * side - uniforms.pivot.x * vdir) * avgSize;
|
|
69
|
+
#endif
|
|
58
70
|
mvPosition.x = stretched.x;
|
|
59
71
|
mvPosition.y = stretched.y;
|
|
60
72
|
mvPosition.z = stretched.z;
|
|
61
73
|
|
|
62
74
|
let clipPosition = uniforms.projection * mvPosition;
|
|
63
75
|
vertexOutputs.position = clipPosition;
|
|
76
|
+
let quadUV = select(vertexInputs.uv, 1.0 - vertexInputs.uv, vertexInputs.size.xy < vec2f(0.0));
|
|
64
77
|
#ifdef SOFT_PARTICLES
|
|
65
78
|
vertexOutputs.projPosition = clipPosition;
|
|
66
79
|
vertexOutputs.linearDepth = -mvPosition.z;
|
|
@@ -71,16 +84,16 @@ fn main(input: VertexInputs) -> FragmentInputs {
|
|
|
71
84
|
let baseTile = floor(vertexInputs.uvTile);
|
|
72
85
|
let tileU = (baseTile % tc.x) / tc.x;
|
|
73
86
|
let tileV = 1.0 - floor(baseTile / tc.x) / tc.y - 1.0 / tc.y;
|
|
74
|
-
vertexOutputs.vUV =
|
|
87
|
+
vertexOutputs.vUV = quadUV / tc + vec2f(tileU, tileV);
|
|
75
88
|
#ifdef TILE_BLEND
|
|
76
89
|
let nextTile = ceil(vertexInputs.uvTile);
|
|
77
90
|
let nextU = (nextTile % tc.x) / tc.x;
|
|
78
91
|
let nextV = 1.0 - floor(nextTile / tc.x) / tc.y - 1.0 / tc.y;
|
|
79
|
-
vertexOutputs.vUV2 =
|
|
92
|
+
vertexOutputs.vUV2 = quadUV / tc + vec2f(nextU, nextV);
|
|
80
93
|
vertexOutputs.vTileBlend = fract(vertexInputs.uvTile);
|
|
81
94
|
#endif
|
|
82
95
|
#else
|
|
83
|
-
vertexOutputs.vUV =
|
|
96
|
+
vertexOutputs.vUV = quadUV;
|
|
84
97
|
#endif
|
|
85
98
|
|
|
86
99
|
vertexOutputs.vColor = vertexInputs.color;
|