babylon.quarks 0.26.0 → 0.27.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 (79) hide show
  1. package/CHANGELOG.md +769 -8
  2. package/README.md +10 -8
  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 +6683 -0
  10. package/dist/types/index.d.ts +1959 -588
  11. package/package.json +12 -6
  12. package/src/BatchedRenderer.ts +204 -101
  13. package/src/EffectPool.ts +4 -1
  14. package/src/MeshSurfaceEmitter.ts +29 -4
  15. package/src/ParticleEmitter.ts +45 -15
  16. package/src/ParticleSystem.ts +909 -2061
  17. package/src/QuarksLoader.ts +421 -67
  18. package/src/QuarksPrefab.ts +91 -4
  19. package/src/QuarksSerializer.ts +484 -0
  20. package/src/QuarksUtil.ts +15 -0
  21. package/src/SpriteBatch.ts +106 -246
  22. package/src/TrailBatch.ts +533 -342
  23. package/src/VFXBatch.ts +236 -136
  24. package/src/batchKey.ts +237 -0
  25. package/src/effectResources.ts +79 -0
  26. package/src/envAtlas.ts +119 -17
  27. package/src/index.ts +32 -33
  28. package/src/nodeId.ts +13 -0
  29. package/src/shaderProgram.ts +325 -0
  30. package/src/shaderVariants.ts +225 -0
  31. package/src/shaders/chunks/fog.ts +4 -2
  32. package/src/shaders/chunks/sheet.ts +111 -0
  33. package/src/shaders/chunks/soft.ts +84 -0
  34. package/src/shaders/chunks/surface.ts +10 -0
  35. package/src/shaders/local_particle_physics_vert.glsl.ts +14 -28
  36. package/src/shaders/local_particle_physics_vert.wgsl.ts +16 -29
  37. package/src/shaders/particle_frag.glsl.ts +3 -22
  38. package/src/shaders/particle_frag.wgsl.ts +3 -23
  39. package/src/shaders/particle_physics_frag.glsl.ts +12 -25
  40. package/src/shaders/particle_physics_frag.wgsl.ts +12 -26
  41. package/src/shaders/particle_vert.glsl.ts +5 -27
  42. package/src/shaders/particle_vert.wgsl.ts +5 -26
  43. package/src/shaders/stretched_bb_particle_vert.glsl.ts +7 -33
  44. package/src/shaders/stretched_bb_particle_vert.wgsl.ts +7 -33
  45. package/src/shaders/trail_frag.glsl.ts +11 -1
  46. package/src/shaders/trail_frag.wgsl.ts +11 -0
  47. package/src/shaders/trail_vert.glsl.ts +45 -36
  48. package/src/shaders/trail_vert.wgsl.ts +46 -40
  49. package/src/softDepth.ts +136 -0
  50. package/src/system/BehaviorPlan.ts +85 -0
  51. package/src/system/BoundsTracker.ts +181 -0
  52. package/src/system/EmissionController.ts +360 -0
  53. package/src/system/EventDispatcher.ts +44 -0
  54. package/src/system/ParticleSpawner.ts +274 -0
  55. package/src/system/SimulationClock.ts +130 -0
  56. package/src/system/geometry.ts +111 -0
  57. package/src/system/integrate.ts +428 -0
  58. package/src/system/materialResolve.ts +116 -0
  59. package/src/system/normalize.ts +95 -0
  60. package/src/system/serialization.ts +401 -0
  61. package/src/system/settingsSchema.ts +317 -0
  62. package/src/system/types.ts +410 -0
  63. package/src/textureSource.ts +30 -0
  64. package/src/trailSettings.ts +51 -0
  65. package/src/BatchedParticleRenderer.ts +0 -3
  66. package/src/materials/ParticleMaterials.ts +0 -23
  67. package/src/materials/index.ts +0 -1
  68. package/src/shaders/chunks/index.ts +0 -1
  69. package/src/shaders/chunks/register-shader-chunks.ts +0 -32
  70. package/src/shaders/chunks/soft_fragment.glsl.ts +0 -13
  71. package/src/shaders/chunks/soft_pars_fragment.glsl.ts +0 -20
  72. package/src/shaders/chunks/soft_pars_vertex.glsl.ts +0 -6
  73. package/src/shaders/chunks/soft_vertex.glsl.ts +0 -6
  74. package/src/shaders/chunks/tile_fragment.glsl.ts +0 -9
  75. package/src/shaders/chunks/tile_pars_fragment.glsl.ts +0 -9
  76. package/src/shaders/chunks/tile_pars_vertex.glsl.ts +0 -28
  77. package/src/shaders/chunks/tile_vertex.glsl.ts +0 -18
  78. package/src/shaders/index.ts +0 -9
  79. package/src/shaders/local_particle_vert.glsl.ts +0 -50
package/src/TrailBatch.ts CHANGED
@@ -1,19 +1,103 @@
1
1
  import {VertexBuffer} from '@babylonjs/core/Buffers/buffer';
2
2
  import {Constants} from '@babylonjs/core/Engines/constants';
3
3
  import {ShaderMaterial} from '@babylonjs/core/Materials/shaderMaterial';
4
- import {Vector2 as BVector2} from '@babylonjs/core/Maths/math.vector';
5
4
  import {Mesh} from '@babylonjs/core/Meshes/mesh';
6
5
  import {Scene} from '@babylonjs/core/scene';
7
6
  import {IParticleSystem, Matrix4, Quaternion, TrailParticle, TrailSettings, Vector3} from 'quarks.core';
8
7
  import {VFXBatchSettings} from './BatchedRenderer';
9
8
  import type {ParticleSystem} from './ParticleSystem';
9
+ import {shaderProgram} from './shaderProgram';
10
+ import {sheetCell} from './shaders/chunks/sheet';
10
11
  import {registerShaders, shaderLanguageFor} from './shaders/shaderLanguageSupport';
11
- import trail_frag from './shaders/trail_frag.glsl';
12
- import trail_frag_wgsl from './shaders/trail_frag.wgsl';
13
- import trail_vert from './shaders/trail_vert.glsl';
14
- import trail_vert_wgsl from './shaders/trail_vert.wgsl';
12
+ import type {DepthBinding} from './softDepth';
13
+ import {
14
+ trailCurveTension,
15
+ trailMinVertexDistance,
16
+ trailTessellation,
17
+ trailTextureMode,
18
+ trailTextureScale,
19
+ } from './trailSettings';
15
20
  import {VFXBatch} from './VFXBatch';
16
21
 
