babylon.quarks 0.27.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 +585 -0
- package/README.md +3 -1
- 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 +4811 -974
- package/dist/types/index.d.ts +4811 -974
- package/package.json +7 -2
- package/src/BatchedRenderer.ts +106 -79
- package/src/EffectPool.ts +96 -16
- package/src/MeshSurfaceEmitter.ts +24 -13
- package/src/ParticleEmitter.ts +119 -26
- package/src/ParticleSystem.ts +305 -55
- package/src/QuarksLoader.ts +298 -171
- package/src/QuarksPrefab.ts +301 -94
- package/src/QuarksSerializer.ts +44 -16
- package/src/QuarksUtil.ts +88 -11
- package/src/SpriteBatch.ts +24 -6
- package/src/TrailBatch.ts +20 -7
- package/src/VFXBatch.ts +182 -528
- package/src/batchKey.ts +6 -2
- package/src/batchSettings.ts +548 -0
- package/src/effectDocumentJSON.ts +261 -0
- package/src/effectResources.ts +21 -24
- package/src/envAtlas.ts +8 -2
- package/src/geometryUtil.ts +1 -1
- package/src/index.ts +41 -4
- package/src/nodeId.ts +25 -2
- package/src/shaderProgram.ts +213 -16
- package/src/shaderVariants.ts +124 -7
- package/src/shaders/chunks/blend.ts +39 -0
- package/src/shaders/chunks/fog.ts +8 -3
- package/src/shaders/chunks/imageProcessing.ts +143 -0
- package/src/shaders/chunks/soft.ts +6 -5
- package/src/shaders/chunks/surface.ts +2 -2
- package/src/shaders/particle_frag.glsl.ts +5 -1
- package/src/shaders/particle_frag.wgsl.ts +4 -0
- package/src/shaders/particle_physics_frag.glsl.ts +7 -2
- package/src/shaders/particle_physics_frag.wgsl.ts +4 -0
- package/src/shaders/trail_frag.glsl.ts +6 -1
- package/src/shaders/trail_frag.wgsl.ts +4 -0
- package/src/system/EmissionController.ts +23 -20
- package/src/system/EventDispatcher.ts +26 -10
- package/src/system/ParticleSpawner.ts +96 -19
- package/src/system/geometry.ts +22 -29
- package/src/system/integrate.ts +61 -4
- package/src/system/materialResolve.ts +56 -7
- package/src/system/normalize.ts +34 -2
- package/src/system/serialization.ts +54 -33
- package/src/system/settingsSchema.ts +57 -17
- package/src/system/types.ts +150 -42
- package/src/trailSettings.ts +9 -6
|
@@ -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
|
+
}
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* The fragment reads the scene depth behind it from `depthTexture`, turns it into view depth with
|
|
5
5
|
* the coefficients in `projParams` (see `softDepthParams`), and fades out over the distance in
|
|
6
6
|
* `softParams` as it nears the scene, as Unity's soft particles and Unreal's Depth Fade do. The
|
|
7
|
-
* fade runs
|
|
7
|
+
* fade lowers alpha only and runs after fog and before the blend resolve (`blend.ts`), so it reads
|
|
8
|
+
* as coverage in every blend mode and both languages produce the same colour.
|
|
8
9
|
*
|
|
9
10
|
* `SOFT_DEPTH_PACKED` reads a depth packed into four 8-bit channels, as a Babylon
|
|
10
11
|
* `DepthRenderer` of type `UNSIGNED_BYTE` writes it.
|
|
@@ -33,14 +34,14 @@ float quarksSceneDepth(vec4 texel) {
|
|
|
33
34
|
#endif
|
|
34
35
|
`;
|
|
35
36
|
|
|
36
|
-
/** Fades `target` as the fragment nears the scene behind it, GLSL. */
|
|
37
|
+
/** Fades the alpha of the vec4 lvalue `target` as the fragment nears the scene behind it, GLSL. */
|
|
37
38
|
export function softFragmentGLSL(target: string): string {
|
|
38
39
|
return /* glsl */ `
|
|
39
40
|
#ifdef SOFT_PARTICLES
|
|
40
41
|
{
|
|
41
42
|
vec2 softUV = 0.5 * (projPosition.xy / projPosition.w) + 0.5;
|
|
42
43
|
float sceneDepth = quarksSceneDepth(texture2D(depthTexture, softUV));
|
|
43
|
-
${target} *= clamp(softParams.y * ((sceneDepth - softParams.x) - linearDepth), 0.0, 1.0);
|
|
44
|
+
${target}.a *= clamp(softParams.y * ((sceneDepth - softParams.x) - linearDepth), 0.0, 1.0);
|
|
44
45
|
}
|
|
45
46
|
#endif
|
|
46
47
|
`;
|
|
@@ -70,14 +71,14 @@ fn quarksSceneDepth(texel: vec4f) -> f32 {
|
|
|
70
71
|
#endif
|
|
71
72
|
`;
|
|
72
73
|
|
|
73
|
-
/** Fades `target` as the fragment nears the scene behind it, WGSL. */
|
|
74
|
+
/** Fades the alpha of the `var` vec4 `target` as the fragment nears the scene behind it, WGSL. */
|
|
74
75
|
export function softFragmentWGSL(target: string): string {
|
|
75
76
|
return /* wgsl */ `
|
|
76
77
|
#ifdef SOFT_PARTICLES
|
|
77
78
|
{
|
|
78
79
|
let softUV = 0.5 * (fragmentInputs.projPosition.xy / fragmentInputs.projPosition.w) + 0.5;
|
|
79
80
|
let sceneDepth = quarksSceneDepth(textureSample(depthTexture, depthTextureSampler, softUV));
|
|
80
|
-
${target} *= clamp(uniforms.softParams.y * ((sceneDepth - uniforms.softParams.x) - fragmentInputs.linearDepth), 0.0, 1.0);
|
|
81
|
+
${target}.a *= clamp(uniforms.softParams.y * ((sceneDepth - uniforms.softParams.x) - fragmentInputs.linearDepth), 0.0, 1.0);
|
|
81
82
|
}
|
|
82
83
|
#endif
|
|
83
84
|
`;
|
|
@@ -35,8 +35,8 @@
|
|
|
35
35
|
* on the way down the shape eats away from its lowest mask values first — a
|
|
36
36
|
* noise mask dissolves it, a gradient mask wipes it. `erosionParams` is
|
|
37
37
|
* (softness, edge width); the edge band just above the cut glows in
|
|
38
|
-
* `erosionEdgeColor`. Erosion only changes alpha,
|
|
39
|
-
*
|
|
38
|
+
* `erosionEdgeColor`. Erosion only changes alpha, which the blend resolve
|
|
39
|
+
* (`blend.ts`) turns into coverage in every blend mode.
|
|
40
40
|
*/
|
|
41
41
|
|
|
42
42
|
/** Fragment-side declarations, GLSL. */
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import {blendFragmentGLSL} from './chunks/blend';
|
|
1
2
|
import {fogFragmentDeclarationGLSL, fogFragmentGLSL} from './chunks/fog';
|
|
3
|
+
import {imageProcessingDeclarationGLSL} from './chunks/imageProcessing';
|
|
2
4
|
import {softFragmentDeclarationGLSL, softFragmentGLSL} from './chunks/soft';
|
|
3
5
|
import {surfaceColorGLSL, surfaceFragmentDeclarationGLSL} from './chunks/surface';
|
|
4
6
|
|
|
@@ -24,6 +26,7 @@ uniform float alphaTest;
|
|
|
24
26
|
#endif
|
|
25
27
|
|
|
26
28
|
${fogFragmentDeclarationGLSL}
|
|
29
|
+
${imageProcessingDeclarationGLSL}
|
|
27
30
|
${softFragmentDeclarationGLSL}
|
|
28
31
|
${surfaceFragmentDeclarationGLSL}
|
|
29
32
|
void main() {
|
|
@@ -35,7 +38,8 @@ ${surfaceColorGLSL(true)}
|
|
|
35
38
|
#endif
|
|
36
39
|
|
|
37
40
|
${fogFragmentGLSL('baseColor.rgb')}
|
|
41
|
+
${softFragmentGLSL('baseColor')}
|
|
42
|
+
${blendFragmentGLSL('baseColor')}
|
|
38
43
|
gl_FragColor = baseColor;
|
|
39
|
-
${softFragmentGLSL('gl_FragColor')}
|
|
40
44
|
}
|
|
41
45
|
`;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import {blendFragmentWGSL} from './chunks/blend';
|
|
1
2
|
import {fogFragmentDeclarationWGSL, fogFragmentWGSL} from './chunks/fog';
|
|
3
|
+
import {imageProcessingDeclarationWGSL} from './chunks/imageProcessing';
|
|
2
4
|
import {softFragmentDeclarationWGSL, softFragmentWGSL} from './chunks/soft';
|
|
3
5
|
import {surfaceColorWGSL, surfaceFragmentDeclarationWGSL} from './chunks/surface';
|
|
4
6
|
|
|
@@ -25,6 +27,7 @@ uniform alphaTest: f32;
|
|
|
25
27
|
#endif
|
|
26
28
|
|
|
27
29
|
${fogFragmentDeclarationWGSL}
|
|
30
|
+
${imageProcessingDeclarationWGSL}
|
|
28
31
|
${softFragmentDeclarationWGSL}
|
|
29
32
|
${surfaceFragmentDeclarationWGSL}
|
|
30
33
|
@fragment
|
|
@@ -38,6 +41,7 @@ ${surfaceColorWGSL(true)}
|
|
|
38
41
|
|
|
39
42
|
${fogFragmentWGSL('baseColor')}
|
|
40
43
|
${softFragmentWGSL('baseColor')}
|
|
44
|
+
${blendFragmentWGSL('baseColor')}
|
|
41
45
|
fragmentOutputs.color = baseColor;
|
|
42
46
|
}
|
|
43
47
|
`;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import {blendFragmentGLSL} from './chunks/blend';
|
|
1
2
|
import {fogFragmentDeclarationGLSL, fogFragmentGLSL} from './chunks/fog';
|
|
3
|
+
import {imageProcessingDeclarationGLSL} from './chunks/imageProcessing';
|
|
2
4
|
import {softFragmentDeclarationGLSL, softFragmentGLSL} from './chunks/soft';
|
|
3
5
|
import {surfaceColorGLSL, surfaceFragmentDeclarationGLSL} from './chunks/surface';
|
|
4
6
|
|
|
@@ -86,6 +88,7 @@ uniform float alphaTest;
|
|
|
86
88
|
#endif
|
|
87
89
|
|
|
88
90
|
${fogFragmentDeclarationGLSL}
|
|
91
|
+
${imageProcessingDeclarationGLSL}
|
|
89
92
|
${softFragmentDeclarationGLSL}
|
|
90
93
|
${surfaceFragmentDeclarationGLSL}
|
|
91
94
|
void main() {
|
|
@@ -130,7 +133,9 @@ ${surfaceColorGLSL(true)}
|
|
|
130
133
|
#endif
|
|
131
134
|
|
|
132
135
|
${fogFragmentGLSL('color')}
|
|
133
|
-
|
|
134
|
-
${softFragmentGLSL('
|
|
136
|
+
vec4 finalColor = vec4(color, baseColor.a);
|
|
137
|
+
${softFragmentGLSL('finalColor')}
|
|
138
|
+
${blendFragmentGLSL('finalColor')}
|
|
139
|
+
gl_FragColor = finalColor;
|
|
135
140
|
}
|
|
136
141
|
`;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import {blendFragmentWGSL} from './chunks/blend';
|
|
1
2
|
import {fogFragmentDeclarationWGSL, fogFragmentWGSL} from './chunks/fog';
|
|
3
|
+
import {imageProcessingDeclarationWGSL} from './chunks/imageProcessing';
|
|
2
4
|
import {softFragmentDeclarationWGSL, softFragmentWGSL} from './chunks/soft';
|
|
3
5
|
import {surfaceColorWGSL, surfaceFragmentDeclarationWGSL} from './chunks/surface';
|
|
4
6
|
|
|
@@ -85,6 +87,7 @@ uniform alphaTest: f32;
|
|
|
85
87
|
#endif
|
|
86
88
|
|
|
87
89
|
${fogFragmentDeclarationWGSL}
|
|
90
|
+
${imageProcessingDeclarationWGSL}
|
|
88
91
|
${softFragmentDeclarationWGSL}
|
|
89
92
|
${surfaceFragmentDeclarationWGSL}
|
|
90
93
|
@fragment
|
|
@@ -133,6 +136,7 @@ ${surfaceColorWGSL(true)}
|
|
|
133
136
|
|
|
134
137
|
${fogFragmentWGSL('finalColor')}
|
|
135
138
|
${softFragmentWGSL('finalColor')}
|
|
139
|
+
${blendFragmentWGSL('finalColor')}
|
|
136
140
|
fragmentOutputs.color = finalColor;
|
|
137
141
|
}
|
|
138
142
|
`;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import {blendFragmentGLSL} from './chunks/blend';
|
|
1
2
|
import {fogFragmentDeclarationGLSL, fogFragmentGLSL} from './chunks/fog';
|
|
3
|
+
import {imageProcessingDeclarationGLSL} from './chunks/imageProcessing';
|
|
2
4
|
import {softFragmentDeclarationGLSL, softFragmentGLSL} from './chunks/soft';
|
|
3
5
|
import {surfaceColorGLSL, surfaceFragmentDeclarationGLSL} from './chunks/surface';
|
|
4
6
|
|
|
@@ -18,6 +20,7 @@ uniform float alphaTest;
|
|
|
18
20
|
#endif
|
|
19
21
|
|
|
20
22
|
${fogFragmentDeclarationGLSL}
|
|
23
|
+
${imageProcessingDeclarationGLSL}
|
|
21
24
|
${softFragmentDeclarationGLSL}
|
|
22
25
|
${surfaceFragmentDeclarationGLSL}
|
|
23
26
|
void main() {
|
|
@@ -29,6 +32,8 @@ ${surfaceColorGLSL(false)}
|
|
|
29
32
|
#endif
|
|
30
33
|
|
|
31
34
|
${fogFragmentGLSL('baseColor.rgb')}
|
|
35
|
+
${softFragmentGLSL('baseColor')}
|
|
36
|
+
${blendFragmentGLSL('baseColor')}
|
|
32
37
|
gl_FragColor = baseColor;
|
|
33
|
-
|
|
38
|
+
}
|
|
34
39
|
`;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import {blendFragmentWGSL} from './chunks/blend';
|
|
1
2
|
import {fogFragmentDeclarationWGSL, fogFragmentWGSL} from './chunks/fog';
|
|
3
|
+
import {imageProcessingDeclarationWGSL} from './chunks/imageProcessing';
|
|
2
4
|
import {softFragmentDeclarationWGSL, softFragmentWGSL} from './chunks/soft';
|
|
3
5
|
import {surfaceColorWGSL, surfaceFragmentDeclarationWGSL} from './chunks/surface';
|
|
4
6
|
|
|
@@ -19,6 +21,7 @@ uniform alphaTest: f32;
|
|
|
19
21
|
#endif
|
|
20
22
|
|
|
21
23
|
${fogFragmentDeclarationWGSL}
|
|
24
|
+
${imageProcessingDeclarationWGSL}
|
|
22
25
|
${softFragmentDeclarationWGSL}
|
|
23
26
|
${surfaceFragmentDeclarationWGSL}
|
|
24
27
|
@fragment
|
|
@@ -32,6 +35,7 @@ ${surfaceColorWGSL(false)}
|
|
|
32
35
|
|
|
33
36
|
${fogFragmentWGSL('baseColor')}
|
|
34
37
|
${softFragmentWGSL('baseColor')}
|
|
38
|
+
${blendFragmentWGSL('baseColor')}
|
|
35
39
|
fragmentOutputs.color = baseColor;
|
|
36
40
|
}
|
|
37
41
|
`;
|
|
@@ -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++) {
|
|
@@ -167,10 +177,8 @@ export class EmissionController {
|
|
|
167
177
|
if (emissionState.burstWaves !== undefined) {
|
|
168
178
|
emissionState.burstWaves.length = 0;
|
|
169
179
|
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
behaviors[i].reset();
|
|
173
|
-
}
|
|
180
|
+
// Behaviors carry on across the loop (a noise field keeps scrolling, sub emissions
|
|
181
|
+
// of live particles keep going); they reset only when the system restarts.
|
|
174
182
|
} else {
|
|
175
183
|
if (!system.isEmitEnded && !system.onlyUsedByOther) {
|
|
176
184
|
system.endEmit();
|
|
@@ -178,11 +186,6 @@ export class EmissionController {
|
|
|
178
186
|
}
|
|
179
187
|
}
|
|
180
188
|
|
|
181
|
-
// Only spawns in world space consume the normal matrix, and computing it
|
|
182
|
-
// inverts a 4x4 — skip it for local-space systems.
|
|
183
|
-
if (system.worldSpace) {
|
|
184
|
-
this.spawner.normalMatrix.getNormalMatrix(emitterMatrix);
|
|
185
|
-
}
|
|
186
189
|
const qualityFactor = system.emissionFactor();
|
|
187
190
|
|
|
188
191
|
if (system.emissionBursts.length > 0 && !endedBefore) {
|
|
@@ -2,26 +2,41 @@ import type {ParticleSystemEvent, ParticleSystemEventType} from 'quarks.core';
|
|
|
2
2
|
|
|
3
3
|
type Listener = (event: ParticleSystemEvent) => void;
|
|
4
4
|
|
|
5
|
-
/**
|
|
5
|
+
/**
|
|
6
|
+
* The callbacks registered on a {@link ParticleSystem}, by event type.
|
|
7
|
+
*
|
|
8
|
+
* The lists are copied on write, not on fire: adding or removing a callback while an event is
|
|
9
|
+
* being fired (a listener removing itself, say) changes the next firing and never the one in
|
|
10
|
+
* progress, so every callback registered when the event fired is called once.
|
|
11
|
+
*/
|
|
6
12
|
export class EventDispatcher {
|
|
7
|
-
private readonly listeners: {[event: string]: Listener[]} = {};
|
|
13
|
+
private readonly listeners: {[event: string]: readonly Listener[]} = {};
|
|
8
14
|
|
|
9
15
|
/** Registers `callback` for `event`. */
|
|
10
16
|
add(event: ParticleSystemEventType, callback: Listener): void {
|
|
11
|
-
|
|
12
|
-
this.listeners[event].push(callback);
|
|
17
|
+
this.listeners[event] = [...(this.listeners[event] ?? []), callback];
|
|
13
18
|
}
|
|
14
19
|
|
|
15
|
-
/** Drops every callback for `event
|
|
16
|
-
removeAll(event
|
|
17
|
-
if (
|
|
20
|
+
/** Drops every callback for `event`, or for every event when it is left out. */
|
|
21
|
+
removeAll(event?: ParticleSystemEventType): void {
|
|
22
|
+
if (event === undefined) {
|
|
23
|
+
for (const key of Object.keys(this.listeners)) {
|
|
24
|
+
delete this.listeners[key];
|
|
25
|
+
}
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
delete this.listeners[event];
|
|
18
29
|
}
|
|
19
30
|
|
|
20
31
|
/** Drops one callback for `event`. */
|
|
21
32
|
remove(event: ParticleSystemEventType, callback: Listener): void {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
33
|
+
const callbacks = this.listeners[event];
|
|
34
|
+
if (callbacks === undefined) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const index = callbacks.indexOf(callback);
|
|
38
|
+
if (index !== -1) {
|
|
39
|
+
this.listeners[event] = callbacks.filter((_, i) => i !== index);
|
|
25
40
|
}
|
|
26
41
|
}
|
|
27
42
|
|
|
@@ -33,6 +48,7 @@ export class EventDispatcher {
|
|
|
33
48
|
|
|
34
49
|
/** Calls the callbacks registered for the event's type, in the order they were added. */
|
|
35
50
|
fire(event: ParticleSystemEvent): void {
|
|
51
|
+
// The list as it is now: a callback added or removed by a listener does not shift it.
|
|
36
52
|
const callbacks = this.listeners[event.type];
|
|
37
53
|
if (callbacks === undefined) {
|
|
38
54
|
return;
|
|
@@ -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
|
}
|