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/ParticleSystem.ts
CHANGED
|
@@ -53,7 +53,16 @@ import {BatchedRenderer, VFXBatchSettings} from './BatchedRenderer';
|
|
|
53
53
|
import {ensureEnvAtlasFromCube, getCachedEnvAtlas} from './envAtlas';
|
|
54
54
|
import {ensureTriangleIndices} from './geometryUtil';
|
|
55
55
|
import {ParticleEmitter} from './ParticleEmitter';
|
|
56
|
-
import {
|
|
56
|
+
import {
|
|
57
|
+
MeshLighting,
|
|
58
|
+
MeshRim,
|
|
59
|
+
normalizeErosion,
|
|
60
|
+
normalizeMeshRim,
|
|
61
|
+
normalizeUVScroll,
|
|
62
|
+
ParticleErosion,
|
|
63
|
+
RenderMode,
|
|
64
|
+
SortMode,
|
|
65
|
+
} from './VFXBatch';
|
|
57
66
|
|
|
58
67
|
export interface BurstParameters {
|
|
59
68
|
time: number;
|
|
@@ -81,6 +90,27 @@ export interface BurstParameters {
|
|
|
81
90
|
*/
|
|
82
91
|
export type CullingMode = 'always' | 'pause' | 'pauseAndCatchUp';
|
|
83
92
|
|
|
93
|
+
/**
|
|
94
|
+
* What the emitter's world scale does to a world-space system (Unity's
|
|
95
|
+
* Scaling Mode, as far as it differs here). A local-space system lives in its
|
|
96
|
+
* emitter's space, so its scale reaches everything either way.
|
|
97
|
+
*
|
|
98
|
+
* - `'shape'` (default): the scale sizes the emission shape and the
|
|
99
|
+
* particles; speeds, forces and speed limits stay in world units, so a
|
|
100
|
+
* scaled-down effect moves as far as the full-size one.
|
|
101
|
+
* - `'hierarchy'`: the scale also multiplies start speeds (per axis),
|
|
102
|
+
* `ForceOverLife` and `LimitSpeedOverLife` (by the mean of the axes), so the
|
|
103
|
+
* whole effect, motion included, scales like a model would.
|
|
104
|
+
*/
|
|
105
|
+
export type ScalingMode = 'shape' | 'hierarchy';
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Where a {@link RenderMode.Beam} system's quads end: a point in world space,
|
|
109
|
+
* or anything with a world position, such as a Babylon `TransformNode`, read
|
|
110
|
+
* every frame.
|
|
111
|
+
*/
|
|
112
|
+
export type BeamEnd = {x: number; y: number; z: number} | {getAbsolutePosition(): {x: number; y: number; z: number}};
|
|
113
|
+
|
|
84
114
|
/** Particle backed by a {@link ParticleStore} row, as this system always creates. */
|
|
85
115
|
type StoreBackedParticle = Particle & {storeIndex: number; setStoreIndex(index: number): void};
|
|
86
116
|
|
|
@@ -199,10 +229,18 @@ export interface ParticleSystemParameters {
|
|
|
199
229
|
lighting?: MeshLighting;
|
|
200
230
|
/** Rim (Fresnel) term on mesh particles, see {@link ParticleSystem.rim}. Default none. */
|
|
201
231
|
rim?: Partial<MeshRim>;
|
|
232
|
+
/** Map UV scroll in UV units per second, see {@link ParticleSystem.uvScroll}. Default none. */
|
|
233
|
+
uvScroll?: [number, number];
|
|
234
|
+
/** Colour ramp for the map, see {@link ParticleSystem.gradientMap}. Default none. */
|
|
235
|
+
gradientMap?: BaseTexture | null;
|
|
236
|
+
/** Alpha erosion, see {@link ParticleSystem.erosion}. Default none. */
|
|
237
|
+
erosion?: Partial<ParticleErosion>;
|
|
202
238
|
/** Multiplier on the time the system is advanced by, see {@link ParticleSystem.playbackSpeed}. Default 1. */
|
|
203
239
|
playbackSpeed?: number;
|
|
204
240
|
/** What the system does while off screen, see {@link CullingMode}. Default `'always'`. */
|
|
205
241
|
cullingMode?: CullingMode;
|
|
242
|
+
/** What the emitter's scale does to a world-space system, see {@link ScalingMode}. Default `'shape'`. */
|
|
243
|
+
scalingMode?: ScalingMode;
|
|
206
244
|
/** Seed for this system's own random generator, see {@link ParticleSystem.seed}. Default none. */
|
|
207
245
|
seed?: number;
|
|
208
246
|
/** Draw order within the batch, see {@link ParticleSystem.sortMode}. Default `'none'`. */
|
|
@@ -258,6 +296,8 @@ export interface ParticleSystemJSONParameters {
|
|
|
258
296
|
playbackSpeed?: number;
|
|
259
297
|
/** Written only when not `'always'`. */
|
|
260
298
|
cullingMode?: CullingMode;
|
|
299
|
+
/** Written only when not `'shape'`. */
|
|
300
|
+
scalingMode?: ScalingMode;
|
|
261
301
|
/** Written only when set. */
|
|
262
302
|
seed?: number;
|
|
263
303
|
/** Written only when not `'none'`. */
|
|
@@ -587,6 +627,77 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
587
627
|
this.neededToUpdateRender = true;
|
|
588
628
|
}
|
|
589
629
|
|
|
630
|
+
/**
|
|
631
|
+
* Scrolls the map's UV by `[u, v]` UV units per second on the renderer's
|
|
632
|
+
* clock: flowing energy, lava, a smear running along a mesh. The texture
|
|
633
|
+
* should wrap. Undefined (the default) or zero does not scroll. Assign a
|
|
634
|
+
* new array to change it. Saved with the material in the effect JSON.
|
|
635
|
+
*/
|
|
636
|
+
get uvScroll(): [number, number] | undefined {
|
|
637
|
+
return this.rendererSettings.uvScroll;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
set uvScroll(scroll: [number, number] | undefined) {
|
|
641
|
+
this.rendererSettings.uvScroll = normalizeUVScroll(scroll);
|
|
642
|
+
this.neededToUpdateRender = true;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* Colour ramp the map is recoloured through (a gradient map): the map's
|
|
647
|
+
* red channel picks a colour along U of this texture, whose alpha
|
|
648
|
+
* multiplies the map's; the particle colour still tints the result. One
|
|
649
|
+
* greyscale texture then serves every palette. Null (the default): none.
|
|
650
|
+
* Saved with the material in the effect JSON.
|
|
651
|
+
*/
|
|
652
|
+
get gradientMap(): BaseTexture | null {
|
|
653
|
+
return this.rendererSettings.gradientMap ?? null;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
set gradientMap(texture: BaseTexture | null) {
|
|
657
|
+
this.rendererSettings.gradientMap = texture ?? null;
|
|
658
|
+
this.neededToUpdateRender = true;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/**
|
|
662
|
+
* Alpha erosion, see {@link ParticleErosion}: the particle's alpha decides
|
|
663
|
+
* how much of the shape is left rather than how transparent it is, so a
|
|
664
|
+
* `ColorOverLife` alpha curve dissolves the shape through a noise mask or
|
|
665
|
+
* wipes it through a gradient one — and an effect told to stop can fade
|
|
666
|
+
* out that way. Undefined (the default): alpha is plain transparency.
|
|
667
|
+
* Fields left out take their defaults; assign a new object to change it.
|
|
668
|
+
* Saved with the material in the effect JSON.
|
|
669
|
+
*/
|
|
670
|
+
get erosion(): ParticleErosion | undefined {
|
|
671
|
+
return this.rendererSettings.erosion;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
set erosion(erosion: Partial<ParticleErosion> | undefined) {
|
|
675
|
+
this.rendererSettings.erosion = normalizeErosion(erosion);
|
|
676
|
+
this.neededToUpdateRender = true;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
/**
|
|
680
|
+
* The emitter's world scale per axis, measured with {@link motionScale}
|
|
681
|
+
* before every step; what `'hierarchy'` multiplies start speeds by. Taken
|
|
682
|
+
* from the system's own emitter rather than the emission matrix, whose
|
|
683
|
+
* columns carry a parent particle's velocity for a sub emitter.
|
|
684
|
+
*/
|
|
685
|
+
private readonly hierarchyScale = new Vector3(1, 1, 1);
|
|
686
|
+
|
|
687
|
+
/** The mean world scale of the emitter for a world-space `'hierarchy'` system, else 1. */
|
|
688
|
+
private measureMotionScale(): number {
|
|
689
|
+
if (this.scalingMode !== 'hierarchy' || !this.worldSpace) {
|
|
690
|
+
this.hierarchyScale.set(1, 1, 1);
|
|
691
|
+
return 1;
|
|
692
|
+
}
|
|
693
|
+
const e = this.emitter.matrixWorld.elements;
|
|
694
|
+
const sx = Math.sqrt(e[0] * e[0] + e[1] * e[1] + e[2] * e[2]);
|
|
695
|
+
const sy = Math.sqrt(e[4] * e[4] + e[5] * e[5] + e[6] * e[6]);
|
|
696
|
+
const sz = Math.sqrt(e[8] * e[8] + e[9] * e[9] + e[10] * e[10]);
|
|
697
|
+
this.hierarchyScale.set(sx, sy, sz);
|
|
698
|
+
return (sx + sy + sz) / 3;
|
|
699
|
+
}
|
|
700
|
+
|
|
590
701
|
/** `lighting` if it is one of the modes, else the default. */
|
|
591
702
|
private static validLighting(lighting: unknown): MeshLighting {
|
|
592
703
|
return lighting === 'unlit' || lighting === 'scene' ? lighting : 'fixed';
|
|
@@ -674,6 +785,7 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
674
785
|
case RenderMode.BillBoard:
|
|
675
786
|
case RenderMode.VerticalBillBoard:
|
|
676
787
|
case RenderMode.HorizontalBillBoard:
|
|
788
|
+
case RenderMode.Beam:
|
|
677
789
|
this.rendererEmitterSettings = {};
|
|
678
790
|
break;
|
|
679
791
|
}
|
|
@@ -730,9 +842,28 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
730
842
|
*/
|
|
731
843
|
playbackSpeed = 1;
|
|
732
844
|
|
|
845
|
+
/**
|
|
846
|
+
* Runtime multiplier on how many particles the system emits: bursts,
|
|
847
|
+
* emission over time and over distance alike. Unlike
|
|
848
|
+
* {@link setQualityFactor}, which only ever thins an effect, it may go
|
|
849
|
+
* above 1 to make a stronger version of the same effect. The two multiply.
|
|
850
|
+
* 1 by default; not saved in the effect JSON.
|
|
851
|
+
*/
|
|
852
|
+
emissionMultiplier = 1;
|
|
853
|
+
|
|
733
854
|
/** What this system does while off screen, see {@link CullingMode}. `'always'` by default. */
|
|
734
855
|
cullingMode: CullingMode = 'always';
|
|
735
856
|
|
|
857
|
+
/** What the emitter's world scale does to a world-space system, see {@link ScalingMode}. `'shape'` by default. */
|
|
858
|
+
scalingMode: ScalingMode = 'shape';
|
|
859
|
+
|
|
860
|
+
/**
|
|
861
|
+
* What world-space forces and speed limits are multiplied by this step:
|
|
862
|
+
* the mean of the emitter's world scale axes in `'hierarchy'` mode, 1
|
|
863
|
+
* otherwise. Updated before every simulation step.
|
|
864
|
+
*/
|
|
865
|
+
motionScale = 1;
|
|
866
|
+
|
|
736
867
|
/**
|
|
737
868
|
* Whether each particle starts turned along the direction its shape sends
|
|
738
869
|
* it (Unity's Align To Direction): a mesh particle's +Z is pointed along
|
|
@@ -880,10 +1011,14 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
880
1011
|
applyFog: parameters.applyFog ?? true,
|
|
881
1012
|
lighting: ParticleSystem.validLighting(parameters.lighting),
|
|
882
1013
|
rim: normalizeMeshRim(parameters.rim),
|
|
1014
|
+
uvScroll: normalizeUVScroll(parameters.uvScroll),
|
|
1015
|
+
gradientMap: parameters.gradientMap ?? null,
|
|
1016
|
+
erosion: normalizeErosion(parameters.erosion),
|
|
883
1017
|
};
|
|
884
1018
|
this.flip = ParticleSystem.nonZeroFlip(parameters.flip);
|
|
885
1019
|
this.playbackSpeed = parameters.playbackSpeed ?? 1;
|
|
886
1020
|
this.cullingMode = parameters.cullingMode ?? 'always';
|
|
1021
|
+
this.scalingMode = parameters.scalingMode === 'hierarchy' ? 'hierarchy' : 'shape';
|
|
887
1022
|
this.alignToDirection = parameters.alignToDirection === true;
|
|
888
1023
|
this.seed = parameters.seed;
|
|
889
1024
|
if (this.rendererSettings.renderMode === RenderMode.Mesh && !this.rendererSettings.instancingNormals) {
|
|
@@ -1062,6 +1197,12 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1062
1197
|
this.qualityFactor = Math.max(0.1, Math.min(1, qualityFactor));
|
|
1063
1198
|
}
|
|
1064
1199
|
|
|
1200
|
+
/** Quality factor times {@link emissionMultiplier}: what emission counts are scaled by. */
|
|
1201
|
+
private emissionFactor(): number {
|
|
1202
|
+
const multiplier = this.emissionMultiplier;
|
|
1203
|
+
return multiplier >= 0 ? this.qualityFactor * multiplier : this.qualityFactor;
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1065
1206
|
/**
|
|
1066
1207
|
* Appends one particle to the pool, giving it the next row of the store.
|
|
1067
1208
|
*
|
|
@@ -1212,7 +1353,12 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1212
1353
|
particle.position.applyMatrix4(matrix);
|
|
1213
1354
|
particle.startSize.multiply(scale).abs();
|
|
1214
1355
|
particle.size.copy(particle.startSize);
|
|
1215
|
-
|
|
1356
|
+
if (this.scalingMode === 'hierarchy') {
|
|
1357
|
+
// Scaled per axis by the emitter's world scale, then turned: the speed scales with it.
|
|
1358
|
+
particle.velocity.multiply(this.hierarchyScale).applyQuaternion(tempQ);
|
|
1359
|
+
} else {
|
|
1360
|
+
particle.velocity.multiply(scale).applyMatrix3(this.normalMatrix);
|
|
1361
|
+
}
|
|
1216
1362
|
if (particle.rotation && particle.rotation instanceof Quaternion) {
|
|
1217
1363
|
particle.rotation.multiplyQuaternions(tempQ, particle.rotation);
|
|
1218
1364
|
}
|
|
@@ -1514,6 +1660,63 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1514
1660
|
* frame behind the display.
|
|
1515
1661
|
*/
|
|
1516
1662
|
getWorldBounds(min: Vector3, max: Vector3): boolean {
|
|
1663
|
+
if (!this.measureWorldBounds(min, max)) {
|
|
1664
|
+
return false;
|
|
1665
|
+
}
|
|
1666
|
+
if (this.rendererSettings.renderMode === RenderMode.Beam) {
|
|
1667
|
+
// The quads reach the beam's end, which moves on its own. Without
|
|
1668
|
+
// one a beam draws nothing beyond its particles.
|
|
1669
|
+
const end = this.beamEndScratch;
|
|
1670
|
+
if (!this.getBeamEnd(end)) {
|
|
1671
|
+
return true;
|
|
1672
|
+
}
|
|
1673
|
+
const pad = this.boundsInWorld ? this.boundsPadding : this.boundsPadding * this.emitterMaxScale();
|
|
1674
|
+
min.set(Math.min(min.x, end.x - pad), Math.min(min.y, end.y - pad), Math.min(min.z, end.z - pad));
|
|
1675
|
+
max.set(Math.max(max.x, end.x + pad), Math.max(max.y, end.y + pad), Math.max(max.z, end.z + pad));
|
|
1676
|
+
}
|
|
1677
|
+
return true;
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
/**
|
|
1681
|
+
* Where a {@link RenderMode.Beam} system's quads end, see {@link BeamEnd}.
|
|
1682
|
+
* Unset (the default), a beam has no length and draws nothing. Not saved
|
|
1683
|
+
* in the effect JSON: it is where this instance is aimed at runtime.
|
|
1684
|
+
*/
|
|
1685
|
+
beamEnd?: BeamEnd;
|
|
1686
|
+
|
|
1687
|
+
private readonly beamEndScratch = new Vector3();
|
|
1688
|
+
|
|
1689
|
+
/** The world position of {@link beamEnd} into `out`; false when it is unset. */
|
|
1690
|
+
getBeamEnd(out: Vector3): boolean {
|
|
1691
|
+
const end = this.beamEnd;
|
|
1692
|
+
if (end === undefined) {
|
|
1693
|
+
return false;
|
|
1694
|
+
}
|
|
1695
|
+
const node = end as {getAbsolutePosition?: () => {x: number; y: number; z: number}};
|
|
1696
|
+
if (typeof node.getAbsolutePosition === 'function') {
|
|
1697
|
+
const position = node.getAbsolutePosition();
|
|
1698
|
+
out.set(position.x, position.y, position.z);
|
|
1699
|
+
} else {
|
|
1700
|
+
// Any point: quarks' Vector3, or a Babylon one passed from plain JavaScript.
|
|
1701
|
+
const point = end as {x: number; y: number; z: number};
|
|
1702
|
+
out.set(point.x, point.y, point.z);
|
|
1703
|
+
}
|
|
1704
|
+
return true;
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
/** The largest axis of the emitter's world scale. */
|
|
1708
|
+
private emitterMaxScale(): number {
|
|
1709
|
+
const e = this.emitter.matrixWorld.elements;
|
|
1710
|
+
return Math.sqrt(
|
|
1711
|
+
Math.max(
|
|
1712
|
+
e[0] * e[0] + e[1] * e[1] + e[2] * e[2],
|
|
1713
|
+
e[4] * e[4] + e[5] * e[5] + e[6] * e[6],
|
|
1714
|
+
e[8] * e[8] + e[9] * e[9] + e[10] * e[10]
|
|
1715
|
+
)
|
|
1716
|
+
);
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
private measureWorldBounds(min: Vector3, max: Vector3): boolean {
|
|
1517
1720
|
if (!this.boundsValid) {
|
|
1518
1721
|
return false;
|
|
1519
1722
|
}
|
|
@@ -1689,6 +1892,7 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1689
1892
|
}
|
|
1690
1893
|
|
|
1691
1894
|
private stepOnce(delta: number): void {
|
|
1895
|
+
this.motionScale = this.measureMotionScale();
|
|
1692
1896
|
if (!this.onlyUsedByOther) {
|
|
1693
1897
|
this.emit(delta, this.emissionState, this.emitter.matrixWorld);
|
|
1694
1898
|
}
|
|
@@ -2060,8 +2264,9 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
2060
2264
|
return;
|
|
2061
2265
|
}
|
|
2062
2266
|
const rawCount = burst.count.genValue(this.memory, this.time);
|
|
2063
|
-
const
|
|
2064
|
-
|
|
2267
|
+
const factor = this.emissionFactor();
|
|
2268
|
+
// Rounded at random, so a fraction is not always lost: three at 1.5 is four or five, on average 4.5.
|
|
2269
|
+
const count = Math.abs(factor - 1) < 0.001 ? rawCount : Math.floor(rawCount * factor + random());
|
|
2065
2270
|
emissionState.isBursting = true;
|
|
2066
2271
|
emissionState.burstParticleCount = count;
|
|
2067
2272
|
this.spawn(count, emissionState, emitterMatrix);
|
|
@@ -2126,6 +2331,12 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
2126
2331
|
}
|
|
2127
2332
|
|
|
2128
2333
|
emit(delta: number, emissionState: EmissionState, emitterMatrix: Matrix4) {
|
|
2334
|
+
// Ended before this step (endEmit, or a past step's end of duration):
|
|
2335
|
+
// no more bursts either. A looping system would otherwise keep firing
|
|
2336
|
+
// them every loop after endEmit and never finish. Only the system's
|
|
2337
|
+
// own emission: a sub emitter's bursts belong to its parent particles,
|
|
2338
|
+
// which still go off as they die after the effect was told to end.
|
|
2339
|
+
const endedBefore = this.emitEnded && emissionState === this.emissionState;
|
|
2129
2340
|
if (emissionState.time > this.duration) {
|
|
2130
2341
|
if (this.looping) {
|
|
2131
2342
|
emissionState.time -= this.duration;
|
|
@@ -2149,9 +2360,9 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
2149
2360
|
this.normalMatrix.getNormalMatrix(emitterMatrix);
|
|
2150
2361
|
}
|
|
2151
2362
|
const emissionBurstCount = this.emissionBursts.length;
|
|
2152
|
-
const qualityFactor = this.
|
|
2363
|
+
const qualityFactor = this.emissionFactor();
|
|
2153
2364
|
|
|
2154
|
-
if (emissionBurstCount > 0) {
|
|
2365
|
+
if (emissionBurstCount > 0 && !endedBefore) {
|
|
2155
2366
|
this.emitBursts(emissionState, emitterMatrix);
|
|
2156
2367
|
}
|
|
2157
2368
|
|
|
@@ -2265,6 +2476,7 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
2265
2476
|
...(this.applyFog ? {} : {applyFog: false}),
|
|
2266
2477
|
...(this.playbackSpeed !== 1 ? {playbackSpeed: this.playbackSpeed} : {}),
|
|
2267
2478
|
...(this.cullingMode !== 'always' ? {cullingMode: this.cullingMode} : {}),
|
|
2479
|
+
...(this.scalingMode !== 'shape' ? {scalingMode: this.scalingMode} : {}),
|
|
2268
2480
|
...(this.seedValue !== undefined ? {seed: this.seedValue} : {}),
|
|
2269
2481
|
...(this.sortMode !== 'none' ? {sortMode: this.sortMode} : {}),
|
|
2270
2482
|
...(this.alignToDirection ? {alignToDirection: true} : {}),
|
|
@@ -2325,6 +2537,10 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
2325
2537
|
typeof materialTextureRef === 'string'
|
|
2326
2538
|
? (meta.textures?.[materialTextureRef] ?? null)
|
|
2327
2539
|
: (materialTextureRef ?? null);
|
|
2540
|
+
// Texture effects reference textures by uuid in a serialised meta, or hold them once a loader resolved them.
|
|
2541
|
+
const resolveTexture = (ref: unknown): BaseTexture | null =>
|
|
2542
|
+
typeof ref === 'string' ? (meta.textures?.[ref] ?? null) : ((ref as BaseTexture | null) ?? null);
|
|
2543
|
+
const erosionMeta = materialMeta?.erosion;
|
|
2328
2544
|
const resolvedGeometryEntry =
|
|
2329
2545
|
typeof json.instancingGeometry === 'string'
|
|
2330
2546
|
? meta.geometries?.[json.instancingGeometry]
|
|
@@ -2372,6 +2588,7 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
2372
2588
|
applyFog: json.applyFog !== false,
|
|
2373
2589
|
playbackSpeed: typeof json.playbackSpeed === 'number' ? json.playbackSpeed : undefined,
|
|
2374
2590
|
cullingMode: ParticleSystem.validCullingMode(json.cullingMode),
|
|
2591
|
+
scalingMode: json.scalingMode === 'hierarchy' ? 'hierarchy' : 'shape',
|
|
2375
2592
|
seed: typeof json.seed === 'number' ? json.seed : undefined,
|
|
2376
2593
|
sortMode: ParticleSystem.validSortMode(json.sortMode),
|
|
2377
2594
|
alignToDirection: json.alignToDirection === true,
|
|
@@ -2401,6 +2618,12 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
2401
2618
|
flip: json.flip ? new Vector2(json.flip[0] ?? 0, json.flip[1] ?? 0) : undefined,
|
|
2402
2619
|
lighting: materialMeta?.lighting,
|
|
2403
2620
|
rim: materialMeta?.rim,
|
|
2621
|
+
uvScroll: materialMeta?.uvScroll,
|
|
2622
|
+
gradientMap: resolveTexture(materialMeta?.gradientMap),
|
|
2623
|
+
erosion:
|
|
2624
|
+
erosionMeta && typeof erosionMeta === 'object'
|
|
2625
|
+
? {...erosionMeta, mask: resolveTexture(erosionMeta.mask)}
|
|
2626
|
+
: undefined,
|
|
2404
2627
|
});
|
|
2405
2628
|
ps.behaviors = (json.behaviors ?? [])
|
|
2406
2629
|
.map((behaviorJson) => {
|
|
@@ -2476,6 +2699,16 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
2476
2699
|
meta.textures[reflectionAtlasUUID] = reflectionAtlas;
|
|
2477
2700
|
}
|
|
2478
2701
|
|
|
2702
|
+
const textureRef = (value: BaseTexture | null | undefined): string | undefined => {
|
|
2703
|
+
if (!(value instanceof Texture)) {
|
|
2704
|
+
return undefined;
|
|
2705
|
+
}
|
|
2706
|
+
const uuid = ParticleSystem.nextSerializationId('quarks_texture');
|
|
2707
|
+
meta.textures[uuid] = value;
|
|
2708
|
+
return uuid;
|
|
2709
|
+
};
|
|
2710
|
+
const erosion = this.erosion;
|
|
2711
|
+
|
|
2479
2712
|
const materialUUID = ParticleSystem.nextSerializationId('quarks_material');
|
|
2480
2713
|
meta.materials[materialUUID] = {
|
|
2481
2714
|
uuid: materialUUID,
|
|
@@ -2491,6 +2724,19 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
2491
2724
|
// Written only when not the default, so existing effects serialise as before.
|
|
2492
2725
|
...(this.lighting !== 'fixed' ? {lighting: this.lighting} : {}),
|
|
2493
2726
|
...(this.rim ? {rim: {...this.rim, color: [...this.rim.color]}} : {}),
|
|
2727
|
+
...(this.uvScroll ? {uvScroll: [...this.uvScroll]} : {}),
|
|
2728
|
+
...(this.gradientMap ? {gradientMap: textureRef(this.gradientMap)} : {}),
|
|
2729
|
+
...(erosion
|
|
2730
|
+
? {
|
|
2731
|
+
erosion: {
|
|
2732
|
+
mask: textureRef(erosion.mask),
|
|
2733
|
+
softness: erosion.softness,
|
|
2734
|
+
edgeWidth: erosion.edgeWidth,
|
|
2735
|
+
edgeColor: [...erosion.edgeColor],
|
|
2736
|
+
maskScroll: [...erosion.maskScroll],
|
|
2737
|
+
},
|
|
2738
|
+
}
|
|
2739
|
+
: {}),
|
|
2494
2740
|
sourceMaterial: this.materialRef ?? undefined,
|
|
2495
2741
|
};
|
|
2496
2742
|
return materialUUID;
|
|
@@ -2638,6 +2884,7 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
2638
2884
|
applyFog: this.applyFog,
|
|
2639
2885
|
playbackSpeed: this.playbackSpeed,
|
|
2640
2886
|
cullingMode: this.cullingMode,
|
|
2887
|
+
scalingMode: this.scalingMode,
|
|
2641
2888
|
seed: this.seedValue,
|
|
2642
2889
|
sortMode: this.sortMode,
|
|
2643
2890
|
alignToDirection: this.alignToDirection,
|
|
@@ -2665,6 +2912,9 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
2665
2912
|
flip: this.flip,
|
|
2666
2913
|
lighting: this.lighting,
|
|
2667
2914
|
rim: this.rim,
|
|
2915
|
+
uvScroll: this.uvScroll,
|
|
2916
|
+
gradientMap: this.gradientMap,
|
|
2917
|
+
erosion: this.erosion,
|
|
2668
2918
|
scene: this.emitter.getScene(),
|
|
2669
2919
|
});
|
|
2670
2920
|
// A sub emitter behavior transforms by its parent system's emitter;
|
package/src/QuarksLoader.ts
CHANGED
|
@@ -824,6 +824,20 @@ export class QuarksLoader {
|
|
|
824
824
|
if (matDef.rim && typeof matDef.rim === 'object') {
|
|
825
825
|
matInfo.rim = matDef.rim;
|
|
826
826
|
}
|
|
827
|
+
// Texture effects (QuarksMaterial): uvScroll [u, v], gradientMap texture uuid,
|
|
828
|
+
// erosion {mask (texture uuid), softness, edgeWidth, edgeColor, maskScroll}.
|
|
829
|
+
if (Array.isArray(matDef.uvScroll)) {
|
|
830
|
+
matInfo.uvScroll = matDef.uvScroll;
|
|
831
|
+
}
|
|
832
|
+
if (matDef.gradientMap) {
|
|
833
|
+
matInfo.gradientMap = meta.textures[matDef.gradientMap] || null;
|
|
834
|
+
}
|
|
835
|
+
if (matDef.erosion && typeof matDef.erosion === 'object') {
|
|
836
|
+
matInfo.erosion = {
|
|
837
|
+
...matDef.erosion,
|
|
838
|
+
mask: matDef.erosion.mask ? meta.textures[matDef.erosion.mask] || null : null,
|
|
839
|
+
};
|
|
840
|
+
}
|
|
827
841
|
|
|
828
842
|
// QuarksMaterial (what ParticleSystem.ensureMaterialMeta emits) writes `alphaMode`
|
|
829
843
|
// directly as a Babylon alpha-mode constant; three.js materials (MeshBasicMaterial
|
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();
|