22
+ /** The texture modes as the vertex loop tells them apart, see `TrailSettings.textureMode`. */
23
+ const enum TextureMode {
24
+ Stretch,
25
+ Tile,
26
+ DistributePerSegment,
27
+ RepeatPerSegment,
28
+ }
29
+
30
+ /**
31
+ * One trail's points for a frame, in world space: where each lies, its side
32
+ * when the trail is swept, its width, its color and its distance along the
33
+ * trail. Grown as needed and kept, so drawing a trail allocates nothing.
34
+ */
35
+ class TrailSamples {
36
+ position = new Float32Array(0);
37
+ side = new Float32Array(0);
38
+ size = new Float32Array(0);
39
+ color = new Float32Array(0);
40
+ /** Distance along the trail, as recorded (`TrailParticle.historyDistances`). */
41
+ distance = new Float32Array(0);
42
+
43
+ reserve(count: number): this {
44
+ if (count > this.size.length) {
45
+ const capacity = Math.max(count, this.size.length * 2, 64);
46
+ this.position = new Float32Array(capacity * 3);
47
+ this.side = new Float32Array(capacity * 3);
48
+ this.size = new Float32Array(capacity);
49
+ this.color = new Float32Array(capacity * 4);
50
+ this.distance = new Float32Array(capacity);
51
+ }
52
+ return this;
53
+ }
54
+
55
+ /** Sample `i` at the point, carried by the matrix `m` (column-major elements) when given. */
56
+ writePoint(i: number, x: number, y: number, z: number, m: ArrayLike<number> | null): void {
57
+ const o = i * 3;
58
+ if (m) {
59
+ const w = 1 / (m[3] * x + m[7] * y + m[11] * z + m[15]);
60
+ this.position[o] = (m[0] * x + m[4] * y + m[8] * z + m[12]) * w;
61
+ this.position[o + 1] = (m[1] * x + m[5] * y + m[9] * z + m[13]) * w;
62
+ this.position[o + 2] = (m[2] * x + m[6] * y + m[10] * z + m[14]) * w;
63
+ } else {
64
+ this.position[o] = x;
65
+ this.position[o + 1] = y;
66
+ this.position[o + 2] = z;
67
+ }
68
+ }
69
+
70
+ /** Sample `i`'s side, a direction turned (and scaled) by `m` when given. */
71
+ writeSide(i: number, x: number, y: number, z: number, m: ArrayLike<number> | null): void {
72
+ const o = i * 3;
73
+ if (m) {
74
+ this.side[o] = m[0] * x + m[4] * y + m[8] * z;
75
+ this.side[o + 1] = m[1] * x + m[5] * y + m[9] * z;
76
+ this.side[o + 2] = m[2] * x + m[6] * y + m[10] * z;
77
+ } else {
78
+ this.side[o] = x;
79
+ this.side[o + 1] = y;
80
+ this.side[o + 2] = z;
81
+ }
82
+ }
83
+
84
+ writeColor(i: number, r: number, g: number, b: number, a: number): void {
85
+ const o = i * 4;
86
+ this.color[o] = r;
87
+ this.color[o + 1] = g;
88
+ this.color[o + 2] = b;
89
+ this.color[o + 3] = a;
90
+ }
91
+ }
92
+
93
+ /** Knot spacing of a centripetal spline: the square root of the length between two points of `p`. */
94
+ function knot(p: Float32Array, from: number, to: number): number {
95
+ return Math.sqrt(
96
+ Math.sqrt((p[to] - p[from]) ** 2 + (p[to + 1] - p[from + 1]) ** 2 + (p[to + 2] - p[from + 2]) ** 2)
97
+ );
98
+ }
99
+
100
+ /** The batch of trail particles. @internal */
17
101
  export class TrailBatch extends VFXBatch {
18
102
  private positionBuffer!: Float32Array;
19
103
  private previousBuffer!: Float32Array;
@@ -32,16 +116,17 @@ export class TrailBatch extends VFXBatch {
32
116
  private sideVB!: VertexBuffer;
33
117
  private widthVB!: VertexBuffer;
34
118
 
35
- constructor(settings: VFXBatchSettings, scene: Scene) {
36
- super(settings, scene);
37
- // Trail buffers hold two vertices per recorded sample and are the largest
38
- // per-batch allocation in the library, so start small and let
39
- // expandBuffers grow to whatever the batch actually needs.
119
+ constructor(settings: VFXBatchSettings, scene: Scene, depth: DepthBinding | null = null) {
120
+ super(settings, scene, depth);
121
+ // For a trail batch `maxParticles` counts vertices, two per drawn point: the buffers are
122
+ // the largest per-batch allocation in the library, so start small and let expandBuffers
123
+ // grow to whatever the batch actually needs.
40
124
  this.maxParticles = 1024;
41
125
  this.setupBuffers();
42
126
  this.rebuildMaterial();
43
127
  }
44
128
 
129
+ /** @internal */
45
130
  setupBuffers(): void {
46
131
  this.mesh.dispose();
47
132
  this.mesh = new Mesh('trailBatch', this.scene);
@@ -50,15 +135,17 @@ export class TrailBatch extends VFXBatch {
50
135
  }
51
136
 
52
137
  private buildGeometryBuffers(): void {
53
- this.positionBuffer = new Float32Array(this.maxParticles * 6);
54
- this.previousBuffer = new Float32Array(this.maxParticles * 6);
55
- this.nextBuffer = new Float32Array(this.maxParticles * 6);
56
- this.uvBuffer = new Float32Array(this.maxParticles * 4);
57
- this.uv1Buffer = this.usesUV1() ? new Float32Array(this.maxParticles * 4) : null;
58
- this.sideBuffer = new Float32Array(this.maxParticles * 2);
59
- this.widthBuffer = new Float32Array(this.maxParticles * 2);
60
- this.colorBuffer = new Float32Array(this.maxParticles * 8);
61
- this.indexBuffer = new Uint32Array(this.maxParticles * 6);
138
+ // `maxParticles` vertices; a ribbon of n points is 2n vertices and 6(n - 1) indices.
139
+ const vertices = this.maxParticles;
140
+ this.positionBuffer = new Float32Array(vertices * 3);
141
+ this.previousBuffer = new Float32Array(vertices * 3);
142
+ this.nextBuffer = new Float32Array(vertices * 3);
143
+ this.uvBuffer = new Float32Array(vertices * 2);
144
+ this.uv1Buffer = this.usesUV1() ? new Float32Array(vertices * 2) : null;
145
+ this.sideBuffer = new Float32Array(vertices);
146
+ this.widthBuffer = new Float32Array(vertices);
147
+ this.colorBuffer = new Float32Array(vertices * 4);
148
+ this.indexBuffer = new Uint32Array(vertices * 3);
62
149
  // Vertices come in pairs, one per side of the ribbon, so the side never
63
150
  // changes: written once here and never uploaded again.
64
151
  for (let i = 0; i < this.sideBuffer.length; i += 2) {
@@ -123,12 +210,13 @@ export class TrailBatch extends VFXBatch {
123
210
  }
124
211
  }
125
212
 
213
+ /** Grows the buffers to hold more than `target` vertices. @internal */
126
214
  expandBuffers(target: number): void {
127
215
  while (target >= this.maxParticles) {
128
216
  this.maxParticles *= 2;
129
217
  }
130
- // Release the previous custom attribute buffers; setVerticesBuffer only
131
- // replaces the reference, so skipping this leaks GPU memory on every grow.
218
+ // `Geometry.setVerticesBuffer` disposes the buffer it replaces; these custom attributes
219
+ // are not in the geometry's map, so they are released here.
132
220
  this.previousVB?.dispose();
133
221
  this.nextVB?.dispose();
134
222
  this.sideVB?.dispose();
@@ -136,45 +224,15 @@ export class TrailBatch extends VFXBatch {
136
224
  this.buildGeometryBuffers();
137
225
  }
138
226
 
227
+ /** @internal */
139
228
  rebuildMaterial(): void {
229
+ const program = shaderProgram(this.settings, {depth: this.depth, envAtlasReady: false, meshUV2: false});
230
+ const {defines, attributes, uniforms, samplers} = program;
140
231
  // Stable name: the trail shader source never varies, so registering it once
141
232
  // keeps Effect.ShadersStore bounded and lets Babylon reuse compiled effects.
142
- const shaderName = 'quarksTrail';
143
- const defines: string[] = this.fogDefines();
144
-
145
- if (this.settings.texture) {
146
- defines.push('USE_MAP');
147
- }
148
- defines.push(...this.surfaceDefines());
149
-
233
+ const shaderName = program.name;
150
234
  const shaderLanguage = shaderLanguageFor(this.scene.getEngine());
151
- registerShaders(
152
- shaderName,
153
- {glsl: trail_vert, wgsl: trail_vert_wgsl},
154
- {glsl: trail_frag, wgsl: trail_frag_wgsl},
155
- shaderLanguage
156
- );
157
-
158
- const attributes = ['position', 'previous', 'next', 'side', 'width', 'uv', 'color'];
159
- if (this.uv1Buffer) {
160
- attributes.push('uv2');
161
- }
162
- const uniforms = [
163
- 'world',
164
- 'view',
165
- 'projection',
166
- 'worldViewProjection',
167
- 'lineWidth',
168
- 'resolution',
169
- 'sizeAttenuation',
170
- ...VFXBatch.FOG_UNIFORMS,
171
- ];
172
- const samplers: string[] = [];
173
-
174
- if (this.settings.texture) {
175
- samplers.push('map');
176
- }
177
- this.surfaceUniforms(uniforms, samplers);
235
+ registerShaders(shaderName, program.vertex, program.fragment, shaderLanguage);
178
236
 
179
237
  const mat = new ShaderMaterial(
180
238
  shaderName,
@@ -193,24 +251,8 @@ export class TrailBatch extends VFXBatch {
193
251
  if (this.settings.texture) {
194
252
  mat.setTexture('map', this.settings.texture);
195
253
  }
196
- const engine = this.scene.getEngine();
197
- mat.setFloat('lineWidth', 1);
198
- // Reused across binds, and only re-uploaded when the render target resizes.
199
- const resolution = new BVector2(engine.getRenderWidth(), engine.getRenderHeight());
200
- mat.setVector2('resolution', resolution);
201
- mat.setFloat('sizeAttenuation', 1);
202
- mat.onBindObservable.add(() => {
203
- const renderEngine = this.scene.getEngine();
204
- const width = renderEngine.getRenderWidth();
205
- const height = renderEngine.getRenderHeight();
206
- if (resolution.x !== width || resolution.y !== height) {
207
- resolution.x = width;
208
- resolution.y = height;
209
- mat.setVector2('resolution', resolution);
210
- }
211
- });
212
-
213
254
  this.bindFog(mat);
255
+ this.bindSoft(mat);
214
256
  this.bindSurface(mat);
215
257
  mat.backFaceCulling = false;
216
258
 
@@ -219,27 +261,198 @@ export class TrailBatch extends VFXBatch {
219
261
  } else {
220
262
  mat.alphaMode = Constants.ALPHA_DISABLE;
221
263
  }
222
- mat.needDepthPrePass = this.settings.materialDepthWrite;
264
+ // A transparent surface that writes depth is drawn into the depth buffer first, so its own
265
+ // overlapping pieces do not fight; an opaque one writes depth as it draws.
266
+ mat.needDepthPrePass = this.settings.materialDepthWrite && this.settings.materialTransparent;
223
267
  mat.forceDepthWrite = this.settings.materialDepthWrite;
224
268
  mat.disableDepthWrite = !this.settings.materialDepthWrite;
269
+ // 0 keeps the engine's depth function (LEQUAL); ALWAYS draws over everything, like ZTest Always.
270
+ mat.depthFunction = this.settings.materialDepthTest ? 0 : Constants.ALWAYS;
225
271
 
272
+ // A rebuild (a depth texture attached or removed) replaces the material and its bind
273
+ // observers. Textures belong to the settings, so they stay.
274
+ (this.mesh.material as ShaderMaterial | null)?.dispose(false, false);
226
275
  this.mesh.material = mat;
227
276
  }
228
277
 
229
- private vector_ = new Vector3();
230
278
  private vector2_ = new Vector3();
231
279
  private vector3_ = new Vector3();
232
280
  private quaternion_ = new Quaternion();
233
281
 
282
+ private readonly samples_ = new TrailSamples();
283
+ private readonly curve_ = new TrailSamples();
284
+
285
+ /** The scratch a trail's samples are gathered into, room for `count`. */
286
+ private trailSamples(count: number): TrailSamples {
287
+ return this.samples_.reserve(count);
288
+ }
289
+
234
290
  /**
235
- * Brings the emitter's world matrix up to date when its transform changed.
236
- *
237
- * Babylon only recomputes a world matrix once per render id, so a frame that
238
- * moves the emitter after that (or a step driven without rendering) would
239
- * draw the ribbon against the old one. Recomputing unconditionally every
240
- * frame costs a matrix compose per system; checking the transform cache
241
- * first makes a still emitter free.
291
+ * Shortens the last segment of a drawn ribbon to `keep` of its length, along the points already
292
+ * drawn for it, so it stays a prefix of that same curve. Every channel follows.
293
+ */
294
+ private cutLastSegment(drawn: TrailSamples, count: number, perSegment: number, keep: number, swept: boolean): void {
295
+ const first = count - 1 - perSegment;
296
+ const scratch = this.cutScratch_.reserve(perSegment + 1);
297
+ scratch.position.set(drawn.position.subarray(first * 3, count * 3));
298
+ scratch.side.set(drawn.side.subarray(first * 3, count * 3));
299
+ scratch.size.set(drawn.size.subarray(first, count));
300
+ scratch.color.set(drawn.color.subarray(first * 4, count * 4));
301
+ scratch.distance.set(drawn.distance.subarray(first, count));
302
+ for (let j = 1; j <= perSegment; j++) {
303
+ const at = keep * j;
304
+ const i = Math.min(perSegment - 1, Math.floor(at));
305
+ const t = at - i;
306
+ const d = first + j;
307
+ for (let c = 0; c < 3; c++) {
308
+ drawn.position[d * 3 + c] = scratch.position[i * 3 + c] * (1 - t) + scratch.position[i * 3 + 3 + c] * t;
309
+ if (swept) {
310
+ drawn.side[d * 3 + c] = scratch.side[i * 3 + c] * (1 - t) + scratch.side[i * 3 + 3 + c] * t;
311
+ }
312
+ }
313
+ drawn.size[d] = scratch.size[i] * (1 - t) + scratch.size[i + 1] * t;
314
+ drawn.distance[d] = scratch.distance[i] * (1 - t) + scratch.distance[i + 1] * t;
315
+ for (let c = 0; c < 4; c++) {
316
+ drawn.color[d * 4 + c] = scratch.color[i * 4 + c] * (1 - t) + scratch.color[i * 4 + 4 + c] * t;
317
+ }
318
+ }
319
+ }
320
+
321
+ private readonly cutScratch_ = new TrailSamples();
322
+
323
+ /**
324
+ * The curve through a trail's `count` samples, each segment drawn as
325
+ * `steps + 1`: through the first `curved` (the recorded samples) a
326
+ * centripetal cardinal spline (Catmull-Rom at tension 0, straight at 1),
327
+ * the same through the sides of a swept trail with each side's length
328
+ * eased between its ends'; past them (the live tip) a straight segment,
329
+ * so the recorded part does not move as the tip does. Widths, colors and
330
+ * distances are eased along. The curve passes through every sample, so
331
+ * the trail keeps its shape and only loses its corners; centripetal
332
+ * spacing keeps it from overshooting where samples lie unevenly apart.
242
333
  */
334
+ private tessellate(
335
+ samples: TrailSamples,
336
+ curved: number,
337
+ count: number,
338
+ steps: number,
339
+ tension: number,
340
+ swept: boolean
341
+ ): TrailSamples {
342
+ const out = this.curve_.reserve((count - 1) * (steps + 1) + 1);
343
+ const p = samples.position;
344
+ const sd = samples.side;
345
+ const op = out.position;
346
+ const os = out.side;
347
+ const tangent = 1 - tension;
348
+ let k = 0;
349
+ for (let i = 0; i + 1 < count; i++) {
350
+ const a = i * 3;
351
+ const b = a + 3;
352
+ // Each end's tangent, as weights of the neighbouring differences
353
+ // (Yuksel et al., parameterization of Catmull-Rom curves).
354
+ let w1a = 0;
355
+ let w1b = 0;
356
+ let w1c = 0;
357
+ let w2a = 0;
358
+ let w2b = 0;
359
+ let w2c = 0;
360
+ let before = a;
361
+ let after = b;
362
+ if (i + 1 < curved) {
363
+ before = (i > 0 ? i - 1 : i) * 3;
364
+ after = (i + 2 < curved ? i + 2 : i + 1) * 3;
365
+ const t12 = knot(p, a, b);
366
+ if (t12 > 1e-6) {
367
+ const t01 = knot(p, before, a);
368
+ const t23 = knot(p, b, after);
369
+ w1c = tangent;
370
+ w2a = tangent;
371
+ if (t01 > 1e-6) {
372
+ w1a = (tangent * t12) / t01;
373
+ w1b = (-tangent * t12) / (t01 + t12);
374
+ }
375
+ if (t23 > 1e-6) {
376
+ w2b = (-tangent * t12) / (t12 + t23);
377
+ w2c = (tangent * t12) / t23;
378
+ }
379
+ }
380
+ }
381
+ // m1 = w1a (A - Before) + w1b (B - Before) + w1c (B - A);
382
+ // m2 = w2a (B - A) + w2b (After - A) + w2c (After - B).
383
+ // With no tangents (the tip's segment, or tension 1) the curve is the straight segment
384
+ // itself: walk it evenly, as the widths, colors and distances are, rather than easing.
385
+ const straight = w1a === 0 && w1b === 0 && w1c === 0 && w2a === 0 && w2b === 0 && w2c === 0;
386
+ const la = swept ? Math.sqrt(sd[a] * sd[a] + sd[a + 1] * sd[a + 1] + sd[a + 2] * sd[a + 2]) : 0;
387
+ const lb = swept ? Math.sqrt(sd[b] * sd[b] + sd[b + 1] * sd[b + 1] + sd[b + 2] * sd[b + 2]) : 0;
388
+ for (let step = 0; step <= steps; step++, k++) {
389
+ const t = step / (steps + 1);
390
+ const t2 = t * t;
391
+ const t3 = t2 * t;
392
+ const h00 = straight ? 1 - t : 2 * t3 - 3 * t2 + 1;
393
+ const h10 = t3 - 2 * t2 + t;
394
+ const h01 = straight ? t : -2 * t3 + 3 * t2;
395
+ const h11 = t3 - t2;
396
+ const o = k * 3;
397
+ for (let c = 0; c < 3; c++) {
398
+ const A = p[a + c];
399
+ const B = p[b + c];
400
+ const m1 = w1a * (A - p[before + c]) + w1b * (B - p[before + c]) + w1c * (B - A);
401
+ const m2 = w2a * (B - A) + w2b * (p[after + c] - A) + w2c * (p[after + c] - B);
402
+ op[o + c] = h00 * A + h10 * m1 + h01 * B + h11 * m2;
403
+ }
404
+ if (swept) {
405
+ let x = 0;
406
+ let y = 0;
407
+ let z = 0;
408
+ for (let c = 0; c < 3; c++) {
409
+ const A = sd[a + c];
410
+ const B = sd[b + c];
411
+ const m1 = w1a * (A - sd[before + c]) + w1b * (B - sd[before + c]) + w1c * (B - A);
412
+ const m2 = w2a * (B - A) + w2b * (sd[after + c] - A) + w2c * (sd[after + c] - B);
413
+ const v = h00 * A + h10 * m1 + h01 * B + h11 * m2;
414
+ if (c === 0) x = v;
415
+ else if (c === 1) y = v;
416
+ else z = v;
417
+ }
418
+ // A side turning with the blade keeps its length on the way round.
419
+ let length = Math.sqrt(x * x + y * y + z * z);
420
+ if (length <= 1e-6 * Math.max(la, lb, 1e-6)) {
421
+ // Turned half round in one step: no way round to follow, so
422
+ // hold the nearer end's side rather than pinch to nothing.
423
+ const from = t < 0.5 ? a : b;
424
+ x = sd[from];
425
+ y = sd[from + 1];
426
+ z = sd[from + 2];
427
+ length = t < 0.5 ? la : lb;
428
+ }
429
+ const fit = length > 0 ? (la + (lb - la) * t) / length : 0;
430
+ os[o] = x * fit;
431
+ os[o + 1] = y * fit;
432
+ os[o + 2] = z * fit;
433
+ }
434
+ out.size[k] = samples.size[i] + (samples.size[i + 1] - samples.size[i]) * t;
435
+ out.distance[k] = samples.distance[i] + (samples.distance[i + 1] - samples.distance[i]) * t;
436
+ const ca = i * 4;
437
+ const co = k * 4;
438
+ for (let c = 0; c < 4; c++) {
439
+ out.color[co + c] = samples.color[ca + c] + (samples.color[ca + 4 + c] - samples.color[ca + c]) * t;
440
+ }
441
+ }
442
+ }
443
+ // The last sample, as it is.
444
+ const last = (count - 1) * 3;
445
+ const o = k * 3;
446
+ for (let c = 0; c < 3; c++) {
447
+ op[o + c] = p[last + c];
448
+ os[o + c] = sd[last + c];
449
+ }
450
+ out.size[k] = samples.size[count - 1];
451
+ out.distance[k] = samples.distance[count - 1];
452
+ for (let c = 0; c < 4; c++) out.color[k * 4 + c] = samples.color[(count - 1) * 4 + c];
453
+ return out;
454
+ }
455
+
243
456
  /**
244
457
  * Writes a swept ribbon's vertex pair at `pi`: the sample at (x, y, z)
245
458
  * pushed `half` widths along its side (sx, sy, sz) one way for the first
@@ -264,6 +477,15 @@ export class TrailBatch extends VFXBatch {
264
477
  positions[pi + 5] = z - sz * half;
265
478
  }
266
479
 
480
+ /**
481
+ * Brings the emitter's world matrix up to date when its transform changed.
482
+ *
483
+ * Babylon only recomputes a world matrix once per render id, so a frame that
484
+ * moves the emitter after that (or a step driven without rendering) would
485
+ * draw the ribbon against the old one. Recomputing unconditionally every
486
+ * frame costs a matrix compose per system; checking the transform cache
487
+ * first makes a still emitter free.
488
+ */
267
489
  private static ensureWorldMatrix(system: IParticleSystem): void {
268
490
  const emitter = system.emitter as {
269
491
  computeWorldMatrix?: (force: boolean) => void;
@@ -274,6 +496,7 @@ export class TrailBatch extends VFXBatch {
274
496
  }
275
497
  }
276
498
 
499
+ /** @internal */
277
500
  update(): void {
278
501
  let index = 0;
279
502
  let triangles = 0;
@@ -283,14 +506,16 @@ export class TrailBatch extends VFXBatch {
283
506
  for (let s = 0; s < visibleSystems.length; s++) {
284
507
  const system = visibleSystems[s];
285
508
  const particles = system.particles;
509
+ const tessellation = trailTessellation(system.rendererEmitterSettings as TrailSettings);
286
510
  for (let j = 0; j < system.particleNum; j++) {
287
511
  const historyCount = (particles[j] as TrailParticle).historyCount;
288
512
  if (historyCount === 0) {
289
513
  continue;
290
514
  }
291
- // +2 reserves the live tip vertex pair that frames between
292
- // simulation steps append ahead of the recorded head.
293
- particleCount += historyCount * 2 + 2;
515
+ // +1 reserves the live tip that frames between simulation
516
+ // steps append ahead of the recorded head; a tessellated trail
517
+ // draws each segment as several.
518
+ particleCount += (historyCount * (tessellation + 1) + 1) * 2;
294
519
  }
295
520
  }
296
521
  if (particleCount > this.maxParticles) {
@@ -342,7 +567,25 @@ export class TrailBatch extends VFXBatch {
342
567
  // Measured from the step the particle just took, not from its
343
568
  // velocity: a trail pulled along by an orbit or by a plugin's own
344
569
  // behavior has motion no velocity term describes.
345
- const stepFraction = residual === 0 ? 0 : residual / (system.simulationStep ?? residual);
570
+ const rawFraction = residual === 0 ? 0 : residual / (system.simulationStep ?? residual);
571
+ // A few ten-thousandths of a step are float noise left in the accumulator, not time: a
572
+ // tip there would sit on the head as a segment of no length.
573
+ const stepFraction = Math.abs(rawFraction) < 1e-4 ? 0 : rawFraction;
574
+
575
+ const tessellation = trailTessellation(trailSettings);
576
+ const curveTension = trailCurveTension(trailSettings);
577
+ const textureModeName = trailTextureMode(trailSettings);
578
+ const textureMode =
579
+ textureModeName === 'stretch'
580
+ ? TextureMode.Stretch
581
+ : textureModeName === 'tile'
582
+ ? TextureMode.Tile
583
+ : textureModeName === 'repeatPerSegment'
584
+ ? TextureMode.RepeatPerSegment
585
+ : TextureMode.DistributePerSegment;
586
+ const textureScale = trailTextureScale(trailSettings);
587
+ // With a minimum vertex distance the last recorded segment is not one step of travel.
588
+ const samplesFollow = trailMinVertexDistance(trailSettings) > 0;
346
589
 
347
590
  for (let j = 0; j < particleNum; j++) {
348
591
  const particle = particles[j] as TrailParticle;
@@ -353,160 +596,239 @@ export class TrailBatch extends VFXBatch {
353
596
  const historyCapacity = particle.historyCapacity;
354
597
  const historyPositions = particle.historyPositions;
355
598
  const historySizes = particle.historySizes;
599
+ const historyDistances = particle.historyDistances;
356
600
  const historySides = particle.historySides;
357
- const swept = sideAxis !== undefined && historySides.length === historyCapacity * 3;
601
+ const swept = sideAxis !== undefined && historySides.length >= historyCapacity * 3;
358
602
  // Every sample takes its color from one place: its own recorded slot, or
359
603
  // the particle's current row in the store.
360
604
  const colors = colorFromParticle ? particle.store.color : particle.historyColors;
361
605
  const particleColorIndex = particle.storeIndex * 4;
362
- const col = particle.uvTile % vTileCount;
363
- const row = Math.floor(particle.uvTile / vTileCount + 0.001);
606
+ // The same cell as the sprite shader, wrapped into the sheet the same way.
607
+ const {column: col, row} = sheetCell(particle.uvTile, uTileCount, vTileCount);
364
608
  const particleMatrix = particle.parentMatrix as unknown as Matrix4 | undefined;
365
609
  const me = (particleMatrix ?? emitterMatrix).elements;
366
- const m00 = me[0],
367
- m01 = me[1],
368
- m02 = me[2],
369
- m03 = me[3];
370
- const m10 = me[4],
371
- m11 = me[5],
372
- m12 = me[6],
373
- m13 = me[7];
374
- const m20 = me[8],
375
- m21 = me[9],
376
- m22 = me[10],
377
- m23 = me[11];
378
- const m30 = me[12],
379
- m31 = me[13],
380
- m32 = me[14],
381
- m33 = me[15];
382
- // The texture spans the ribbon end to end: up to the live tip when one is
383
- // drawn, otherwise up to the newest recorded sample.
384
- const invHistoryLength =
385
- 1 / (stepFraction !== 0 ? particleHistoryLength : Math.max(1, particleHistoryLength - 1));
386
-
387
- // Walk the ring buffer oldest-first with hand-rolled wrapping so the
388
- // inner loop stays free of modulo operations. Each sample is brought
389
- // into world space once: the point written as `next` on one
390
- // iteration is `current` on the following one and `previous` on
391
- // the one after that.
392
- let currentSlot = particle.getHistoryIndex(0);
393
- let currentX = historyPositions[currentSlot * 3];
394
- let currentY = historyPositions[currentSlot * 3 + 1];
395
- let currentZ = historyPositions[currentSlot * 3 + 2];
396
- if (!samplesInWorld) {
397
- const w = 1 / (m03 * currentX + m13 * currentY + m23 * currentZ + m33);
398
- const tx = (m00 * currentX + m10 * currentY + m20 * currentZ + m30) * w;
399
- const ty = (m01 * currentX + m11 * currentY + m21 * currentZ + m31) * w;
400
- currentZ = (m02 * currentX + m12 * currentY + m22 * currentZ + m32) * w;
401
- currentX = tx;
402
- currentY = ty;
403
- }
404
- let previousX = currentX;
405
- let previousY = currentY;
406
- let previousZ = currentZ;
407
-
408
- for (let i = 0; i < particleHistoryLength; i++, index += 2) {
409
- let nextX = currentX;
410
- let nextY = currentY;
411
- let nextZ = currentZ;
412
- let nextSlot = currentSlot;
413
- if (i + 1 < particleHistoryLength) {
414
- nextSlot = currentSlot + 1 === historyCapacity ? 0 : currentSlot + 1;
415
- const nextPosIndex = nextSlot * 3;
416
- nextX = historyPositions[nextPosIndex];
417
- nextY = historyPositions[nextPosIndex + 1];
418
- nextZ = historyPositions[nextPosIndex + 2];
419
- if (!samplesInWorld) {
420
- const w = 1 / (m03 * nextX + m13 * nextY + m23 * nextZ + m33);
421
- const tx = (m00 * nextX + m10 * nextY + m20 * nextZ + m30) * w;
422
- const ty = (m01 * nextX + m11 * nextY + m21 * nextZ + m31) * w;
423
- nextZ = (m02 * nextX + m12 * nextY + m22 * nextZ + m32) * w;
424
- nextX = tx;
425
- nextY = ty;
426
- }
610
+ const widthScale = systemWorldSpace || particle.parentMatrix ? 1 : objectScale;
611
+ // Recorded distances are world lengths once the samples are, a local trail's scale with the emitter.
612
+ const distanceScale = samplesInWorld || particle.parentMatrix ? 1 : objectScale;
613
+ // A particle at the end of its life records nothing more (TrailParticle.update), so
614
+ // its trail ends at the last sample: a tip would follow the particle on as a growing spike.
615
+ const recording = particle.age < particle.life;
616
+ // The display runs ahead of the last step: draw a tip that far past the head.
617
+ const tipAhead = recording && stepFraction > 0;
618
+ const sampleCount = particleHistoryLength + (tipAhead ? 1 : 0);
619
+ const samples = this.trailSamples(sampleCount);
620
+
621
+ // The recorded samples, oldest first, brought into world space once.
622
+ let slot = particle.getHistoryIndex(0);
623
+ for (let i = 0; i < particleHistoryLength; i++) {
624
+ const p = historyPositions;
625
+ samples.writePoint(i, p[slot * 3], p[slot * 3 + 1], p[slot * 3 + 2], samplesInWorld ? null : me);
626
+ if (swept) {
627
+ const s = historySides;
628
+ samples.writeSide(i, s[slot * 3], s[slot * 3 + 1], s[slot * 3 + 2], samplesInWorld ? null : me);
427
629
  }
630
+ samples.size[i] = historySizes[slot];
631
+ samples.distance[i] = historyDistances[slot];
632
+ const ci = colorFromParticle ? particleColorIndex : slot * 4;
633
+ samples.writeColor(i, colors[ci], colors[ci + 1], colors[ci + 2], colors[ci + 3]);
634
+ slot = slot + 1 === historyCapacity ? 0 : slot + 1;
635
+ }
428
636
 
429
- const currentSize = historySizes[currentSlot];
430
- const currentColorIndex = colorFromParticle ? particleColorIndex : currentSlot * 4;
431
- const currentColorX = colors[currentColorIndex];
432
- const currentColorY = colors[currentColorIndex + 1];
433
- const currentColorZ = colors[currentColorIndex + 2];
434
- const currentColorW = colors[currentColorIndex + 3];
435
-
436
- const pi = index * 3;
637
+ // Live tip: same place a sprite would be drawn this frame, past
638
+ // the last recorded sample. The recorded ribbon stays put.
639
+ if (tipAhead) {
640
+ const previousHead = particle.previousPosition;
641
+ const position = particle.position;
642
+ const tip = particleHistoryLength;
643
+ let tx = position.x + (position.x - previousHead.x) * stepFraction;
644
+ let ty = position.y + (position.y - previousHead.y) * stepFraction;
645
+ let tz = position.z + (position.z - previousHead.z) * stepFraction;
646
+ samples.writePoint(tip, tx, ty, tz, systemWorldSpace ? null : me);
647
+ // The tip's distance along the trail, measured in the space the samples are
648
+ // recorded in, as theirs are.
649
+ if (samplesInWorld && !systemWorldSpace) {
650
+ const w = 1 / (me[3] * tx + me[7] * ty + me[11] * tz + me[15]);
651
+ const wx = (me[0] * tx + me[4] * ty + me[8] * tz + me[12]) * w;
652
+ const wy = (me[1] * tx + me[5] * ty + me[9] * tz + me[13]) * w;
653
+ tz = (me[2] * tx + me[6] * ty + me[10] * tz + me[14]) * w;
654
+ tx = wx;
655
+ ty = wy;
656
+ }
657
+ const headSlot = particle.getHistoryIndex(particleHistoryLength - 1) * 3;
658
+ const hx = tx - historyPositions[headSlot];
659
+ const hy = ty - historyPositions[headSlot + 1];
660
+ const hz = tz - historyPositions[headSlot + 2];
661
+ samples.distance[tip] = samples.distance[tip - 1] + Math.sqrt(hx * hx + hy * hy + hz * hz);
437
662
  if (swept) {
438
- const si = currentSlot * 3;
439
- let sx = historySides[si];
440
- let sy = historySides[si + 1];
441
- let sz = historySides[si + 2];
442
- if (!samplesInWorld) {
443
- const tx = m00 * sx + m10 * sy + m20 * sz;
444
- const ty = m01 * sx + m11 * sy + m21 * sz;
445
- sz = m02 * sx + m12 * sy + m22 * sz;
446
- sx = tx;
447
- sy = ty;
448
- }
449
- this.writeSweptPair(pi, currentX, currentY, currentZ, sx, sy, sz, currentSize * 0.5);
450
- } else {
451
- this.positionBuffer[pi] = currentX;
452
- this.positionBuffer[pi + 1] = currentY;
453
- this.positionBuffer[pi + 2] = currentZ;
454
- this.positionBuffer[pi + 3] = currentX;
455
- this.positionBuffer[pi + 4] = currentY;
456
- this.positionBuffer[pi + 5] = currentZ;
663
+ // The side as the emitter holds it now.
664
+ samples.writeSide(tip, sideAxis!.x, sideAxis!.y, sideAxis!.z, me);
457
665
  }
666
+ // As wide as the recorded head, which WidthOverLength has already scaled.
667
+ samples.size[tip] = samples.size[tip - 1];
668
+ const tipColor = particle.color;
669
+ samples.writeColor(tip, tipColor.x, tipColor.y, tipColor.z, tipColor.w);
670
+ }
458
671
 
459
- this.previousBuffer[pi] = previousX;
460
- this.previousBuffer[pi + 1] = previousY;
461
- this.previousBuffer[pi + 2] = previousZ;
462
- this.previousBuffer[pi + 3] = previousX;
463
- this.previousBuffer[pi + 4] = previousY;
464
- this.previousBuffer[pi + 5] = previousZ;
465
-
466
- this.nextBuffer[pi] = nextX;
467
- this.nextBuffer[pi + 1] = nextY;
468
- this.nextBuffer[pi + 2] = nextZ;
469
- this.nextBuffer[pi + 3] = nextX;
470
- this.nextBuffer[pi + 4] = nextY;
471
- this.nextBuffer[pi + 5] = nextZ;
672
+ // Curved through the samples (see TrailSettings.tessellation),
673
+ // or drawn through them as they are.
674
+ const drawn =
675
+ tessellation > 0
676
+ ? this.tessellate(
677
+ samples,
678
+ particleHistoryLength,
679
+ sampleCount,
680
+ tessellation,
681
+ curveTension,
682
+ swept
683
+ )
684
+ : samples;
685
+ const drawnCount = tessellation > 0 ? (sampleCount - 1) * (tessellation + 1) + 1 : sampleCount;
686
+ const perSegment = tessellation + 1;
687
+ // The last step ran ahead of the display: the head is drawn where it was at display
688
+ // time, back along the drawn last segment, rather than a tip folding back over it.
689
+ // Cut from the drawn curve, so the rest of the ribbon is the same as on any frame.
690
+ const pullBack = recording && stepFraction < 0 && particleHistoryLength > 1;
691
+ // How much of the last drawn segment is kept: all but the step's share that ran
692
+ // ahead. The segment is one step of travel, unless the head has been following the
693
+ // particle (minVertexDistance), when it is that step's share of its length.
694
+ let keep = 1 + stepFraction;
695
+ if (pullBack && samplesFollow) {
696
+ const dd = drawn.distance;
697
+ const segmentLength = dd[drawnCount - 1] - dd[drawnCount - 1 - perSegment];
698
+ if (segmentLength > 1e-9) {
699
+ const previousHead = particle.previousPosition;
700
+ const position = particle.position;
701
+ let sx = position.x - previousHead.x;
702
+ let sy = position.y - previousHead.y;
703
+ let sz = position.z - previousHead.z;
704
+ if (samplesInWorld && !systemWorldSpace) {
705
+ const wx = me[0] * sx + me[4] * sy + me[8] * sz;
706
+ const wy = me[1] * sx + me[5] * sy + me[9] * sz;
707
+ sz = me[2] * sx + me[6] * sy + me[10] * sz;
708
+ sx = wx;
709
+ sy = wy;
710
+ }
711
+ const stepTravel = Math.sqrt(sx * sx + sy * sy + sz * sz);
712
+ keep = 1 + stepFraction * Math.min(1, stepTravel / segmentLength);
713
+ }
714
+ }
715
+ if (pullBack) {
716
+ this.cutLastSegment(drawn, drawnCount, perSegment, keep, swept);
717
+ }
718
+ // The per-segment texture modes span the ribbon in samples: the recorded ones, plus
719
+ // the part of a step the tip runs ahead or the head was drawn back, so a sample's U
720
+ // changes smoothly from frame to frame whichever way the residual falls. The others
721
+ // read each point's distance along the trail, tile mode in world units.
722
+ const spanFraction = tipAhead ? stepFraction : pullBack ? keep - 1 : 0;
723
+ const spanSamples = particleHistoryLength - 1 + spanFraction;
724
+ const invLength = 1 / Math.max(1e-3, spanSamples);
725
+ const lastSegment = (sampleCount - 2) * perSegment;
726
+ const dp = drawn.position;
727
+ const dd = drawn.distance;
728
+ const tailDistance = dd[0];
729
+ const headDistance = dd[drawnCount - 1];
730
+ // A ribbon that has not gone anywhere has no distance to stretch over: lay the
731
+ // texture out per segment instead, which at least keeps its ends where they belong.
732
+ const stretch = textureMode === TextureMode.Stretch && headDistance - tailDistance > 1e-6;
733
+ const invSpanDistance = 1 / Math.max(1e-6, headDistance - tailDistance);
734
+ // Tile mode: U in repeats of the texture along the trail, anchored where the particle
735
+ // was born, less the whole repeats behind the tail so U stays small (a texture that
736
+ // wraps cannot tell) however far the particle has travelled.
737
+ const tileScale = distanceScale * textureScale;
738
+ const tileOffset = Math.floor(tailDistance * tileScale);
739
+ const vTop = (vTileCount - row - 1) * tileHeight;
740
+ const vBottom = (vTileCount - row) * tileHeight;
741
+
742
+ for (let k = 0; k < drawnCount; k++, index += 2) {
743
+ const o = k * 3;
744
+ const prev = k > 0 ? o - 3 : o;
745
+ const next = k + 1 < drawnCount ? o + 3 : o;
746
+ const x = dp[o];
747
+ const y = dp[o + 1];
748
+ const z = dp[o + 2];
749
+ const size = drawn.size[k];
472
750
 
751
+ const pi = index * 3;
473
752
  if (swept) {
753
+ const ds = drawn.side;
754
+ this.writeSweptPair(pi, x, y, z, ds[o], ds[o + 1], ds[o + 2], size * 0.5);
755
+ // A zero width keeps the shader from pushing the pair.
474
756
  this.widthBuffer[index] = 0;
475
757
  this.widthBuffer[index + 1] = 0;
476
- } else if (systemWorldSpace || particle.parentMatrix) {
477
- this.widthBuffer[index] = currentSize;
478
- this.widthBuffer[index + 1] = currentSize;
479
758
  } else {
480
- this.widthBuffer[index] = currentSize * objectScale;
481
- this.widthBuffer[index + 1] = currentSize * objectScale;
759
+ this.positionBuffer[pi] = x;
760
+ this.positionBuffer[pi + 1] = y;
761
+ this.positionBuffer[pi + 2] = z;
762
+ this.positionBuffer[pi + 3] = x;
763
+ this.positionBuffer[pi + 4] = y;
764
+ this.positionBuffer[pi + 5] = z;
765
+ this.widthBuffer[index] = size * widthScale;
766
+ this.widthBuffer[index + 1] = size * widthScale;
482
767
  }
483
768
 
769
+ this.previousBuffer[pi] = dp[prev];
770
+ this.previousBuffer[pi + 1] = dp[prev + 1];
771
+ this.previousBuffer[pi + 2] = dp[prev + 2];
772
+ this.previousBuffer[pi + 3] = dp[prev];
773
+ this.previousBuffer[pi + 4] = dp[prev + 1];
774
+ this.previousBuffer[pi + 5] = dp[prev + 2];
775
+
776
+ this.nextBuffer[pi] = dp[next];
777
+ this.nextBuffer[pi + 1] = dp[next + 1];
778
+ this.nextBuffer[pi + 2] = dp[next + 2];
779
+ this.nextBuffer[pi + 3] = dp[next];
780
+ this.nextBuffer[pi + 4] = dp[next + 1];
781
+ this.nextBuffer[pi + 5] = dp[next + 2];
782
+
484
783
  const ui = index * 2;
485
- const along = i * invHistoryLength;
486
- const u = ((uvFromHead ? 1 - along : along) + col) * tileWidth;
784
+ let u1: number;
785
+ if (stretch) {
786
+ const along = (dd[k] - tailDistance) * invSpanDistance;
787
+ u1 = (uvFromHead ? 1 - along : along) * textureScale;
788
+ } else if (textureMode === TextureMode.Tile) {
789
+ // A sample's U holds while it lives; from the particle back with uvFromHead.
790
+ u1 = uvFromHead ? (headDistance - dd[k]) * tileScale : dd[k] * tileScale - tileOffset;
791
+ } else {
792
+ // Where this point sits along the ribbon, in samples.
793
+ let at = k / perSegment;
794
+ if (k > lastSegment && (tipAhead || pullBack)) {
795
+ const into = (k - lastSegment) / perSegment;
796
+ at = tipAhead
797
+ ? particleHistoryLength - 1 + stepFraction * into
798
+ : particleHistoryLength - 2 + keep * into;
799
+ }
800
+ if (textureMode !== TextureMode.RepeatPerSegment) {
801
+ const along = Math.min(1, at * invLength);
802
+ u1 = (uvFromHead ? 1 - along : along) * textureScale;
803
+ } else {
804
+ u1 = (uvFromHead ? spanSamples - at : at) * textureScale;
805
+ }
806
+ }
807
+ const u = (u1 + col) * tileWidth;
487
808
  this.uvBuffer[ui] = u;
488
- this.uvBuffer[ui + 1] = (vTileCount - row - 1) * tileHeight;
809
+ this.uvBuffer[ui + 1] = vTop;
489
810
  this.uvBuffer[ui + 2] = u;
490
- this.uvBuffer[ui + 3] = (vTileCount - row) * tileHeight;
811
+ this.uvBuffer[ui + 3] = vBottom;
491
812
  if (uv1) {
492
- const u1 = uvFromHead ? 1 - along : along;
493
813
  uv1[ui] = u1;
494
814
  uv1[ui + 1] = 0;
495
815
  uv1[ui + 2] = u1;
496
816
  uv1[ui + 3] = 1;
497
817
  }
498
818
 
819
+ const dc = drawn.color;
820
+ const c = k * 4;
499
821
  const cci = index * 4;
500
- this.colorBuffer[cci] = currentColorX;
501
- this.colorBuffer[cci + 1] = currentColorY;
502
- this.colorBuffer[cci + 2] = currentColorZ;
503
- this.colorBuffer[cci + 3] = currentColorW;
504
- this.colorBuffer[cci + 4] = currentColorX;
505
- this.colorBuffer[cci + 5] = currentColorY;
506
- this.colorBuffer[cci + 6] = currentColorZ;
507
- this.colorBuffer[cci + 7] = currentColorW;
508
-
509
- if (i + 1 < particleHistoryLength) {
822
+ this.colorBuffer[cci] = dc[c];
823
+ this.colorBuffer[cci + 1] = dc[c + 1];
824
+ this.colorBuffer[cci + 2] = dc[c + 2];
825
+ this.colorBuffer[cci + 3] = dc[c + 3];
826
+ this.colorBuffer[cci + 4] = dc[c];
827
+ this.colorBuffer[cci + 5] = dc[c + 1];
828
+ this.colorBuffer[cci + 6] = dc[c + 2];
829
+ this.colorBuffer[cci + 7] = dc[c + 3];
830
+
831
+ if (k + 1 < drawnCount) {
510
832
  this.indexBuffer[triangles * 3] = index;
511
833
  this.indexBuffer[triangles * 3 + 1] = index + 1;
512
834
  this.indexBuffer[triangles * 3 + 2] = index + 2;
@@ -516,138 +838,6 @@ export class TrailBatch extends VFXBatch {
516
838
  this.indexBuffer[triangles * 3 + 2] = index + 3;
517
839
  triangles++;
518
840
  }
519
-
520
- previousX = currentX;
521
- previousY = currentY;
522
- previousZ = currentZ;
523
- currentX = nextX;
524
- currentY = nextY;
525
- currentZ = nextZ;
526
- currentSlot = nextSlot;
527
- }
528
-
529
- // Live tip: same place a sprite would be drawn this frame, past
530
- // the last recorded sample. The recorded ribbon stays put.
531
- if (stepFraction !== 0) {
532
- const previousHead = particle.previousPosition;
533
- const position = particle.position;
534
- let tipX = position.x + (position.x - previousHead.x) * stepFraction;
535
- let tipY = position.y + (position.y - previousHead.y) * stepFraction;
536
- let tipZ = position.z + (position.z - previousHead.z) * stepFraction;
537
- const headIndex = index - 2;
538
- // The pair's midpoint: a swept pair lies to either side of its sample.
539
- const headX = (this.positionBuffer[headIndex * 3] + this.positionBuffer[headIndex * 3 + 3]) * 0.5;
540
- const headY =
541
- (this.positionBuffer[headIndex * 3 + 1] + this.positionBuffer[headIndex * 3 + 4]) * 0.5;
542
- const headZ =
543
- (this.positionBuffer[headIndex * 3 + 2] + this.positionBuffer[headIndex * 3 + 5]) * 0.5;
544
-
545
- if (!systemWorldSpace) {
546
- const w = 1 / (m03 * tipX + m13 * tipY + m23 * tipZ + m33);
547
- const tx = (m00 * tipX + m10 * tipY + m20 * tipZ + m30) * w;
548
- const ty = (m01 * tipX + m11 * tipY + m21 * tipZ + m31) * w;
549
- tipZ = (m02 * tipX + m12 * tipY + m22 * tipZ + m32) * w;
550
- tipX = tx;
551
- tipY = ty;
552
- }
553
-
554
- const tipPi = index * 3;
555
- const tipSize = particle.size.x;
556
- if (swept) {
557
- // The side as the emitter holds it now.
558
- const ax = sideAxis!.x;
559
- const ay = sideAxis!.y;
560
- const az = sideAxis!.z;
561
- this.writeSweptPair(
562
- tipPi,
563
- tipX,
564
- tipY,
565
- tipZ,
566
- m00 * ax + m10 * ay + m20 * az,
567
- m01 * ax + m11 * ay + m21 * az,
568
- m02 * ax + m12 * ay + m22 * az,
569
- tipSize * 0.5
570
- );
571
- } else {
572
- this.positionBuffer[tipPi] = tipX;
573
- this.positionBuffer[tipPi + 1] = tipY;
574
- this.positionBuffer[tipPi + 2] = tipZ;
575
- this.positionBuffer[tipPi + 3] = tipX;
576
- this.positionBuffer[tipPi + 4] = tipY;
577
- this.positionBuffer[tipPi + 5] = tipZ;
578
- }
579
-
580
- this.previousBuffer[tipPi] = headX;
581
- this.previousBuffer[tipPi + 1] = headY;
582
- this.previousBuffer[tipPi + 2] = headZ;
583
- this.previousBuffer[tipPi + 3] = headX;
584
- this.previousBuffer[tipPi + 4] = headY;
585
- this.previousBuffer[tipPi + 5] = headZ;
586
-
587
- this.nextBuffer[tipPi] = tipX;
588
- this.nextBuffer[tipPi + 1] = tipY;
589
- this.nextBuffer[tipPi + 2] = tipZ;
590
- this.nextBuffer[tipPi + 3] = tipX;
591
- this.nextBuffer[tipPi + 4] = tipY;
592
- this.nextBuffer[tipPi + 5] = tipZ;
593
-
594
- // The recorded head's `next` was itself; point it at the tip
595
- // so the shader builds the live segment from one version of
596
- // each endpoint.
597
- const headPi = headIndex * 3;
598
- this.nextBuffer[headPi] = tipX;
599
- this.nextBuffer[headPi + 1] = tipY;
600
- this.nextBuffer[headPi + 2] = tipZ;
601
- this.nextBuffer[headPi + 3] = tipX;
602
- this.nextBuffer[headPi + 4] = tipY;
603
- this.nextBuffer[headPi + 5] = tipZ;
604
-
605
- if (swept) {
606
- this.widthBuffer[index] = 0;
607
- this.widthBuffer[index + 1] = 0;
608
- } else if (systemWorldSpace || particle.parentMatrix) {
609
- this.widthBuffer[index] = tipSize;
610
- this.widthBuffer[index + 1] = tipSize;
611
- } else {
612
- this.widthBuffer[index] = tipSize * objectScale;
613
- this.widthBuffer[index + 1] = tipSize * objectScale;
614
- }
615
-
616
- const tipUi = index * 2;
617
- const tipU = ((uvFromHead ? 0 : 1) + col) * tileWidth;
618
- this.uvBuffer[tipUi] = tipU;
619
- this.uvBuffer[tipUi + 1] = (vTileCount - row - 1) * tileHeight;
620
- this.uvBuffer[tipUi + 2] = tipU;
621
- this.uvBuffer[tipUi + 3] = (vTileCount - row) * tileHeight;
622
- if (uv1) {
623
- const tipU1 = uvFromHead ? 0 : 1;
624
- uv1[tipUi] = tipU1;
625
- uv1[tipUi + 1] = 0;
626
- uv1[tipUi + 2] = tipU1;
627
- uv1[tipUi + 3] = 1;
628
- }
629
-
630
- const tipColor = particle.color;
631
- const tipCi = index * 4;
632
- this.colorBuffer[tipCi] = tipColor.x;
633
- this.colorBuffer[tipCi + 1] = tipColor.y;
634
- this.colorBuffer[tipCi + 2] = tipColor.z;
635
- this.colorBuffer[tipCi + 3] = tipColor.w;
636
- this.colorBuffer[tipCi + 4] = tipColor.x;
637
- this.colorBuffer[tipCi + 5] = tipColor.y;
638
- this.colorBuffer[tipCi + 6] = tipColor.z;
639
- this.colorBuffer[tipCi + 7] = tipColor.w;
640
-
641
- this.indexBuffer[triangles * 3] = headIndex;
642
- this.indexBuffer[triangles * 3 + 1] = headIndex + 1;
643
- this.indexBuffer[triangles * 3 + 2] = index;
644
- triangles++;
645
- this.indexBuffer[triangles * 3] = index;
646
- this.indexBuffer[triangles * 3 + 1] = headIndex + 1;
647
- this.indexBuffer[triangles * 3 + 2] = index + 1;
648
- triangles++;
649
-
650
- index += 2;
651
841
  }
652
842
  }
653
843
  const tint = (system as ParticleSystem).tint;
@@ -783,6 +973,7 @@ export class TrailBatch extends VFXBatch {
783
973
  }
784
974
  }
785
975
 
976
+ /** @internal */
786
977
  dispose(): void {
787
978
  if (this.mesh.material) {
788
979
  this.mesh.material.dispose();