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.
Files changed (83) hide show
  1. package/CHANGELOG.md +1194 -8
  2. package/README.md +13 -9
  3. package/dist/babylon.quarks.cjs +1 -1
  4. package/dist/babylon.quarks.cjs.map +1 -1
  5. package/dist/babylon.quarks.esm.js +1 -1
  6. package/dist/babylon.quarks.esm.js.map +1 -1
  7. package/dist/babylon.quarks.umd.min.js +1 -1
  8. package/dist/babylon.quarks.umd.min.js.map +1 -1
  9. package/dist/types/index.d.cts +10108 -0
  10. package/dist/types/index.d.ts +5794 -998
  11. package/package.json +18 -7
  12. package/src/BatchedRenderer.ts +274 -156
  13. package/src/EffectPool.ts +94 -12
  14. package/src/MeshSurfaceEmitter.ts +50 -14
  15. package/src/ParticleEmitter.ts +75 -17
  16. package/src/ParticleSystem.ts +981 -2066
  17. package/src/QuarksLoader.ts +666 -194
  18. package/src/QuarksPrefab.ts +367 -73
  19. package/src/QuarksSerializer.ts +512 -0
  20. package/src/QuarksUtil.ts +77 -7
  21. package/src/SpriteBatch.ts +127 -252
  22. package/src/TrailBatch.ts +545 -348
  23. package/src/VFXBatch.ts +238 -596
  24. package/src/batchKey.ts +239 -0
  25. package/src/batchSettings.ts +546 -0
  26. package/src/effectDocumentJSON.ts +261 -0
  27. package/src/effectResources.ts +76 -0
  28. package/src/envAtlas.ts +126 -18
  29. package/src/geometryUtil.ts +1 -1
  30. package/src/index.ts +69 -35
  31. package/src/nodeId.ts +36 -0
  32. package/src/shaderProgram.ts +419 -0
  33. package/src/shaderVariants.ts +245 -0
  34. package/src/shaders/chunks/blend.ts +31 -0
  35. package/src/shaders/chunks/fog.ts +12 -5
  36. package/src/shaders/chunks/sheet.ts +111 -0
  37. package/src/shaders/chunks/soft.ts +85 -0
  38. package/src/shaders/chunks/surface.ts +12 -2
  39. package/src/shaders/local_particle_physics_vert.glsl.ts +14 -28
  40. package/src/shaders/local_particle_physics_vert.wgsl.ts +16 -29
  41. package/src/shaders/particle_frag.glsl.ts +5 -22
  42. package/src/shaders/particle_frag.wgsl.ts +5 -23
  43. package/src/shaders/particle_physics_frag.glsl.ts +16 -26
  44. package/src/shaders/particle_physics_frag.wgsl.ts +14 -26
  45. package/src/shaders/particle_vert.glsl.ts +5 -27
  46. package/src/shaders/particle_vert.wgsl.ts +5 -26
  47. package/src/shaders/stretched_bb_particle_vert.glsl.ts +7 -33
  48. package/src/shaders/stretched_bb_particle_vert.wgsl.ts +7 -33
  49. package/src/shaders/trail_frag.glsl.ts +13 -0
  50. package/src/shaders/trail_frag.wgsl.ts +13 -0
  51. package/src/shaders/trail_vert.glsl.ts +45 -36
  52. package/src/shaders/trail_vert.wgsl.ts +46 -40
  53. package/src/softDepth.ts +136 -0
  54. package/src/system/BehaviorPlan.ts +85 -0
  55. package/src/system/BoundsTracker.ts +181 -0
  56. package/src/system/EmissionController.ts +358 -0
  57. package/src/system/EventDispatcher.ts +60 -0
  58. package/src/system/ParticleSpawner.ts +274 -0
  59. package/src/system/SimulationClock.ts +130 -0
  60. package/src/system/geometry.ts +104 -0
  61. package/src/system/integrate.ts +428 -0
  62. package/src/system/materialResolve.ts +165 -0
  63. package/src/system/normalize.ts +100 -0
  64. package/src/system/serialization.ts +422 -0
  65. package/src/system/settingsSchema.ts +339 -0
  66. package/src/system/types.ts +449 -0
  67. package/src/textureSource.ts +30 -0
  68. package/src/trailSettings.ts +54 -0
  69. package/src/BatchedParticleRenderer.ts +0 -3
  70. package/src/materials/ParticleMaterials.ts +0 -23
  71. package/src/materials/index.ts +0 -1
  72. package/src/shaders/chunks/index.ts +0 -1
  73. package/src/shaders/chunks/register-shader-chunks.ts +0 -32
  74. package/src/shaders/chunks/soft_fragment.glsl.ts +0 -13
  75. package/src/shaders/chunks/soft_pars_fragment.glsl.ts +0 -20
  76. package/src/shaders/chunks/soft_pars_vertex.glsl.ts +0 -6
  77. package/src/shaders/chunks/soft_vertex.glsl.ts +0 -6
  78. package/src/shaders/chunks/tile_fragment.glsl.ts +0 -9
  79. package/src/shaders/chunks/tile_pars_fragment.glsl.ts +0 -9
  80. package/src/shaders/chunks/tile_pars_vertex.glsl.ts +0 -28
  81. package/src/shaders/chunks/tile_vertex.glsl.ts +0 -18
  82. package/src/shaders/index.ts +0 -9
  83. 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
- vec2 fix(vec4 i, float aspect) {
27
- vec2 res = i.xy / i.w;
28
- res.x *= aspect;
29
- return res;
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
- float aspect = resolution.x / resolution.y;
35
- vec4 finalPosition = projection * view * vec4(position, 1.0);
36
- vec4 prevPos = projection * view * vec4(previous, 1.0);
37
- vec4 nextPos = projection * view * vec4(next, 1.0);
38
-
39
- vec2 currentP = fix(finalPosition, aspect);
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
- vec4 normal = vec4(-dir.y, dir.x, 0.0, 1.0);
55
- normal.xy *= 0.5 * w;
56
- normal *= projection;
57
- if (sizeAttenuation == 0.0) {
58
- normal.xy *= finalPosition.w;
59
- normal.xy /= (vec4(resolution, 0.0, 1.0) * projection).xy;
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
- fn fix(i: vec4f, aspect: f32) -> vec2f {
27
- var res = i.xy / i.w;
28
- res.x *= aspect;
29
- return res;
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
- let aspect = uniforms.resolution.x / uniforms.resolution.y;
36
- var finalPosition = uniforms.projection * uniforms.view * vec4f(vertexInputs.position, 1.0);
37
- let prevPos = uniforms.projection * uniforms.view * vec4f(vertexInputs.previous, 1.0);
38
- let nextPos = uniforms.projection * uniforms.view * vec4f(vertexInputs.next, 1.0);
39
-
40
- let currentP = fix(finalPosition, aspect);
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
- var normal = vec4f(-dir.y, dir.x, 0.0, 1.0);
56
- normal.x *= 0.5 * w;
57
- normal.y *= 0.5 * w;
58
- normal = normal * uniforms.projection;
59
- if (uniforms.sizeAttenuation == 0.0) {
60
- let screen = (vec4f(uniforms.resolution, 0.0, 1.0) * uniforms.projection).xy;
61
- normal.x = normal.x * finalPosition.w / screen.x;
62
- normal.y = normal.y * finalPosition.w / screen.y;
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;
@@ -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
+ }