babylon.quarks 0.26.0 → 0.28.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 +1194 -8
- package/README.md +13 -9
- 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 +10108 -0
- package/dist/types/index.d.ts +5794 -998
- package/package.json +18 -7
- package/src/BatchedRenderer.ts +274 -156
- package/src/EffectPool.ts +94 -12
- package/src/MeshSurfaceEmitter.ts +50 -14
- package/src/ParticleEmitter.ts +75 -17
- package/src/ParticleSystem.ts +981 -2066
- package/src/QuarksLoader.ts +666 -194
- package/src/QuarksPrefab.ts +367 -73
- package/src/QuarksSerializer.ts +512 -0
- package/src/QuarksUtil.ts +77 -7
- package/src/SpriteBatch.ts +127 -252
- package/src/TrailBatch.ts +545 -348
- package/src/VFXBatch.ts +238 -596
- package/src/batchKey.ts +239 -0
- package/src/batchSettings.ts +546 -0
- package/src/effectDocumentJSON.ts +261 -0
- package/src/effectResources.ts +76 -0
- package/src/envAtlas.ts +126 -18
- package/src/geometryUtil.ts +1 -1
- package/src/index.ts +69 -35
- package/src/nodeId.ts +36 -0
- package/src/shaderProgram.ts +419 -0
- package/src/shaderVariants.ts +245 -0
- package/src/shaders/chunks/blend.ts +31 -0
- package/src/shaders/chunks/fog.ts +12 -5
- package/src/shaders/chunks/sheet.ts +111 -0
- package/src/shaders/chunks/soft.ts +85 -0
- package/src/shaders/chunks/surface.ts +12 -2
- package/src/shaders/local_particle_physics_vert.glsl.ts +14 -28
- package/src/shaders/local_particle_physics_vert.wgsl.ts +16 -29
- package/src/shaders/particle_frag.glsl.ts +5 -22
- package/src/shaders/particle_frag.wgsl.ts +5 -23
- package/src/shaders/particle_physics_frag.glsl.ts +16 -26
- package/src/shaders/particle_physics_frag.wgsl.ts +14 -26
- package/src/shaders/particle_vert.glsl.ts +5 -27
- package/src/shaders/particle_vert.wgsl.ts +5 -26
- package/src/shaders/stretched_bb_particle_vert.glsl.ts +7 -33
- package/src/shaders/stretched_bb_particle_vert.wgsl.ts +7 -33
- package/src/shaders/trail_frag.glsl.ts +13 -0
- package/src/shaders/trail_frag.wgsl.ts +13 -0
- package/src/shaders/trail_vert.glsl.ts +45 -36
- package/src/shaders/trail_vert.wgsl.ts +46 -40
- package/src/softDepth.ts +136 -0
- package/src/system/BehaviorPlan.ts +85 -0
- package/src/system/BoundsTracker.ts +181 -0
- package/src/system/EmissionController.ts +358 -0
- package/src/system/EventDispatcher.ts +60 -0
- package/src/system/ParticleSpawner.ts +274 -0
- package/src/system/SimulationClock.ts +130 -0
- package/src/system/geometry.ts +104 -0
- package/src/system/integrate.ts +428 -0
- package/src/system/materialResolve.ts +165 -0
- package/src/system/normalize.ts +100 -0
- package/src/system/serialization.ts +422 -0
- package/src/system/settingsSchema.ts +339 -0
- package/src/system/types.ts +449 -0
- package/src/textureSource.ts +30 -0
- package/src/trailSettings.ts +54 -0
- package/src/BatchedParticleRenderer.ts +0 -3
- package/src/materials/ParticleMaterials.ts +0 -23
- package/src/materials/index.ts +0 -1
- package/src/shaders/chunks/index.ts +0 -1
- package/src/shaders/chunks/register-shader-chunks.ts +0 -32
- package/src/shaders/chunks/soft_fragment.glsl.ts +0 -13
- package/src/shaders/chunks/soft_pars_fragment.glsl.ts +0 -20
- package/src/shaders/chunks/soft_pars_vertex.glsl.ts +0 -6
- package/src/shaders/chunks/soft_vertex.glsl.ts +0 -6
- package/src/shaders/chunks/tile_fragment.glsl.ts +0 -9
- package/src/shaders/chunks/tile_pars_fragment.glsl.ts +0 -9
- package/src/shaders/chunks/tile_pars_vertex.glsl.ts +0 -28
- package/src/shaders/chunks/tile_vertex.glsl.ts +0 -18
- package/src/shaders/index.ts +0 -9
- package/src/shaders/local_particle_vert.glsl.ts +0 -50
|
@@ -11,9 +11,6 @@ attribute vec4 color;
|
|
|
11
11
|
|
|
12
12
|
uniform mat4 view;
|
|
13
13
|
uniform mat4 projection;
|
|
14
|
-
uniform float lineWidth;
|
|
15
|
-
uniform vec2 resolution;
|
|
16
|
-
uniform float sizeAttenuation;
|
|
17
14
|
|
|
18
15
|
varying vec2 vUV;
|
|
19
16
|
varying vec4 vColor;
|
|
@@ -23,44 +20,56 @@ attribute vec2 uv2;
|
|
|
23
20
|
varying vec2 vUV1;
|
|
24
21
|
#endif
|
|
25
22
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
23
|
+
#ifdef SOFT_PARTICLES
|
|
24
|
+
varying vec4 projPosition;
|
|
25
|
+
varying float linearDepth;
|
|
26
|
+
#endif
|
|
27
|
+
|
|
28
|
+
// The unit tangent of a segment, or zero for a segment of no length.
|
|
29
|
+
vec3 trailDirection(vec3 d) {
|
|
30
|
+
float l = dot(d, d);
|
|
31
|
+
return l > 1e-12 ? d * inversesqrt(l) : vec3(0.0);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// The direction a ribbon point is widened along, in view space: across the trail's tangent and
|
|
35
|
+
// the line of sight, as Unity's View alignment and Niagara's ribbon facing do. Stays finite at
|
|
36
|
+
// repeated points, at a hairpin and for a segment pointing at the camera.
|
|
37
|
+
vec3 trailSide(vec3 previousPoint, vec3 point, vec3 nextPoint) {
|
|
38
|
+
vec3 incoming = trailDirection(point - previousPoint);
|
|
39
|
+
vec3 outgoing = trailDirection(nextPoint - point);
|
|
40
|
+
vec3 tangent = incoming + outgoing;
|
|
41
|
+
if (dot(tangent, tangent) < 1e-6) {
|
|
42
|
+
// A hairpin turns the two directions against each other: face the way it now goes.
|
|
43
|
+
tangent = dot(outgoing, outgoing) > 0.0 ? outgoing : incoming;
|
|
44
|
+
}
|
|
45
|
+
// The line of sight: to the point for a perspective camera, the view axis for an orthographic one.
|
|
46
|
+
vec3 sight = projection[3][3] > 0.5 ? vec3(0.0, 0.0, 1.0) : point;
|
|
47
|
+
vec3 side = cross(tangent, sight);
|
|
48
|
+
if (dot(side, side) < 1e-12) {
|
|
49
|
+
side = cross(tangent, vec3(0.0, 0.0, 1.0));
|
|
50
|
+
}
|
|
51
|
+
if (dot(side, side) < 1e-12) {
|
|
52
|
+
side = vec3(1.0, 0.0, 0.0);
|
|
53
|
+
}
|
|
54
|
+
return normalize(side);
|
|
30
55
|
}
|
|
31
56
|
|
|
32
57
|
${fogVertexDeclarationGLSL}
|
|
33
58
|
void main() {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
vec2 prevP = fix(prevPos, aspect);
|
|
41
|
-
vec2 nextP = fix(nextPos, aspect);
|
|
42
|
-
|
|
43
|
-
float w = lineWidth * width;
|
|
44
|
-
vec2 dir;
|
|
45
|
-
if (distance(nextP, currentP) < 0.0001) {
|
|
46
|
-
dir = normalize(currentP - prevP);
|
|
47
|
-
} else if (distance(prevP, currentP) < 0.0001) {
|
|
48
|
-
dir = normalize(nextP - currentP);
|
|
49
|
-
} else {
|
|
50
|
-
vec2 dir1 = normalize(currentP - prevP);
|
|
51
|
-
vec2 dir2 = normalize(nextP - currentP);
|
|
52
|
-
dir = normalize(dir1 + dir2);
|
|
59
|
+
vec3 point = (view * vec4(position, 1.0)).xyz;
|
|
60
|
+
// A swept trail lays its vertices out on the CPU and gives them no width.
|
|
61
|
+
if (width > 0.0) {
|
|
62
|
+
vec3 previousPoint = (view * vec4(previous, 1.0)).xyz;
|
|
63
|
+
vec3 nextPoint = (view * vec4(next, 1.0)).xyz;
|
|
64
|
+
point += trailSide(previousPoint, point, nextPoint) * (0.5 * width * side);
|
|
53
65
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
finalPosition.xy += normal.xy * side;
|
|
62
|
-
gl_Position = finalPosition;
|
|
63
|
-
${fogVertexGLSL('(view * vec4(position, 1.0)).xyz')}
|
|
66
|
+
gl_Position = projection * vec4(point, 1.0);
|
|
67
|
+
${fogVertexGLSL('point')}
|
|
68
|
+
#ifdef SOFT_PARTICLES
|
|
69
|
+
projPosition = gl_Position;
|
|
70
|
+
// View depth is +z ahead of a left-handed camera and -z ahead of a right-handed one.
|
|
71
|
+
linearDepth = abs(point.z);
|
|
72
|
+
#endif
|
|
64
73
|
vUV = uv;
|
|
65
74
|
#ifdef UV1
|
|
66
75
|
vUV1 = uv2;
|
|
@@ -11,9 +11,6 @@ attribute color: vec4f;
|
|
|
11
11
|
|
|
12
12
|
uniform view: mat4x4f;
|
|
13
13
|
uniform projection: mat4x4f;
|
|
14
|
-
uniform lineWidth: f32;
|
|
15
|
-
uniform resolution: vec2f;
|
|
16
|
-
uniform sizeAttenuation: f32;
|
|
17
14
|
|
|
18
15
|
varying vUV: vec2f;
|
|
19
16
|
varying vColor: vec4f;
|
|
@@ -23,49 +20,58 @@ attribute uv2: vec2f;
|
|
|
23
20
|
varying vUV1: vec2f;
|
|
24
21
|
#endif
|
|
25
22
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
23
|
+
#ifdef SOFT_PARTICLES
|
|
24
|
+
varying projPosition: vec4f;
|
|
25
|
+
varying linearDepth: f32;
|
|
26
|
+
#endif
|
|
27
|
+
|
|
28
|
+
// The unit tangent of a segment, or zero for a segment of no length.
|
|
29
|
+
fn trailDirection(d: vec3f) -> vec3f {
|
|
30
|
+
let l = dot(d, d);
|
|
31
|
+
return select(vec3f(0.0), d * inverseSqrt(max(l, 1e-24)), l > 1e-12);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// The direction a ribbon point is widened along, in view space: across the trail's tangent and
|
|
35
|
+
// the line of sight, as Unity's View alignment and Niagara's ribbon facing do. Stays finite at
|
|
36
|
+
// repeated points, at a hairpin and for a segment pointing at the camera.
|
|
37
|
+
fn trailSide(previousPoint: vec3f, point: vec3f, nextPoint: vec3f) -> vec3f {
|
|
38
|
+
let incoming = trailDirection(point - previousPoint);
|
|
39
|
+
let outgoing = trailDirection(nextPoint - point);
|
|
40
|
+
var tangent = incoming + outgoing;
|
|
41
|
+
if (dot(tangent, tangent) < 1e-6) {
|
|
42
|
+
// A hairpin turns the two directions against each other: face the way it now goes.
|
|
43
|
+
tangent = select(incoming, outgoing, dot(outgoing, outgoing) > 0.0);
|
|
44
|
+
}
|
|
45
|
+
// The line of sight: to the point for a perspective camera, the view axis for an orthographic one.
|
|
46
|
+
let sight = select(point, vec3f(0.0, 0.0, 1.0), uniforms.projection[3][3] > 0.5);
|
|
47
|
+
var side = cross(tangent, sight);
|
|
48
|
+
if (dot(side, side) < 1e-12) {
|
|
49
|
+
side = cross(tangent, vec3f(0.0, 0.0, 1.0));
|
|
50
|
+
}
|
|
51
|
+
if (dot(side, side) < 1e-12) {
|
|
52
|
+
side = vec3f(1.0, 0.0, 0.0);
|
|
53
|
+
}
|
|
54
|
+
return normalize(side);
|
|
30
55
|
}
|
|
31
56
|
|
|
32
57
|
${fogVertexDeclarationWGSL}
|
|
33
58
|
@vertex
|
|
34
59
|
fn main(input: VertexInputs) -> FragmentInputs {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
let prevP = fix(prevPos, aspect);
|
|
42
|
-
let nextP = fix(nextPos, aspect);
|
|
43
|
-
|
|
44
|
-
let w = uniforms.lineWidth * vertexInputs.width;
|
|
45
|
-
var dir: vec2f;
|
|
46
|
-
if (distance(nextP, currentP) < 0.0001) {
|
|
47
|
-
dir = normalize(currentP - prevP);
|
|
48
|
-
} else if (distance(prevP, currentP) < 0.0001) {
|
|
49
|
-
dir = normalize(nextP - currentP);
|
|
50
|
-
} else {
|
|
51
|
-
let dir1 = normalize(currentP - prevP);
|
|
52
|
-
let dir2 = normalize(nextP - currentP);
|
|
53
|
-
dir = normalize(dir1 + dir2);
|
|
60
|
+
var point = (uniforms.view * vec4f(vertexInputs.position, 1.0)).xyz;
|
|
61
|
+
// A swept trail lays its vertices out on the CPU and gives them no width.
|
|
62
|
+
if (vertexInputs.width > 0.0) {
|
|
63
|
+
let previousPoint = (uniforms.view * vec4f(vertexInputs.previous, 1.0)).xyz;
|
|
64
|
+
let nextPoint = (uniforms.view * vec4f(vertexInputs.next, 1.0)).xyz;
|
|
65
|
+
point += trailSide(previousPoint, point, nextPoint) * (0.5 * vertexInputs.width * vertexInputs.side);
|
|
54
66
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}
|
|
64
|
-
finalPosition.x += normal.x * vertexInputs.side;
|
|
65
|
-
finalPosition.y += normal.y * vertexInputs.side;
|
|
66
|
-
|
|
67
|
-
vertexOutputs.position = finalPosition;
|
|
68
|
-
${fogVertexWGSL('(uniforms.view * vec4f(vertexInputs.position, 1.0)).xyz')}
|
|
67
|
+
let clipPosition = uniforms.projection * vec4f(point, 1.0);
|
|
68
|
+
vertexOutputs.position = clipPosition;
|
|
69
|
+
${fogVertexWGSL('point')}
|
|
70
|
+
#ifdef SOFT_PARTICLES
|
|
71
|
+
vertexOutputs.projPosition = clipPosition;
|
|
72
|
+
// View depth is +z ahead of a left-handed camera and -z ahead of a right-handed one.
|
|
73
|
+
vertexOutputs.linearDepth = abs(point.z);
|
|
74
|
+
#endif
|
|
69
75
|
vertexOutputs.vUV = vertexInputs.uv;
|
|
70
76
|
#ifdef UV1
|
|
71
77
|
vertexOutputs.vUV1 = vertexInputs.uv2;
|
package/src/softDepth.ts
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import {Camera} from '@babylonjs/core/Cameras/camera';
|
|
2
|
+
import type {BaseTexture} from '@babylonjs/core/Materials/Textures/baseTexture';
|
|
3
|
+
import type {Vector4} from '@babylonjs/core/Maths/math.vector';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* What a soft-particle depth texture stores per pixel, so the shaders can turn it back into the
|
|
7
|
+
* distance from the camera. These are the three outputs of Babylon's `DepthRenderer`:
|
|
8
|
+
*
|
|
9
|
+
* - `'linear'`: Babylon's default depth metric, `scene.enableDepthRenderer()`. Linear in view
|
|
10
|
+
* depth between the camera's near and far planes.
|
|
11
|
+
* - `'nonLinear'`: the depth buffer value, `enableDepthRenderer(camera, true)`. This is what
|
|
12
|
+
* Unity's `_CameraDepthTexture` holds and what `LinearEyeDepth` undoes.
|
|
13
|
+
* - `'viewZ'`: the view-space Z itself, `enableDepthRenderer(camera, false, true, sampling, true)`.
|
|
14
|
+
* Exact at any range and with an infinite far plane (`camera.maxZ = 0`), which the other two
|
|
15
|
+
* cannot represent; this is the closest match to Unreal's SceneDepth.
|
|
16
|
+
*
|
|
17
|
+
* Perspective and orthographic cameras, both NDC depth ranges (WebGL, WebGPU) and a reverse
|
|
18
|
+
* depth buffer are all handled, from the camera drawing the particles. With a reverse depth buffer
|
|
19
|
+
* and `'nonLinear'`, clear the depth renderer to 0 (`depthRenderer.clearColor.r = 0`): it clears
|
|
20
|
+
* to 1 by default, which a reverse buffer reads as the near plane, so empty sky would fade every
|
|
21
|
+
* particle in front of it. A packed `'viewZ'` cannot be decoded.
|
|
22
|
+
*/
|
|
23
|
+
export type SoftDepthEncoding = 'linear' | 'nonLinear' | 'viewZ';
|
|
24
|
+
|
|
25
|
+
/** How a depth texture given to {@link BatchedRenderer.setDepthTexture} is read. */
|
|
26
|
+
export interface DepthTextureOptions {
|
|
27
|
+
/** What the texture stores. Defaults to the encoding of the `DepthRenderer` given, else `'linear'`. */
|
|
28
|
+
encoding?: SoftDepthEncoding;
|
|
29
|
+
/**
|
|
30
|
+
* The depth is packed into the four 8-bit channels, as a `DepthRenderer` of type
|
|
31
|
+
* `UNSIGNED_BYTE` writes it. Defaults to the `DepthRenderer`'s `isPacked`, else false.
|
|
32
|
+
*/
|
|
33
|
+
packed?: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** The depth texture a batch's soft particles read, and how. */
|
|
37
|
+
export interface DepthBinding {
|
|
38
|
+
texture: BaseTexture;
|
|
39
|
+
encoding: SoftDepthEncoding;
|
|
40
|
+
packed: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** The parts of a Babylon `DepthRenderer` read here, without importing it and its side effects. */
|
|
44
|
+
interface DepthRendererLike {
|
|
45
|
+
getDepthMap(): BaseTexture;
|
|
46
|
+
readonly isPacked: boolean;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** True for a Babylon `DepthRenderer`, as opposed to a texture. */
|
|
50
|
+
export function isDepthRenderer(value: unknown): value is DepthRendererLike {
|
|
51
|
+
return typeof (value as DepthRendererLike | null)?.getDepthMap === 'function';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** The binding for a depth texture or `DepthRenderer`, with its encoding read off the renderer. */
|
|
55
|
+
export function depthBinding(depth: BaseTexture | DepthRendererLike, options: DepthTextureOptions = {}): DepthBinding {
|
|
56
|
+
const binding = readBinding(depth, options);
|
|
57
|
+
if (binding.packed && binding.encoding === 'viewZ') {
|
|
58
|
+
// Packing keeps only the fraction of the value, which loses a view Z past 1 unit.
|
|
59
|
+
throw new Error('[babylon.quarks] A packed view-Z depth cannot be decoded; use a float depth texture');
|
|
60
|
+
}
|
|
61
|
+
return binding;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function readBinding(depth: BaseTexture | DepthRendererLike, options: DepthTextureOptions): DepthBinding {
|
|
65
|
+
if (isDepthRenderer(depth)) {
|
|
66
|
+
// DepthRenderer keeps its encoding private; these are its constructor's own fields.
|
|
67
|
+
const stored = depth as unknown as {_storeCameraSpaceZ?: boolean; _storeNonLinearDepth?: boolean};
|
|
68
|
+
const detected: SoftDepthEncoding = stored._storeCameraSpaceZ
|
|
69
|
+
? 'viewZ'
|
|
70
|
+
: stored._storeNonLinearDepth
|
|
71
|
+
? 'nonLinear'
|
|
72
|
+
: 'linear';
|
|
73
|
+
return {
|
|
74
|
+
texture: depth.getDepthMap(),
|
|
75
|
+
encoding: options.encoding ?? detected,
|
|
76
|
+
packed: options.packed ?? depth.isPacked,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
return {texture: depth, encoding: options.encoding ?? 'linear', packed: options.packed ?? false};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Writes the four coefficients the soft-particle shaders decode a depth sample `d` with:
|
|
84
|
+
* view depth = |(x·d + y) / (z·d + w)|. Each encoding is a ratio of linear terms in `d` for
|
|
85
|
+
* every projection Babylon builds, so one formula covers them all.
|
|
86
|
+
*
|
|
87
|
+
* @param camera - The camera the particles are drawn for, normally the one the depth was drawn for.
|
|
88
|
+
* @param encoding - What the depth texture stores.
|
|
89
|
+
* @param out - Receives the coefficients.
|
|
90
|
+
*/
|
|
91
|
+
export function softDepthParams(camera: Camera, encoding: SoftDepthEncoding, out: Vector4): Vector4 {
|
|
92
|
+
if (encoding === 'viewZ') {
|
|
93
|
+
return out.set(1, 0, 0, 1);
|
|
94
|
+
}
|
|
95
|
+
const engine = camera.getEngine();
|
|
96
|
+
const reverse = engine.useReverseDepthBuffer;
|
|
97
|
+
const halfZ = engine.isNDCHalfZRange;
|
|
98
|
+
// A view-space point (0, 0, z) projects to clip z = z·m10 + m14, clip w = z·m11 + m15.
|
|
99
|
+
const m = camera.getProjectionMatrix().m;
|
|
100
|
+
const m10 = m[10];
|
|
101
|
+
const m11 = m[11];
|
|
102
|
+
const m14 = m[14];
|
|
103
|
+
const m15 = m[15];
|
|
104
|
+
if (encoding === 'linear') {
|
|
105
|
+
// Babylon's depth metric, (±clip z + depthValues.x) / depthValues.y, solved for z.
|
|
106
|
+
const ortho = camera.mode === Camera.ORTHOGRAPHIC_CAMERA;
|
|
107
|
+
let near: number;
|
|
108
|
+
let far: number;
|
|
109
|
+
if (ortho) {
|
|
110
|
+
near = !reverse && halfZ ? 0 : 1;
|
|
111
|
+
far = reverse && halfZ ? 0 : 1;
|
|
112
|
+
} else {
|
|
113
|
+
near = reverse && halfZ ? camera.minZ : halfZ ? 0 : camera.minZ;
|
|
114
|
+
far = reverse && halfZ ? 0 : camera.maxZ;
|
|
115
|
+
}
|
|
116
|
+
const sign = reverse ? -1 : 1;
|
|
117
|
+
const span = near + far;
|
|
118
|
+
return out.set((sign * span) / m10, (-sign * near - m14) / m10, 0, 1);
|
|
119
|
+
}
|
|
120
|
+
// The depth buffer value: NDC z = d on a half range, 2d - 1 on a full one; then
|
|
121
|
+
// z = (m14 - ndc·m15) / (ndc·m11 - m10).
|
|
122
|
+
const scale = halfZ ? 1 : 2;
|
|
123
|
+
const offset = halfZ ? 0 : -1;
|
|
124
|
+
return out.set(-scale * m15, m14 - offset * m15, scale * m11, offset * m11 - m10);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* The view depth the shaders read off a depth sample, for the coefficients of
|
|
129
|
+
* {@link softDepthParams}. Nothing there (a `'viewZ'` sample of 0, or a sample at an infinite far
|
|
130
|
+
* plane) reads as infinitely far, so the particle does not fade.
|
|
131
|
+
*/
|
|
132
|
+
export function decodeSoftDepth(sample: number, params: Vector4): number {
|
|
133
|
+
const denominator = params.z * sample + params.w;
|
|
134
|
+
const depth = Math.abs(denominator) > 1e-8 ? Math.abs((params.x * sample + params.y) / denominator) : 0;
|
|
135
|
+
return depth > 0 ? depth : 1e30;
|
|
136
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import {Behavior, FusionStep, planBehaviorFusion} from 'quarks.core';
|
|
2
|
+
|
|
3
|
+
/** Built-in behaviors that write a particle's size / colour, and those that touch neither. */
|
|
4
|
+
const SIZE_BEHAVIORS = new Set(['SizeOverLife', 'SizeBySpeed']);
|
|
5
|
+
const COLOR_BEHAVIORS = new Set(['ColorOverLife', 'ColorBySpeed']);
|
|
6
|
+
const INERT_FOR_SIZE_AND_COLOR = new Set([
|
|
7
|
+
'ApplyForce',
|
|
8
|
+
'Noise',
|
|
9
|
+
'TurbulenceField',
|
|
10
|
+
'GravityForce',
|
|
11
|
+
'RotationOverLife',
|
|
12
|
+
'Rotation3DOverLife',
|
|
13
|
+
'RotationBySpeed',
|
|
14
|
+
'SpeedOverLife',
|
|
15
|
+
'FrameOverLife',
|
|
16
|
+
'ForceOverLife',
|
|
17
|
+
'VelocityOverLife',
|
|
18
|
+
'InheritVelocity',
|
|
19
|
+
'OrbitOverLife',
|
|
20
|
+
'WidthOverLength',
|
|
21
|
+
'ChangeEmitDirection',
|
|
22
|
+
'EmitSubParticleSystem',
|
|
23
|
+
'LimitSpeedOverLife',
|
|
24
|
+
'ApplyCollision',
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* How a {@link ParticleSystem}'s behaviors run each step: consecutive behaviors that can be compiled
|
|
29
|
+
* into one loop are merged, and the plan records whether any of them can change a particle's size
|
|
30
|
+
* or colour after birth.
|
|
31
|
+
*/
|
|
32
|
+
export class BehaviorPlan {
|
|
33
|
+
/** Behaviors the cached plan was built from, compared by identity. */
|
|
34
|
+
private plan?: Array<Behavior>;
|
|
35
|
+
private fusionSteps: Array<FusionStep> = [];
|
|
36
|
+
/**
|
|
37
|
+
* Whether any behavior in the plan can change a particle's size or colour
|
|
38
|
+
* after birth. Only then does a step have to snapshot them for the renderer.
|
|
39
|
+
*/
|
|
40
|
+
changesSize = true;
|
|
41
|
+
changesColor = true;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Execution plan for this system's behaviors, rebuilt when the list changes.
|
|
45
|
+
*
|
|
46
|
+
* Consecutive behaviors that can be compiled into one loop are merged, so
|
|
47
|
+
* the particles are walked once instead of once per behavior. The plan is
|
|
48
|
+
* keyed on the behavior instances, not their types: swapping one out has to
|
|
49
|
+
* change the generated code's indices.
|
|
50
|
+
*/
|
|
51
|
+
steps(behaviors: Array<Behavior>): Array<FusionStep> {
|
|
52
|
+
const cached = this.plan;
|
|
53
|
+
if (cached !== undefined && cached.length === behaviors.length) {
|
|
54
|
+
let same = true;
|
|
55
|
+
for (let i = 0; i < behaviors.length; i++) {
|
|
56
|
+
if (cached[i] !== behaviors[i]) {
|
|
57
|
+
same = false;
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (same) {
|
|
62
|
+
return this.fusionSteps;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
this.plan = behaviors.slice();
|
|
66
|
+
this.fusionSteps = planBehaviorFusion(behaviors);
|
|
67
|
+
let changesSize = false;
|
|
68
|
+
let changesColor = false;
|
|
69
|
+
for (let i = 0; i < behaviors.length; i++) {
|
|
70
|
+
const type = behaviors[i].type;
|
|
71
|
+
if (SIZE_BEHAVIORS.has(type)) {
|
|
72
|
+
changesSize = true;
|
|
73
|
+
} else if (COLOR_BEHAVIORS.has(type)) {
|
|
74
|
+
changesColor = true;
|
|
75
|
+
} else if (!INERT_FOR_SIZE_AND_COLOR.has(type)) {
|
|
76
|
+
// Unknown to this list, so it may write anything.
|
|
77
|
+
changesSize = true;
|
|
78
|
+
changesColor = true;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
this.changesSize = changesSize;
|
|
82
|
+
this.changesColor = changesColor;
|
|
83
|
+
return this.fusionSteps;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import {RendererEmitterSettings, StretchedBillBoardSettings, Vector3} from 'quarks.core';
|
|
2
|
+
import type {VFXBatchSettings} from '../BatchedRenderer';
|
|
3
|
+
import {RenderMode} from '../VFXBatch';
|
|
4
|
+
|
|
5
|
+
/** Steps between full measurements of a system's bounds; in between they are carried and padded. */
|
|
6
|
+
const BOUNDS_REFRESH_STEPS = 4;
|
|
7
|
+
|
|
8
|
+
/** Anything with a world matrix, such as the system's emitter node; read only when the bounds are local. */
|
|
9
|
+
export interface BoundsSpace {
|
|
10
|
+
/** Its world matrix, column-major. */
|
|
11
|
+
readonly matrixWorld: {readonly elements: ArrayLike<number>};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** The largest axis of the scale in a column-major 4x4 matrix. */
|
|
15
|
+
export function maxAxisScale(e: ArrayLike<number>): number {
|
|
16
|
+
return Math.sqrt(
|
|
17
|
+
Math.max(
|
|
18
|
+
e[0] * e[0] + e[1] * e[1] + e[2] * e[2],
|
|
19
|
+
e[4] * e[4] + e[5] * e[5] + e[6] * e[6],
|
|
20
|
+
e[8] * e[8] + e[9] * e[9] + e[10] * e[10]
|
|
21
|
+
)
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The bounds a {@link ParticleSystem} keeps of what it draws, for culling: an axis-aligned box of
|
|
27
|
+
* its live particles in simulation space, and how far a particle's quad, mesh or stretch reaches
|
|
28
|
+
* past its position. The simulation step measures the box in full every few steps and carries it,
|
|
29
|
+
* padded for the motion since, in between.
|
|
30
|
+
*/
|
|
31
|
+
export class BoundsTracker {
|
|
32
|
+
/** Axis-aligned bounds of the live particles, in simulation space. */
|
|
33
|
+
readonly min = new Vector3();
|
|
34
|
+
readonly max = new Vector3();
|
|
35
|
+
/** How far a particle's quad, mesh or stretch can reach past its position, in simulation units. */
|
|
36
|
+
padding = 0;
|
|
37
|
+
valid = false;
|
|
38
|
+
/** Steps since the last full measurement; between them the bounds are carried and padded. */
|
|
39
|
+
private stepsSince = 0;
|
|
40
|
+
/** Largest particle size seen since the last full measurement. */
|
|
41
|
+
maxSize = 0;
|
|
42
|
+
/** Largest squared particle speed seen since the last full measurement. */
|
|
43
|
+
maxSpeedSq = 0;
|
|
44
|
+
/** True when the bounds are already in world space (a world-space system, or bounds the renderer measured). */
|
|
45
|
+
inWorld = false;
|
|
46
|
+
private geometryRadiusFor?: Float32Array;
|
|
47
|
+
private geometryRadius = 1;
|
|
48
|
+
|
|
49
|
+
/** Whether this step should measure every particle instead of carrying the last bounds. */
|
|
50
|
+
get due(): boolean {
|
|
51
|
+
return this.stepsSince >= BOUNDS_REFRESH_STEPS || !this.valid;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Records the box the sprite step measured (or carried) over the live particles, and pads it
|
|
56
|
+
* for how the system draws. Trails are measured by their renderer instead, since their ribbons
|
|
57
|
+
* live outside the store; see {@link setWorld}.
|
|
58
|
+
* @param step - The simulation step in seconds, which the renderer moves particles along their velocity by.
|
|
59
|
+
*/
|
|
60
|
+
store(
|
|
61
|
+
count: number,
|
|
62
|
+
measured: boolean,
|
|
63
|
+
minX: number,
|
|
64
|
+
minY: number,
|
|
65
|
+
minZ: number,
|
|
66
|
+
maxX: number,
|
|
67
|
+
maxY: number,
|
|
68
|
+
maxZ: number,
|
|
69
|
+
maxSize: number,
|
|
70
|
+
maxSpeedSq: number,
|
|
71
|
+
settings: VFXBatchSettings,
|
|
72
|
+
emitterSettings: RendererEmitterSettings,
|
|
73
|
+
worldSpace: boolean,
|
|
74
|
+
step: number
|
|
75
|
+
): void {
|
|
76
|
+
if (count === 0) {
|
|
77
|
+
this.valid = false;
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
this.stepsSince = measured ? 0 : this.stepsSince + 1;
|
|
81
|
+
this.maxSize = maxSize;
|
|
82
|
+
this.maxSpeedSq = maxSpeedSq;
|
|
83
|
+
const maxSpeed = Math.sqrt(maxSpeedSq);
|
|
84
|
+
const renderMode = settings.renderMode;
|
|
85
|
+
// How far past its position a particle can be drawn: half a quad's
|
|
86
|
+
// diagonal plus its pivot, a mesh's radius, or a stretched quad's tail.
|
|
87
|
+
// Plus the motion since the last measurement, and the step the renderer
|
|
88
|
+
// moves particles on along their velocity.
|
|
89
|
+
let padding = maxSpeed * step * (this.stepsSince + 1);
|
|
90
|
+
if (renderMode === RenderMode.Mesh) {
|
|
91
|
+
padding += maxSize * this.meshGeometryRadius(settings.instancingGeometry);
|
|
92
|
+
} else if (renderMode === RenderMode.StretchedBillBoard) {
|
|
93
|
+
const stretch = emitterSettings as StretchedBillBoardSettings;
|
|
94
|
+
padding += maxSize * (1 + (stretch.lengthFactor ?? 0) + maxSpeed * (stretch.speedFactor ?? 0));
|
|
95
|
+
} else {
|
|
96
|
+
const pivot = settings.pivot;
|
|
97
|
+
const pivotReach = pivot === undefined ? 0 : Math.abs(pivot.x) + Math.abs(pivot.y) + Math.abs(pivot.z);
|
|
98
|
+
padding += maxSize * (0.75 + pivotReach);
|
|
99
|
+
}
|
|
100
|
+
this.min.set(minX, minY, minZ);
|
|
101
|
+
this.max.set(maxX, maxY, maxZ);
|
|
102
|
+
this.padding = padding;
|
|
103
|
+
this.inWorld = worldSpace;
|
|
104
|
+
this.valid = true;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Records bounds already in world space, which a renderer measured. */
|
|
108
|
+
setWorld(minX: number, minY: number, minZ: number, maxX: number, maxY: number, maxZ: number, padding: number) {
|
|
109
|
+
this.min.set(minX, minY, minZ);
|
|
110
|
+
this.max.set(maxX, maxY, maxZ);
|
|
111
|
+
this.padding = padding;
|
|
112
|
+
this.inWorld = true;
|
|
113
|
+
this.valid = true;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** The padding in world units, for bounds kept in `space`. */
|
|
117
|
+
worldPadding(space: BoundsSpace): number {
|
|
118
|
+
return this.inWorld ? this.padding : this.padding * maxAxisScale(space.matrixWorld.elements);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* The padded bounds in world space into `min` and `max`, for bounds kept in `space`; false
|
|
123
|
+
* while there are none.
|
|
124
|
+
*/
|
|
125
|
+
toWorld(min: Vector3, max: Vector3, space: BoundsSpace): boolean {
|
|
126
|
+
if (!this.valid) {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
const bmin = this.min;
|
|
130
|
+
const bmax = this.max;
|
|
131
|
+
if (this.inWorld) {
|
|
132
|
+
const pad = this.padding;
|
|
133
|
+
min.set(bmin.x - pad, bmin.y - pad, bmin.z - pad);
|
|
134
|
+
max.set(bmax.x + pad, bmax.y + pad, bmax.z + pad);
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
// Local bounds: the box of the eight transformed corners, padded by the
|
|
138
|
+
// reach scaled by the emitter's largest axis.
|
|
139
|
+
const e = space.matrixWorld.elements;
|
|
140
|
+
const pad = this.padding * maxAxisScale(e);
|
|
141
|
+
let minX = Infinity;
|
|
142
|
+
let minY = Infinity;
|
|
143
|
+
let minZ = Infinity;
|
|
144
|
+
let maxX = -Infinity;
|
|
145
|
+
let maxY = -Infinity;
|
|
146
|
+
let maxZ = -Infinity;
|
|
147
|
+
for (let corner = 0; corner < 8; corner++) {
|
|
148
|
+
const x = corner & 1 ? bmax.x : bmin.x;
|
|
149
|
+
const y = corner & 2 ? bmax.y : bmin.y;
|
|
150
|
+
const z = corner & 4 ? bmax.z : bmin.z;
|
|
151
|
+
const w = 1 / (e[3] * x + e[7] * y + e[11] * z + e[15]);
|
|
152
|
+
const wx = (e[0] * x + e[4] * y + e[8] * z + e[12]) * w;
|
|
153
|
+
const wy = (e[1] * x + e[5] * y + e[9] * z + e[13]) * w;
|
|
154
|
+
const wz = (e[2] * x + e[6] * y + e[10] * z + e[14]) * w;
|
|
155
|
+
if (wx < minX) minX = wx;
|
|
156
|
+
if (wx > maxX) maxX = wx;
|
|
157
|
+
if (wy < minY) minY = wy;
|
|
158
|
+
if (wy > maxY) maxY = wy;
|
|
159
|
+
if (wz < minZ) minZ = wz;
|
|
160
|
+
if (wz > maxZ) maxZ = wz;
|
|
161
|
+
}
|
|
162
|
+
min.set(minX - pad, minY - pad, minZ - pad);
|
|
163
|
+
max.set(maxX + pad, maxY + pad, maxZ + pad);
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Distance of the farthest vertex of the mesh particles' geometry from its origin. */
|
|
168
|
+
private meshGeometryRadius(geometry: Float32Array): number {
|
|
169
|
+
if (geometry !== this.geometryRadiusFor) {
|
|
170
|
+
let radiusSq = 0;
|
|
171
|
+
for (let o = 0; o + 2 < geometry.length; o += 3) {
|
|
172
|
+
const d =
|
|
173
|
+
geometry[o] * geometry[o] + geometry[o + 1] * geometry[o + 1] + geometry[o + 2] * geometry[o + 2];
|
|
174
|
+
if (d > radiusSq) radiusSq = d;
|
|
175
|
+
}
|
|
176
|
+
this.geometryRadius = Math.sqrt(radiusSq);
|
|
177
|
+
this.geometryRadiusFor = geometry;
|
|
178
|
+
}
|
|
179
|
+
return this.geometryRadius;
|
|
180
|
+
}
|
|
181
|
+
}
|