babylon.quarks 0.28.0 → 0.29.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 +160 -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.cts +602 -190
- package/dist/types/index.d.ts +602 -190
- package/package.json +1 -1
- package/src/BatchedRenderer.ts +13 -1
- package/src/EffectPool.ts +5 -4
- package/src/ParticleEmitter.ts +89 -24
- package/src/ParticleSystem.ts +213 -30
- package/src/QuarksLoader.ts +9 -0
- package/src/QuarksSerializer.ts +1 -1
- package/src/QuarksUtil.ts +24 -2
- package/src/SpriteBatch.ts +3 -0
- package/src/TrailBatch.ts +8 -1
- package/src/VFXBatch.ts +112 -0
- package/src/batchKey.ts +2 -0
- package/src/batchSettings.ts +2 -0
- package/src/index.ts +2 -0
- package/src/shaderProgram.ts +103 -0
- package/src/shaderVariants.ts +102 -5
- package/src/shaders/chunks/blend.ts +15 -7
- package/src/shaders/chunks/imageProcessing.ts +143 -0
- package/src/shaders/particle_frag.glsl.ts +2 -0
- package/src/shaders/particle_frag.wgsl.ts +2 -0
- package/src/shaders/particle_physics_frag.glsl.ts +2 -0
- package/src/shaders/particle_physics_frag.wgsl.ts +2 -0
- package/src/shaders/trail_frag.glsl.ts +2 -0
- package/src/shaders/trail_frag.wgsl.ts +2 -0
- package/src/system/EmissionController.ts +21 -16
- package/src/system/ParticleSpawner.ts +96 -19
- package/src/system/integrate.ts +61 -4
- package/src/system/normalize.ts +28 -1
- package/src/system/settingsSchema.ts +25 -7
- package/src/system/types.ts +83 -14
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The scene's image processing for the particle shaders, in both shader languages.
|
|
3
|
+
*
|
|
4
|
+
* Babylon's own includes do the work (`imageProcessingDeclaration`, `helperFunctions`,
|
|
5
|
+
* `imageProcessingFunctions`), so the particles are tone mapped, graded and vignetted exactly as
|
|
6
|
+
* the scene's standard and PBR materials and its built-in particle systems are. The defines come
|
|
7
|
+
* from `ImageProcessingConfiguration.prepareDefines`, see `imageProcessingDefines` in
|
|
8
|
+
* `shaderProgram.ts`:
|
|
9
|
+
*
|
|
10
|
+
* - `IMAGEPROCESSING`: the material applies the configuration. The colour, authored in gamma
|
|
11
|
+
* space, is brought to linear space and `applyImageProcessing` returns it to gamma space with
|
|
12
|
+
* the configuration's effects on the way.
|
|
13
|
+
* - `IMAGEPROCESSINGPOSTPROCESS`: a post process applies it later, to the frame; the material only
|
|
14
|
+
* leaves its colour in the linear space that post process expects.
|
|
15
|
+
*
|
|
16
|
+
* The step runs on the straight colour, after the fog and the soft fade and before the blend
|
|
17
|
+
* resolve (`blend.ts`), so the resolve's meaning of alpha survives it: a premultiplied colour is
|
|
18
|
+
* the processed colour times its coverage. Particles that multiply the frame (`BLEND_MULTIPLY`)
|
|
19
|
+
* are a factor, not light, so `shaderProgram` compiles no in-material processing for them, and
|
|
20
|
+
* under a post process their resolved factor is brought to linear space after the resolve: white,
|
|
21
|
+
* which leaves the frame as it is, stays white.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import {ShaderStore} from '@babylonjs/core/Engines/shaderStore';
|
|
25
|
+
import '@babylonjs/core/Shaders/ShadersInclude/helperFunctions';
|
|
26
|
+
import '@babylonjs/core/Shaders/ShadersInclude/imageProcessingDeclaration';
|
|
27
|
+
import '@babylonjs/core/Shaders/ShadersInclude/imageProcessingFunctions';
|
|
28
|
+
import '@babylonjs/core/ShadersWGSL/ShadersInclude/helperFunctions';
|
|
29
|
+
import '@babylonjs/core/ShadersWGSL/ShadersInclude/imageProcessingDeclaration';
|
|
30
|
+
import '@babylonjs/core/ShadersWGSL/ShadersInclude/imageProcessingFunctions';
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Babylon's WGSL `imageProcessingFunctions` samples a 2D colour grading texture (a LUT laid out as
|
|
34
|
+
* slices) with a `textureSample` overload WGSL does not have, so it does not compile; its GLSL
|
|
35
|
+
* reads the slices through a `sampleTexture3D` polyfill. This is that polyfill in WGSL, and the
|
|
36
|
+
* include with the call routed through it, registered as `quarksImageProcessingFunctions`. When a
|
|
37
|
+
* Babylon release fixes the call, the include is registered as it comes.
|
|
38
|
+
*/
|
|
39
|
+
const SAMPLE_TEXTURE_3D_WGSL = /* wgsl */ `
|
|
40
|
+
#if defined(COLORGRADING) && !defined(COLORGRADING3D)
|
|
41
|
+
fn quarksSampleTexture3D(colorTransform: texture_2d<f32>, colorTransformSampler: sampler, colorIn: vec3f, sampler3dSetting: vec2f) -> vec3f {
|
|
42
|
+
let sliceSize = 2.0 * sampler3dSetting.x;
|
|
43
|
+
#ifdef SAMPLER3DGREENDEPTH
|
|
44
|
+
let sliceContinuous = (colorIn.g - sampler3dSetting.x) * sampler3dSetting.y;
|
|
45
|
+
var sliceUV = colorIn.rb;
|
|
46
|
+
#else
|
|
47
|
+
let sliceContinuous = (colorIn.b - sampler3dSetting.x) * sampler3dSetting.y;
|
|
48
|
+
var sliceUV = colorIn.rg;
|
|
49
|
+
#endif
|
|
50
|
+
let sliceInteger = floor(sliceContinuous);
|
|
51
|
+
let sliceFraction = sliceContinuous - sliceInteger;
|
|
52
|
+
sliceUV.x = sliceUV.x * sliceSize + sliceInteger * sliceSize;
|
|
53
|
+
let slice0Color = textureSample(colorTransform, colorTransformSampler, clamp(sliceUV, vec2f(0.0), vec2f(1.0)));
|
|
54
|
+
let slice1Color = textureSample(colorTransform, colorTransformSampler, clamp(sliceUV + vec2f(sliceSize, 0.0), vec2f(0.0), vec2f(1.0)));
|
|
55
|
+
let result = mix(slice0Color.rgb, slice1Color.rgb, sliceFraction);
|
|
56
|
+
#ifdef SAMPLER3DBGRMAP
|
|
57
|
+
return result.rgb;
|
|
58
|
+
#else
|
|
59
|
+
return result.bgr;
|
|
60
|
+
#endif
|
|
61
|
+
}
|
|
62
|
+
#endif
|
|
63
|
+
`;
|
|
64
|
+
|
|
65
|
+
const BROKEN_2D_SAMPLE =
|
|
66
|
+
'textureSample(txColorTransform,txColorTransformSampler,colorTransformInput,uniforms.colorTransformSettings.yz).rgb';
|
|
67
|
+
|
|
68
|
+
const babylonFunctionsWGSL = ShaderStore.IncludesShadersStoreWGSL['imageProcessingFunctions'];
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Whether Babylon's WGSL include still has the broken call, and so is registered patched. The
|
|
72
|
+
* tests pin this for the Babylon version the library is built against, and the shader validation
|
|
73
|
+
* compiles 2D colour grading in WGSL, so a Babylon release that changes the include is noticed.
|
|
74
|
+
* @internal
|
|
75
|
+
*/
|
|
76
|
+
export const PATCHES_IMAGE_PROCESSING_WGSL = babylonFunctionsWGSL.includes(BROKEN_2D_SAMPLE);
|
|
77
|
+
|
|
78
|
+
ShaderStore.IncludesShadersStoreWGSL['quarksImageProcessingFunctions'] = PATCHES_IMAGE_PROCESSING_WGSL
|
|
79
|
+
? SAMPLE_TEXTURE_3D_WGSL +
|
|
80
|
+
babylonFunctionsWGSL.replace(
|
|
81
|
+
BROKEN_2D_SAMPLE,
|
|
82
|
+
'quarksSampleTexture3D(txColorTransform,txColorTransformSampler,colorTransformInput,uniforms.colorTransformSettings.yz)'
|
|
83
|
+
)
|
|
84
|
+
: babylonFunctionsWGSL;
|
|
85
|
+
|
|
86
|
+
/** Fragment-side declaration, GLSL. */
|
|
87
|
+
export const imageProcessingDeclarationGLSL = /* glsl */ `
|
|
88
|
+
#if defined(IMAGEPROCESSING) || defined(IMAGEPROCESSINGPOSTPROCESS)
|
|
89
|
+
#include<imageProcessingDeclaration>
|
|
90
|
+
#include<helperFunctions>
|
|
91
|
+
#include<imageProcessingFunctions>
|
|
92
|
+
#endif
|
|
93
|
+
`;
|
|
94
|
+
|
|
95
|
+
/** Processes the straight vec4 lvalue `color`, before the blend resolve, GLSL. */
|
|
96
|
+
export function imageProcessingFragmentGLSL(color: string): string {
|
|
97
|
+
return /* glsl */ `
|
|
98
|
+
#if defined(IMAGEPROCESSINGPOSTPROCESS) && !defined(BLEND_MULTIPLY)
|
|
99
|
+
${color}.rgb = toLinearSpace(${color}.rgb);
|
|
100
|
+
#elif defined(IMAGEPROCESSING)
|
|
101
|
+
${color}.rgb = toLinearSpace(${color}.rgb);
|
|
102
|
+
${color} = applyImageProcessing(${color});
|
|
103
|
+
#endif
|
|
104
|
+
`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Brings a resolved multiply factor in `color` to linear space for a post process, GLSL. */
|
|
108
|
+
export function imageProcessingResolvedGLSL(color: string): string {
|
|
109
|
+
return /* glsl */ `
|
|
110
|
+
#if defined(IMAGEPROCESSINGPOSTPROCESS) && defined(BLEND_MULTIPLY)
|
|
111
|
+
${color}.rgb = toLinearSpace(${color}.rgb);
|
|
112
|
+
#endif
|
|
113
|
+
`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Fragment-side declaration, WGSL. */
|
|
117
|
+
export const imageProcessingDeclarationWGSL = /* wgsl */ `
|
|
118
|
+
#if defined(IMAGEPROCESSING) || defined(IMAGEPROCESSINGPOSTPROCESS)
|
|
119
|
+
#include<imageProcessingDeclaration>
|
|
120
|
+
#include<helperFunctions>
|
|
121
|
+
#include<quarksImageProcessingFunctions>
|
|
122
|
+
#endif
|
|
123
|
+
`;
|
|
124
|
+
|
|
125
|
+
/** Processes the straight `var` vec4 `color`, before the blend resolve, WGSL. */
|
|
126
|
+
export function imageProcessingFragmentWGSL(color: string): string {
|
|
127
|
+
return /* wgsl */ `
|
|
128
|
+
#if defined(IMAGEPROCESSINGPOSTPROCESS) && !defined(BLEND_MULTIPLY)
|
|
129
|
+
${color} = vec4f(toLinearSpaceVec3(${color}.rgb), ${color}.a);
|
|
130
|
+
#elif defined(IMAGEPROCESSING)
|
|
131
|
+
${color} = applyImageProcessing(vec4f(toLinearSpaceVec3(${color}.rgb), ${color}.a));
|
|
132
|
+
#endif
|
|
133
|
+
`;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Brings a resolved multiply factor in `color` to linear space for a post process, WGSL. */
|
|
137
|
+
export function imageProcessingResolvedWGSL(color: string): string {
|
|
138
|
+
return /* wgsl */ `
|
|
139
|
+
#if defined(IMAGEPROCESSINGPOSTPROCESS) && defined(BLEND_MULTIPLY)
|
|
140
|
+
${color} = vec4f(toLinearSpaceVec3(${color}.rgb), ${color}.a);
|
|
141
|
+
#endif
|
|
142
|
+
`;
|
|
143
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {blendFragmentGLSL} from './chunks/blend';
|
|
2
2
|
import {fogFragmentDeclarationGLSL, fogFragmentGLSL} from './chunks/fog';
|
|
3
|
+
import {imageProcessingDeclarationGLSL} from './chunks/imageProcessing';
|
|
3
4
|
import {softFragmentDeclarationGLSL, softFragmentGLSL} from './chunks/soft';
|
|
4
5
|
import {surfaceColorGLSL, surfaceFragmentDeclarationGLSL} from './chunks/surface';
|
|
5
6
|
|
|
@@ -25,6 +26,7 @@ uniform float alphaTest;
|
|
|
25
26
|
#endif
|
|
26
27
|
|
|
27
28
|
${fogFragmentDeclarationGLSL}
|
|
29
|
+
${imageProcessingDeclarationGLSL}
|
|
28
30
|
${softFragmentDeclarationGLSL}
|
|
29
31
|
${surfaceFragmentDeclarationGLSL}
|
|
30
32
|
void main() {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {blendFragmentWGSL} from './chunks/blend';
|
|
2
2
|
import {fogFragmentDeclarationWGSL, fogFragmentWGSL} from './chunks/fog';
|
|
3
|
+
import {imageProcessingDeclarationWGSL} from './chunks/imageProcessing';
|
|
3
4
|
import {softFragmentDeclarationWGSL, softFragmentWGSL} from './chunks/soft';
|
|
4
5
|
import {surfaceColorWGSL, surfaceFragmentDeclarationWGSL} from './chunks/surface';
|
|
5
6
|
|
|
@@ -26,6 +27,7 @@ uniform alphaTest: f32;
|
|
|
26
27
|
#endif
|
|
27
28
|
|
|
28
29
|
${fogFragmentDeclarationWGSL}
|
|
30
|
+
${imageProcessingDeclarationWGSL}
|
|
29
31
|
${softFragmentDeclarationWGSL}
|
|
30
32
|
${surfaceFragmentDeclarationWGSL}
|
|
31
33
|
@fragment
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {blendFragmentGLSL} from './chunks/blend';
|
|
2
2
|
import {fogFragmentDeclarationGLSL, fogFragmentGLSL} from './chunks/fog';
|
|
3
|
+
import {imageProcessingDeclarationGLSL} from './chunks/imageProcessing';
|
|
3
4
|
import {softFragmentDeclarationGLSL, softFragmentGLSL} from './chunks/soft';
|
|
4
5
|
import {surfaceColorGLSL, surfaceFragmentDeclarationGLSL} from './chunks/surface';
|
|
5
6
|
|
|
@@ -87,6 +88,7 @@ uniform float alphaTest;
|
|
|
87
88
|
#endif
|
|
88
89
|
|
|
89
90
|
${fogFragmentDeclarationGLSL}
|
|
91
|
+
${imageProcessingDeclarationGLSL}
|
|
90
92
|
${softFragmentDeclarationGLSL}
|
|
91
93
|
${surfaceFragmentDeclarationGLSL}
|
|
92
94
|
void main() {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {blendFragmentWGSL} from './chunks/blend';
|
|
2
2
|
import {fogFragmentDeclarationWGSL, fogFragmentWGSL} from './chunks/fog';
|
|
3
|
+
import {imageProcessingDeclarationWGSL} from './chunks/imageProcessing';
|
|
3
4
|
import {softFragmentDeclarationWGSL, softFragmentWGSL} from './chunks/soft';
|
|
4
5
|
import {surfaceColorWGSL, surfaceFragmentDeclarationWGSL} from './chunks/surface';
|
|
5
6
|
|
|
@@ -86,6 +87,7 @@ uniform alphaTest: f32;
|
|
|
86
87
|
#endif
|
|
87
88
|
|
|
88
89
|
${fogFragmentDeclarationWGSL}
|
|
90
|
+
${imageProcessingDeclarationWGSL}
|
|
89
91
|
${softFragmentDeclarationWGSL}
|
|
90
92
|
${surfaceFragmentDeclarationWGSL}
|
|
91
93
|
@fragment
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {blendFragmentGLSL} from './chunks/blend';
|
|
2
2
|
import {fogFragmentDeclarationGLSL, fogFragmentGLSL} from './chunks/fog';
|
|
3
|
+
import {imageProcessingDeclarationGLSL} from './chunks/imageProcessing';
|
|
3
4
|
import {softFragmentDeclarationGLSL, softFragmentGLSL} from './chunks/soft';
|
|
4
5
|
import {surfaceColorGLSL, surfaceFragmentDeclarationGLSL} from './chunks/surface';
|
|
5
6
|
|
|
@@ -19,6 +20,7 @@ uniform float alphaTest;
|
|
|
19
20
|
#endif
|
|
20
21
|
|
|
21
22
|
${fogFragmentDeclarationGLSL}
|
|
23
|
+
${imageProcessingDeclarationGLSL}
|
|
22
24
|
${softFragmentDeclarationGLSL}
|
|
23
25
|
${surfaceFragmentDeclarationGLSL}
|
|
24
26
|
void main() {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {blendFragmentWGSL} from './chunks/blend';
|
|
2
2
|
import {fogFragmentDeclarationWGSL, fogFragmentWGSL} from './chunks/fog';
|
|
3
|
+
import {imageProcessingDeclarationWGSL} from './chunks/imageProcessing';
|
|
3
4
|
import {softFragmentDeclarationWGSL, softFragmentWGSL} from './chunks/soft';
|
|
4
5
|
import {surfaceColorWGSL, surfaceFragmentDeclarationWGSL} from './chunks/surface';
|
|
5
6
|
|
|
@@ -20,6 +21,7 @@ uniform alphaTest: f32;
|
|
|
20
21
|
#endif
|
|
21
22
|
|
|
22
23
|
${fogFragmentDeclarationWGSL}
|
|
24
|
+
${imageProcessingDeclarationWGSL}
|
|
23
25
|
${softFragmentDeclarationWGSL}
|
|
24
26
|
${surfaceFragmentDeclarationWGSL}
|
|
25
27
|
@fragment
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
Matrix4,
|
|
6
6
|
PiecewiseBezier,
|
|
7
7
|
random,
|
|
8
|
+
RandomBetweenCurves,
|
|
8
9
|
ValueGenerator,
|
|
9
10
|
Vector3,
|
|
10
11
|
} from 'quarks.core';
|
|
@@ -65,19 +66,24 @@ export class EmissionController {
|
|
|
65
66
|
return delay;
|
|
66
67
|
}
|
|
67
68
|
|
|
68
|
-
/**
|
|
69
|
-
|
|
69
|
+
/**
|
|
70
|
+
* Whether continuous or queued emission can still spawn particles before the system is done:
|
|
71
|
+
* its own, or with `state` the emission a sub emitter runs into it for one parent particle.
|
|
72
|
+
*/
|
|
73
|
+
hasPendingEmission(state?: EmissionState): boolean {
|
|
70
74
|
const system = this.system;
|
|
71
|
-
if (system.
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
75
|
+
if (state === undefined || state === system.emissionState) {
|
|
76
|
+
if (system.isEmitEnded) {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
if (system.looping) {
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
if (system.onlyUsedByOther) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
state = system.emissionState;
|
|
79
86
|
}
|
|
80
|
-
const state = system.emissionState;
|
|
81
87
|
if (state.waitEmiting > 0) {
|
|
82
88
|
return true;
|
|
83
89
|
}
|
|
@@ -108,6 +114,10 @@ export class EmissionController {
|
|
|
108
114
|
if (rate.type !== 'function') {
|
|
109
115
|
return false;
|
|
110
116
|
}
|
|
117
|
+
if (rate instanceof RandomBetweenCurves) {
|
|
118
|
+
// Every mix lies between the two curves, so it emits later if either does.
|
|
119
|
+
return this.emitsFrom(rate.curve1, timeRatio) || this.emitsFrom(rate.curve2, timeRatio);
|
|
120
|
+
}
|
|
111
121
|
if (rate instanceof PiecewiseBezier) {
|
|
112
122
|
// A Bezier stays within its control points, so this is exact however short a pulse.
|
|
113
123
|
for (let i = 0; i < rate.functions.length; i++) {
|
|
@@ -176,11 +186,6 @@ export class EmissionController {
|
|
|
176
186
|
}
|
|
177
187
|
}
|
|
178
188
|
|
|
179
|
-
// Only spawns in world space consume the normal matrix, and computing it
|
|
180
|
-
// inverts a 4x4 — skip it for local-space systems.
|
|
181
|
-
if (system.worldSpace) {
|
|
182
|
-
this.spawner.normalMatrix.getNormalMatrix(emitterMatrix);
|
|
183
|
-
}
|
|
184
189
|
const qualityFactor = system.emissionFactor();
|
|
185
190
|
|
|
186
191
|
if (system.emissionBursts.length > 0 && !endedBefore) {
|
|
@@ -18,13 +18,14 @@ import type {ParticleSystem} from '../ParticleSystem';
|
|
|
18
18
|
import {RenderMode} from '../VFXBatch';
|
|
19
19
|
|
|
20
20
|
/** Particle backed by a `ParticleStore` row, as a system always creates. */
|
|
21
|
-
type StoreBackedParticle = Particle & {storeIndex: number};
|
|
21
|
+
type StoreBackedParticle = Particle & {storeIndex: number; setStoreIndex(index: number): void};
|
|
22
22
|
|
|
23
23
|
const UP = new Vector3(0, 0, 1);
|
|
24
24
|
const IDENTITY_QUATERNION = new Quaternion();
|
|
25
25
|
const tempQ = new Quaternion();
|
|
26
26
|
const tempV = new Vector3();
|
|
27
27
|
const tempV2 = new Vector3();
|
|
28
|
+
const tempM3 = new Matrix3();
|
|
28
29
|
|
|
29
30
|
/**
|
|
30
31
|
* Gives a system its newborn particles: grows the pool and the store they live in, rolls each
|
|
@@ -33,13 +34,64 @@ const tempV2 = new Vector3();
|
|
|
33
34
|
* spawn and when, this decides what each one starts as.
|
|
34
35
|
*/
|
|
35
36
|
export class ParticleSpawner {
|
|
37
|
+
constructor(private readonly system: ParticleSystem) {}
|
|
38
|
+
|
|
36
39
|
/**
|
|
37
|
-
* The
|
|
38
|
-
*
|
|
40
|
+
* The row a full ring buffer replaces next, the oldest. Particles of a ring buffer never die,
|
|
41
|
+
* so they stay in the order they were born: the oldest is row 0 when the system fills up, and
|
|
42
|
+
* the cursor walks on from there.
|
|
39
43
|
*/
|
|
40
|
-
|
|
44
|
+
private ringCursor = 0;
|
|
41
45
|
|
|
42
|
-
|
|
46
|
+
/** Swaps rows `a` and `b`: the particle objects and their store rows. */
|
|
47
|
+
private swapRows(a: number, b: number): void {
|
|
48
|
+
const particles = this.system.particles;
|
|
49
|
+
const first = particles[a] as StoreBackedParticle;
|
|
50
|
+
const second = particles[b] as StoreBackedParticle;
|
|
51
|
+
particles[a] = second;
|
|
52
|
+
particles[b] = first;
|
|
53
|
+
this.system.store.swapRows(a, b);
|
|
54
|
+
second.setStoreIndex(a);
|
|
55
|
+
first.setStoreIndex(b);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Reverses rows `from` to `to`, inclusive. */
|
|
59
|
+
private reverseRows(from: number, to: number): void {
|
|
60
|
+
for (; from < to; from++, to--) {
|
|
61
|
+
this.swapRows(from, to);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Moves the first `by` of the `count` live rows to the end, keeping the order of both parts. */
|
|
66
|
+
private rotateRows(by: number, count: number): void {
|
|
67
|
+
if (by <= 0 || by >= count) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
this.reverseRows(0, by - 1);
|
|
71
|
+
this.reverseRows(by, count - 1);
|
|
72
|
+
this.reverseRows(0, count - 1);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Keeps a ring buffer within `maxParticles` once it was lowered: its particles never die, so
|
|
77
|
+
* the oldest over the limit are dropped, and the rest stay in birth order from row 0. Called
|
|
78
|
+
* at the start of every step; costs nothing while the system is within its limit.
|
|
79
|
+
*/
|
|
80
|
+
fitRing(): void {
|
|
81
|
+
const system = this.system;
|
|
82
|
+
const max = system.maxParticles;
|
|
83
|
+
const count = system.particleNum;
|
|
84
|
+
if (max === undefined || count <= max || system.ringBufferMode === 'disabled') {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (system.rendererSettings.renderMode === RenderMode.Trail) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
// Oldest first from the cursor: the survivors start `count - max` rows after it.
|
|
91
|
+
this.rotateRows((this.ringCursor + count - max) % count, count);
|
|
92
|
+
system.particleNum = max;
|
|
93
|
+
this.ringCursor = 0;
|
|
94
|
+
}
|
|
43
95
|
|
|
44
96
|
/** Points every particle at the store's columns again, after any were replaced. */
|
|
45
97
|
rebind(): void {
|
|
@@ -75,6 +127,7 @@ export class ParticleSpawner {
|
|
|
75
127
|
const scale = tempV2;
|
|
76
128
|
// decompose writes tempQ, so no separate setFromRotationMatrix is needed.
|
|
77
129
|
matrix.decompose(translation, tempQ, scale);
|
|
130
|
+
const linear = tempM3.setFromMatrix4(matrix);
|
|
78
131
|
const behaviors = system.behaviors;
|
|
79
132
|
const behaviorCount = behaviors.length;
|
|
80
133
|
const renderMode = system.rendererSettings.renderMode;
|
|
@@ -89,20 +142,43 @@ export class ParticleSpawner {
|
|
|
89
142
|
const trailSettings = isTrailMode ? (system.rendererEmitterSettings as TrailSettings) : undefined;
|
|
90
143
|
const followLocalOrigin = Boolean(trailSettings?.followLocalOrigin);
|
|
91
144
|
const worldSpace = system.worldSpace;
|
|
92
|
-
|
|
145
|
+
// The emitter's matrix carries the scale the scaling mode applies (see
|
|
146
|
+
// `ParticleEmitter.matrixWorld`); 'shape' scales the shape alone, here.
|
|
147
|
+
const shapeScale = system.scalingMode === 'shape' ? system.emitter.shapeScale : undefined;
|
|
93
148
|
const particles = system.particles;
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
149
|
+
const maxParticles = system.maxParticles;
|
|
150
|
+
// A ring buffer replaces the oldest particles of a full system (see `ringBufferMode`).
|
|
151
|
+
const ring = maxParticles !== undefined && system.ringBufferMode !== 'disabled' && !isTrailMode;
|
|
152
|
+
if (maxParticles !== undefined) {
|
|
153
|
+
// Like Unity, a full system skips the particles it has no room for rather than
|
|
154
|
+
// queueing them; a ring buffer replaces at most every particle once.
|
|
155
|
+
count = ring
|
|
156
|
+
? Math.min(count, maxParticles)
|
|
157
|
+
: Math.min(count, Math.max(0, maxParticles - system.particleNum));
|
|
97
158
|
}
|
|
98
159
|
|
|
99
160
|
for (let i = 0; i < count; i++) {
|
|
100
161
|
emissionState.burstParticleIndex = i;
|
|
101
|
-
|
|
102
|
-
if (
|
|
103
|
-
this.
|
|
162
|
+
let particle: Particle;
|
|
163
|
+
if (ring && system.particleNum >= maxParticles) {
|
|
164
|
+
if (this.ringCursor >= system.particleNum) {
|
|
165
|
+
this.ringCursor = 0;
|
|
166
|
+
}
|
|
167
|
+
particle = particles[this.ringCursor];
|
|
168
|
+
this.ringCursor = (this.ringCursor + 1) % system.particleNum;
|
|
169
|
+
} else {
|
|
170
|
+
if (ring && this.ringCursor !== 0) {
|
|
171
|
+
// A ring that wrapped and was given room (a higher limit): put its oldest at
|
|
172
|
+
// row 0 again, so the newborn appended after the newest stays in birth order.
|
|
173
|
+
this.rotateRows(this.ringCursor, system.particleNum);
|
|
174
|
+
this.ringCursor = 0;
|
|
175
|
+
}
|
|
176
|
+
system.particleNum++;
|
|
177
|
+
if (particles.length < system.particleNum) {
|
|
178
|
+
this.grow(isTrailMode);
|
|
179
|
+
}
|
|
180
|
+
particle = particles[system.particleNum - 1];
|
|
104
181
|
}
|
|
105
|
-
const particle = particles[system.particleNum - 1];
|
|
106
182
|
particle.reset();
|
|
107
183
|
particle.serial = (particle.serial ?? 0) + 1;
|
|
108
184
|
particle.speedModifier = 1;
|
|
@@ -178,6 +254,9 @@ export class ParticleSpawner {
|
|
|
178
254
|
}
|
|
179
255
|
|
|
180
256
|
system.emitterShape.initialize(particle, emissionState);
|
|
257
|
+
if (shapeScale !== undefined) {
|
|
258
|
+
particle.position.multiply(shapeScale);
|
|
259
|
+
}
|
|
181
260
|
|
|
182
261
|
if (system.alignToDirection && isSpriteMode) {
|
|
183
262
|
alignParticleToDirection(particle as SpriteParticle, isMeshMode);
|
|
@@ -201,12 +280,10 @@ export class ParticleSpawner {
|
|
|
201
280
|
particle.position.applyMatrix4(matrix);
|
|
202
281
|
particle.startSize.multiply(scale).abs();
|
|
203
282
|
particle.size.copy(particle.startSize);
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
particle.velocity.multiply(scale).applyMatrix3(this.normalMatrix);
|
|
209
|
-
}
|
|
283
|
+
// Through the matrix's own axes, as the position is: scaled, turned and mirrored
|
|
284
|
+
// with it. For a sub emission that is the parent particle's frame, the same one
|
|
285
|
+
// its start size takes.
|
|
286
|
+
particle.velocity.applyMatrix3(linear);
|
|
210
287
|
if (particle.rotation && particle.rotation instanceof Quaternion) {
|
|
211
288
|
particle.rotation.multiplyQuaternions(tempQ, particle.rotation);
|
|
212
289
|
}
|
package/src/system/integrate.ts
CHANGED
|
@@ -129,6 +129,34 @@ function retire(particles: Particle[], store: ParticleStore, i: number, last: nu
|
|
|
129
129
|
return particle;
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
+
/**
|
|
133
|
+
* What a ring buffer does to a particle at the end of its life (see `RingBufferMode`): `pause`
|
|
134
|
+
* holds it frozen at its end, else it goes back to `loopStart` (a fraction of its life) once it
|
|
135
|
+
* reaches `loopEnd`.
|
|
136
|
+
*/
|
|
137
|
+
export interface RingBufferStep {
|
|
138
|
+
/** Freeze at the end of life rather than loop. */
|
|
139
|
+
pause: boolean;
|
|
140
|
+
/** Where a looping particle goes back to, a fraction of its life, 0 to 1. */
|
|
141
|
+
loopStart: number;
|
|
142
|
+
/**
|
|
143
|
+
* Where a looping particle goes back from, a fraction of its life, `loopStart` to 1. Equal to
|
|
144
|
+
* `loopStart`, the particle is held there.
|
|
145
|
+
*/
|
|
146
|
+
loopEnd: number;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** The least age a held particle keeps, as a fraction of its life: past 0, a newborn's age. */
|
|
150
|
+
const HELD_AGE = 1e-6;
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* The age a looping ring particle is held at, or Infinity when it loops: a loop range of no
|
|
154
|
+
* length holds it at that point of its life, as a pause does at the end.
|
|
155
|
+
*/
|
|
156
|
+
function holdAge(ring: RingBufferStep, life: number): number {
|
|
157
|
+
return ring.loopEnd > ring.loopStart ? Infinity : Math.max(ring.loopEnd, HELD_AGE) * life;
|
|
158
|
+
}
|
|
159
|
+
|
|
132
160
|
/**
|
|
133
161
|
* The sprite path's step after the behaviors, as one pass over the columns that does three
|
|
134
162
|
* things, none of which touches a particle object until a death. Returns the live count.
|
|
@@ -142,7 +170,8 @@ function retire(particles: Particle[], store: ParticleStore, i: number, last: nu
|
|
|
142
170
|
* Position is integrated along the velocity.
|
|
143
171
|
*
|
|
144
172
|
* A particle whose age reached its life is swapped with the last live one, rows included. The
|
|
145
|
-
* survivor moved into the slot has not been visited yet, so the slot is processed again.
|
|
173
|
+
* survivor moved into the slot has not been visited yet, so the slot is processed again. With a
|
|
174
|
+
* `ring` buffer no particle dies: it is frozen at the end of its life, or sent back into it.
|
|
146
175
|
*
|
|
147
176
|
* The bounds fall out of the same pass. Measuring every particle on every step costs about as
|
|
148
177
|
* much as the integration itself, so the full measurement runs every few steps; in between,
|
|
@@ -158,9 +187,11 @@ export function integrateSprites(
|
|
|
158
187
|
collapseColor: boolean,
|
|
159
188
|
bounds: BoundsTracker,
|
|
160
189
|
settings: BoundsSettings,
|
|
161
|
-
onDied: DeathListener | undefined
|
|
190
|
+
onDied: DeathListener | undefined,
|
|
191
|
+
ring?: RingBufferStep
|
|
162
192
|
): number {
|
|
163
193
|
const scalars = store.scalars;
|
|
194
|
+
const pause = ring !== undefined && ring.pause;
|
|
164
195
|
const stride = ParticleStore.SCALAR_STRIDE;
|
|
165
196
|
const positions = store.position;
|
|
166
197
|
const velocities = store.velocity;
|
|
@@ -198,7 +229,12 @@ export function integrateSprites(
|
|
|
198
229
|
prevColor[o4 + 3] = curColor[o4 + 3];
|
|
199
230
|
}
|
|
200
231
|
}
|
|
201
|
-
const
|
|
232
|
+
const life = scalars[scalar + ParticleStore.LIFE];
|
|
233
|
+
// Frozen until a newborn replaces it, no motion and no ageing: at the end of its life when
|
|
234
|
+
// paused, or at a loop range of no length, held a hair past 0 so Birth fires only once.
|
|
235
|
+
const hold = ring === undefined ? Infinity : pause ? life : holdAge(ring, life);
|
|
236
|
+
const frozen = age >= hold;
|
|
237
|
+
const step = frozen ? 0 : delta * scalars[scalar + ParticleStore.SPEED_MODIFIER];
|
|
202
238
|
const vx = velocities[offset];
|
|
203
239
|
const vy = velocities[offset + 1];
|
|
204
240
|
const vz = velocities[offset + 2];
|
|
@@ -219,10 +255,31 @@ export function integrateSprites(
|
|
|
219
255
|
}
|
|
220
256
|
// Speed is tracked every step: it bounds how far the particles
|
|
221
257
|
// can have moved since the last measurement.
|
|
258
|
+
if (frozen) {
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
222
261
|
maxSpeedSq = Math.max(maxSpeedSq, vx * vx + vy * vy + vz * vz);
|
|
223
262
|
age += delta;
|
|
224
263
|
scalars[scalar + ParticleStore.AGE] = age;
|
|
225
|
-
if (
|
|
264
|
+
if (ring !== undefined) {
|
|
265
|
+
if (age > hold) {
|
|
266
|
+
scalars[scalar + ParticleStore.AGE] = hold;
|
|
267
|
+
} else if (!pause && hold === Infinity && age >= ring.loopEnd * life) {
|
|
268
|
+
const loopStart = ring.loopStart * life;
|
|
269
|
+
const wrapped = loopStart + ((age - ring.loopEnd * life) % ((ring.loopEnd - ring.loopStart) * life));
|
|
270
|
+
// Never exactly 0, which is a newborn's age: Birth sub emitters would fire again.
|
|
271
|
+
scalars[scalar + ParticleStore.AGE] = wrapped > 0 ? wrapped : HELD_AGE * life;
|
|
272
|
+
// The jump back is not motion: the renderer must not extrapolate size or colour across it.
|
|
273
|
+
prevSize[offset] = curSize[offset];
|
|
274
|
+
prevSize[offset + 1] = curSize[offset + 1];
|
|
275
|
+
prevSize[offset + 2] = curSize[offset + 2];
|
|
276
|
+
const o4 = i * 4;
|
|
277
|
+
prevColor[o4] = curColor[o4];
|
|
278
|
+
prevColor[o4 + 1] = curColor[o4 + 1];
|
|
279
|
+
prevColor[o4 + 2] = curColor[o4 + 2];
|
|
280
|
+
prevColor[o4 + 3] = curColor[o4 + 3];
|
|
281
|
+
}
|
|
282
|
+
} else if (age >= life) {
|
|
226
283
|
const particle = retire(particles, store, i, liveParticleCount - 1);
|
|
227
284
|
liveParticleCount--;
|
|
228
285
|
i--;
|
package/src/system/normalize.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {ConstantValue, FunctionValueGenerator, ValueGenerator, Vector2, Vector3} from 'quarks.core';
|
|
2
2
|
import type {MeshLighting, RenderFace, SortMode} from '../batchSettings';
|
|
3
|
-
import type {BurstParameters, CullingMode} from './types';
|
|
3
|
+
import type {BurstParameters, CullingMode, RingBufferMode, ScalingMode, StopAction} from './types';
|
|
4
4
|
|
|
5
5
|
// Normalisers shared by the ParticleSystem constructor, its setters and its JSON reader, so a
|
|
6
6
|
// value read from a file and one set in code end up the same.
|
|
@@ -25,6 +25,33 @@ export function validCullingMode(mode: unknown): CullingMode {
|
|
|
25
25
|
return mode === 'pause' || mode === 'pauseAndCatchUp' ? mode : 'always';
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
/** `action` if it is one of the stop actions, else `'none'`. */
|
|
29
|
+
export function validStopAction(action: unknown): StopAction {
|
|
30
|
+
return action === 'disable' || action === 'destroy' ? action : 'none';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** `mode` if it is one of the ring buffer modes, else `'disabled'`. */
|
|
34
|
+
export function validRingBufferMode(mode: unknown): RingBufferMode {
|
|
35
|
+
return mode === 'pauseUntilReplaced' || mode === 'loopUntilReplaced' ? mode : 'disabled';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* A ring buffer loop range within 0..1, start no later than end; a missing or non-finite bound
|
|
40
|
+
* takes its default (0 or 1).
|
|
41
|
+
*/
|
|
42
|
+
export function validLoopRange(range: {x: number; y: number} | undefined): Vector2 {
|
|
43
|
+
const clamp = (value: number | undefined, fallback: number) =>
|
|
44
|
+
typeof value === 'number' && Number.isFinite(value) ? Math.min(1, Math.max(0, value)) : fallback;
|
|
45
|
+
const start = clamp(range?.x, 0);
|
|
46
|
+
const end = clamp(range?.y, 1);
|
|
47
|
+
return new Vector2(Math.min(start, end), Math.max(start, end));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** `mode` if it is one of the scaling modes, else `'local'`. */
|
|
51
|
+
export function validScalingMode(mode: unknown): ScalingMode {
|
|
52
|
+
return mode === 'hierarchy' || mode === 'shape' ? mode : 'local';
|
|
53
|
+
}
|
|
54
|
+
|
|
28
55
|
/** `count` as a whole number of particles, or undefined (no limit) when it is unset, NaN or negative. */
|
|
29
56
|
export function validMaxParticles(count: unknown): number | undefined {
|
|
30
57
|
return typeof count === 'number' && count >= 0 && count < Infinity ? Math.floor(count) : undefined;
|