babylon.quarks 0.17.2 → 0.17.4

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.
@@ -25,16 +25,35 @@ export interface VFXBatchSettings {
25
25
  texture: Texture | null;
26
26
  layerMask: number;
27
27
  }
28
+ export interface AdaptivePerformanceOptions {
29
+ targetFrameMs: number;
30
+ minQuality: number;
31
+ maxQuality: number;
32
+ decreaseStep: number;
33
+ increaseStep: number;
34
+ }
35
+ export interface AdaptivePerformanceState extends AdaptivePerformanceOptions {
36
+ enabled: boolean;
37
+ currentQuality: number;
38
+ lastFrameCpuMs: number;
39
+ }
28
40
  export declare class BatchedRenderer extends TransformNode {
29
41
  batches: Array<VFXBatch>;
30
42
  systemToBatchIndex: Map<IParticleSystem, number>;
31
43
  depthTexture: BaseTexture | null;
44
+ private adaptivePerformanceState;
45
+ private lastAppliedQuality;
32
46
  constructor(name: string, scene: Scene);
47
+ private static clamp;
33
48
  private static equals;
34
49
  addSystem(system: IParticleSystem): void;
35
50
  deleteSystem(system: IParticleSystem): void;
36
51
  updateSystem(system: IParticleSystem): void;
37
52
  setDepthTexture(depthTexture: BaseTexture | null): void;
53
+ configureAdaptivePerformance(options?: Partial<AdaptivePerformanceOptions>): void;
54
+ disableAdaptivePerformance(resetQuality?: boolean): void;
55
+ getAdaptivePerformanceState(): AdaptivePerformanceState;
56
+ private applyAdaptiveQuality;
38
57
  update(delta: number): void;
39
58
  dispose(): void;
40
59
  }
@@ -162,6 +162,8 @@ export declare class ParticleSystem implements IParticleSystem {
162
162
  private listeners;
163
163
  private readonly layerMaskProxy;
164
164
  private materialRef;
165
+ private qualityFactor;
166
+ private useFastTrailHistory;
165
167
  /** @internal **/
166
168
  _renderer?: BatchedRenderer;
167
169
  set time(time: number);
@@ -198,6 +200,12 @@ export declare class ParticleSystem implements IParticleSystem {
198
200
  pause(): void;
199
201
  play(): void;
200
202
  stop(): void;
203
+ private refreshTrailHistoryMode;
204
+ private ensureTrailHistoryCapacity;
205
+ private resetTrailHistory;
206
+ private getTrailHistoryCount;
207
+ private updateFastTrailHistory;
208
+ setQualityFactor(qualityFactor: number): void;
201
209
  private spawn;
202
210
  endEmit(): void;
203
211
  dispose(): void;
@@ -27,6 +27,7 @@ export declare class SpriteBatch extends VFXBatch {
27
27
  private quaternion3_;
28
28
  private rotationMat_;
29
29
  private rotationMat2_;
30
+ private lastStretchedSpeedFactor;
30
31
  update(): void;
31
32
  dispose(): void;
32
33
  }
@@ -14,6 +14,7 @@ export declare class TrailBatch extends VFXBatch {
14
14
  private nextVB;
15
15
  private sideVB;
16
16
  private widthVB;
17
+ private getTrailHistoryCount;
17
18
  constructor(settings: VFXBatchSettings, scene: Scene);
18
19
  setupBuffers(): void;
19
20
  expandBuffers(target: number): void;
@@ -0,0 +1,2 @@
1
+ /** Sequential 0..N-1 indices for non-indexed triangle-list BufferGeometry (Three.js drawArrays). */
2
+ export declare function ensureTriangleIndices(positions: Float32Array, indices?: Uint32Array | Uint16Array): Uint32Array | Uint16Array;
@@ -1,2 +1,2 @@
1
- declare const _default: "\nattribute vec3 position;\nattribute vec2 uv;\nattribute vec3 normal;\nattribute vec3 offset;\nattribute vec4 color;\nattribute vec3 size;\nattribute vec4 rotation;\nattribute float uvTile;\n\nuniform mat4 world;\nuniform mat4 view;\nuniform mat4 projection;\n\n#ifdef UV_TILE\nuniform float tileCountX;\nuniform float tileCountY;\n#endif\n\nvarying vec2 vUV;\nvarying vec4 vColor;\nvarying vec3 vNormal;\nvarying vec3 vWorldPos;\n\n#ifdef SOFT_PARTICLES\nvarying vec4 projPosition;\nvarying float linearDepth;\n#endif\n\nvec3 applyQuaternion(vec3 v, vec4 q) {\n vec3 qVec = q.xyz;\n float qW = q.w;\n vec3 t = 2.0 * cross(qVec, v);\n return v + qW * t + cross(qVec, t);\n}\n\nvoid main() {\n vec3 scaledPosition = position * size;\n vec3 rotatedPosition = applyQuaternion(scaledPosition, rotation);\n vec3 rotatedNormal = normalize(applyQuaternion(normal, rotation));\n vec3 localPos = rotatedPosition + offset;\n\n vec4 worldPos = world * vec4(localPos, 1.0);\n vec4 viewPos = view * worldPos;\n gl_Position = projection * viewPos;\n\n vWorldPos = worldPos.xyz;\n vNormal = normalize((world * vec4(rotatedNormal, 0.0)).xyz);\n\n#ifdef SOFT_PARTICLES\n projPosition = gl_Position;\n linearDepth = -viewPos.z;\n#endif\n\n#ifdef UV_TILE\n vec2 tc = vec2(tileCountX, tileCountY);\n float tileU = mod(uvTile, tc.x) / tc.x;\n float tileV = 1.0 - floor(uvTile / tc.x) / tc.y - 1.0 / tc.y;\n vUV = uv / tc + vec2(tileU, tileV);\n#else\n vUV = uv;\n#endif\n\n vColor = color;\n}\n";
1
+ declare const _default: "\nattribute vec3 position;\nattribute vec2 uv;\nattribute vec3 normal;\nattribute vec3 offset;\nattribute vec4 color;\nattribute vec3 size;\nattribute vec4 rotation;\nattribute float uvTile;\n\nuniform mat4 world;\nuniform mat4 view;\nuniform mat4 projection;\n\n#ifdef UV_TILE\nuniform float tileCountX;\nuniform float tileCountY;\n#endif\n\nvarying vec2 vUV;\nvarying vec4 vColor;\nvarying vec3 vNormal;\nvarying vec3 vWorldPos;\n\n#ifdef TILE_BLEND\nvarying vec2 vUV2;\nvarying float vTileBlend;\n#endif\n\n#ifdef SOFT_PARTICLES\nvarying vec4 projPosition;\nvarying float linearDepth;\n#endif\n\nvec3 applyQuaternion(vec3 v, vec4 q) {\n vec3 qVec = q.xyz;\n float qW = q.w;\n vec3 t = 2.0 * cross(qVec, v);\n return v + qW * t + cross(qVec, t);\n}\n\nvoid main() {\n vec3 scaledPosition = position * size;\n vec3 rotatedPosition = applyQuaternion(scaledPosition, rotation);\n vec3 rotatedNormal = normalize(applyQuaternion(normal, rotation));\n vec3 localPos = rotatedPosition + offset;\n\n vec4 worldPos = world * vec4(localPos, 1.0);\n vec4 viewPos = view * worldPos;\n gl_Position = projection * viewPos;\n\n vWorldPos = worldPos.xyz;\n vNormal = normalize((world * vec4(rotatedNormal, 0.0)).xyz);\n\n#ifdef SOFT_PARTICLES\n projPosition = gl_Position;\n linearDepth = -viewPos.z;\n#endif\n\n#ifdef UV_TILE\n vec2 tc = vec2(tileCountX, tileCountY);\n float baseTile = floor(uvTile);\n float tileU = mod(baseTile, tc.x) / tc.x;\n float tileV = 1.0 - floor(baseTile / tc.x) / tc.y - 1.0 / tc.y;\n vUV = uv / tc + vec2(tileU, tileV);\n #ifdef TILE_BLEND\n float nextTile = ceil(uvTile);\n float nextU = mod(nextTile, tc.x) / tc.x;\n float nextV = 1.0 - floor(nextTile / tc.x) / tc.y - 1.0 / tc.y;\n vUV2 = uv / tc + vec2(nextU, nextV);\n vTileBlend = fract(uvTile);\n #endif\n#else\n vUV = uv;\n#endif\n\n vColor = color;\n}\n";
2
2
  export default _default;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "babylon.quarks",
3
- "version": "0.17.2",
3
+ "version": "0.17.4",
4
4
  "description": "A High-Performance Particle System for Babylon.js",
5
5
  "type": "module",
6
6
  "types": "./dist/types/index.d.ts",
@@ -25,6 +25,7 @@
25
25
  "build:js": "cross-env NODE_ENV=production rollup -c",
26
26
  "build": "npm run clean && npm run build:types && npm run build:js",
27
27
  "test": "jest",
28
+ "test:coverage": "jest --coverage",
28
29
  "prettier": "prettier --write --ignore-unknown **/*.ts"
29
30
  },
30
31
  "repository": {
@@ -30,15 +30,44 @@ export interface VFXBatchSettings {
30
30
  layerMask: number;
31
31
  }
32
32
 
33
+ export interface AdaptivePerformanceOptions {
34
+ targetFrameMs: number;
35
+ minQuality: number;
36
+ maxQuality: number;
37
+ decreaseStep: number;
38
+ increaseStep: number;
39
+ }
40
+
41
+ export interface AdaptivePerformanceState extends AdaptivePerformanceOptions {
42
+ enabled: boolean;
43
+ currentQuality: number;
44
+ lastFrameCpuMs: number;
45
+ }
46
+
33
47
  export class BatchedRenderer extends TransformNode {
34
48
  batches: Array<VFXBatch> = [];
35
49
  systemToBatchIndex: Map<IParticleSystem, number> = new Map<IParticleSystem, number>();
36
50
  depthTexture: BaseTexture | null = null;
51
+ private adaptivePerformanceState: AdaptivePerformanceState = {
52
+ enabled: false,
53
+ targetFrameMs: 16.7,
54
+ minQuality: 0.35,
55
+ maxQuality: 1,
56
+ decreaseStep: 0.08,
57
+ increaseStep: 0.02,
58
+ currentQuality: 1,
59
+ lastFrameCpuMs: 0,
60
+ };
61
+ private lastAppliedQuality = Number.NaN;
37
62
 
38
63
  constructor(name: string, scene: Scene) {
39
64
  super(name, scene);
40
65
  }
41
66
 
67
+ private static clamp(value: number, min: number, max: number): number {
68
+ return Math.max(min, Math.min(max, value));
69
+ }
70
+
42
71
  private static equals(a: StoredBatchSettings, b: VFXBatchSettings): boolean {
43
72
  return (
44
73
  a.materialBlendMode === b.materialBlendMode &&
@@ -62,6 +91,9 @@ export class BatchedRenderer extends TransformNode {
62
91
 
63
92
  addSystem(system: IParticleSystem) {
64
93
  (system as unknown as ParticleSystem)._renderer = this;
94
+ if (this.adaptivePerformanceState.enabled) {
95
+ (system as any).setQualityFactor?.(this.adaptivePerformanceState.currentQuality);
96
+ }
65
97
  const settings = (system as unknown as ParticleSystem).getRendererSettings();
66
98
  for (let i = 0; i < this.batches.length; i++) {
67
99
  if (BatchedRenderer.equals(this.batches[i].settings, settings)) {
@@ -115,13 +147,73 @@ export class BatchedRenderer extends TransformNode {
115
147
  }
116
148
  }
117
149
 
150
+ configureAdaptivePerformance(options: Partial<AdaptivePerformanceOptions> = {}) {
151
+ const state = this.adaptivePerformanceState;
152
+ const minQuality = BatchedRenderer.clamp(options.minQuality ?? state.minQuality, 0.05, 1);
153
+ const maxQuality = BatchedRenderer.clamp(options.maxQuality ?? state.maxQuality, minQuality, 1);
154
+ state.targetFrameMs = Math.max(options.targetFrameMs ?? state.targetFrameMs, 0.1);
155
+ state.minQuality = minQuality;
156
+ state.maxQuality = maxQuality;
157
+ state.decreaseStep = Math.max(options.decreaseStep ?? state.decreaseStep, 0.001);
158
+ state.increaseStep = Math.max(options.increaseStep ?? state.increaseStep, 0.001);
159
+ state.currentQuality = BatchedRenderer.clamp(state.currentQuality, state.minQuality, state.maxQuality);
160
+ state.enabled = true;
161
+ this.lastAppliedQuality = Number.NaN;
162
+ }
163
+
164
+ disableAdaptivePerformance(resetQuality = true) {
165
+ this.adaptivePerformanceState.enabled = false;
166
+ if (resetQuality) {
167
+ this.adaptivePerformanceState.currentQuality = this.adaptivePerformanceState.maxQuality;
168
+ this.applyAdaptiveQuality();
169
+ }
170
+ }
171
+
172
+ getAdaptivePerformanceState(): AdaptivePerformanceState {
173
+ return {...this.adaptivePerformanceState};
174
+ }
175
+
176
+ private applyAdaptiveQuality() {
177
+ const quality = this.adaptivePerformanceState.currentQuality;
178
+ if (quality === this.lastAppliedQuality) {
179
+ return;
180
+ }
181
+ for (const ps of this.systemToBatchIndex.keys()) {
182
+ (ps as any).setQualityFactor?.(quality);
183
+ }
184
+ this.lastAppliedQuality = quality;
185
+ }
186
+
118
187
  update(delta: number) {
119
- this.systemToBatchIndex.forEach((_value, ps) => {
188
+ const adaptiveState = this.adaptivePerformanceState;
189
+ const frameStart = adaptiveState.enabled ? performance.now() : 0;
190
+ if (adaptiveState.enabled) {
191
+ this.applyAdaptiveQuality();
192
+ }
193
+ for (const ps of this.systemToBatchIndex.keys()) {
120
194
  (ps as any).update(delta);
121
- });
195
+ }
122
196
  for (let i = 0; i < this.batches.length; i++) {
123
197
  this.batches[i].update();
124
198
  }
199
+ if (!adaptiveState.enabled) {
200
+ return;
201
+ }
202
+
203
+ adaptiveState.lastFrameCpuMs = performance.now() - frameStart;
204
+ if (adaptiveState.lastFrameCpuMs > adaptiveState.targetFrameMs) {
205
+ adaptiveState.currentQuality = BatchedRenderer.clamp(
206
+ adaptiveState.currentQuality - adaptiveState.decreaseStep,
207
+ adaptiveState.minQuality,
208
+ adaptiveState.maxQuality
209
+ );
210
+ } else {
211
+ adaptiveState.currentQuality = BatchedRenderer.clamp(
212
+ adaptiveState.currentQuality + adaptiveState.increaseStep,
213
+ adaptiveState.minQuality,
214
+ adaptiveState.maxQuality
215
+ );
216
+ }
125
217
  }
126
218
 
127
219
  dispose(): void {
@@ -41,6 +41,7 @@ import {
41
41
  import {ParticleEmitter} from './ParticleEmitter';
42
42
  import {RenderMode} from './VFXBatch';
43
43
  import {BatchedRenderer, VFXBatchSettings} from './BatchedRenderer';
44
+ import {ensureTriangleIndices} from './geometryUtil';
44
45
 
45
46
  export interface BurstParameters {
46
47
  time: number;
@@ -206,6 +207,8 @@ export class ParticleSystem implements IParticleSystem {
206
207
  private listeners: {[event: string]: Array<(event: ParticleSystemEvent) => void>} = {};
207
208
  private readonly layerMaskProxy: {mask: number};
208
209
  private materialRef: any = null;
210
+ private qualityFactor = 1;
211
+ private useFastTrailHistory = true;
209
212
  /** @internal **/
210
213
  _renderer?: BatchedRenderer;
211
214
 
@@ -456,6 +459,7 @@ export class ParticleSystem implements IParticleSystem {
456
459
 
457
460
  this.emissionBursts.forEach((burst) => burst.count.startGen(this.memory));
458
461
  this.emissionOverDistance.startGen(this.memory);
462
+ this.refreshTrailHistoryMode();
459
463
 
460
464
  this.emitEnded = false;
461
465
  this.markForDestroy = false;
@@ -474,6 +478,7 @@ export class ParticleSystem implements IParticleSystem {
474
478
  layerMask?: number;
475
479
  } = {}
476
480
  ) {
481
+ /* istanbul ignore if -- rendererSettings is always assigned in the constructor before any code path can call this private method */
477
482
  if (!this.rendererSettings) {
478
483
  return;
479
484
  }
@@ -534,17 +539,93 @@ export class ParticleSystem implements IParticleSystem {
534
539
  play() { this.paused = false; }
535
540
  stop() { this.restart(); this.pause(); }
536
541
 
542
+ private refreshTrailHistoryMode() {
543
+ this.useFastTrailHistory = !this.behaviors.some((behavior) => (behavior as any).type === 'WidthOverLength');
544
+ }
545
+
546
+ private ensureTrailHistoryCapacity(particle: TrailParticle, capacity: number) {
547
+ const trailAny = particle as any;
548
+ if (trailAny._trailHistoryCapacity === capacity && trailAny._trailHistoryPositions) {
549
+ return;
550
+ }
551
+ trailAny._trailHistoryCapacity = capacity;
552
+ trailAny._trailHistoryPositions = new Float32Array(capacity * 3);
553
+ trailAny._trailHistorySizes = new Float32Array(capacity);
554
+ trailAny._trailHistoryColors = new Float32Array(capacity * 4);
555
+ trailAny._trailHistoryHead = 0;
556
+ trailAny._trailHistoryCount = 0;
557
+ }
558
+
559
+ private resetTrailHistory(particle: TrailParticle) {
560
+ const trailAny = particle as any;
561
+ trailAny._trailHistoryHead = 0;
562
+ trailAny._trailHistoryCount = 0;
563
+ }
564
+
565
+ private getTrailHistoryCount(particle: TrailParticle): number {
566
+ const trailAny = particle as any;
567
+ const fastHistoryCount = trailAny._trailHistoryCount;
568
+ if (typeof fastHistoryCount === 'number') {
569
+ return fastHistoryCount;
570
+ }
571
+ return particle.previous.length;
572
+ }
573
+
574
+ private updateFastTrailHistory(particle: TrailParticle) {
575
+ const trailAny = particle as any;
576
+ const capacity = Math.max(1, Math.ceil(particle.length));
577
+ this.ensureTrailHistoryCapacity(particle, capacity);
578
+ let head = trailAny._trailHistoryHead as number;
579
+ let count = trailAny._trailHistoryCount as number;
580
+ const positions = trailAny._trailHistoryPositions as Float32Array;
581
+ const sizes = trailAny._trailHistorySizes as Float32Array;
582
+ const colors = trailAny._trailHistoryColors as Float32Array;
583
+
584
+ if (particle.age <= particle.life) {
585
+ const posIndex = head * 3;
586
+ positions[posIndex] = particle.position.x;
587
+ positions[posIndex + 1] = particle.position.y;
588
+ positions[posIndex + 2] = particle.position.z;
589
+ sizes[head] = particle.size.x;
590
+ const colorIndex = head * 4;
591
+ colors[colorIndex] = particle.color.x;
592
+ colors[colorIndex + 1] = particle.color.y;
593
+ colors[colorIndex + 2] = particle.color.z;
594
+ colors[colorIndex + 3] = particle.color.w;
595
+ head = (head + 1) % capacity;
596
+ if (count < capacity) {
597
+ count++;
598
+ }
599
+ } else if (count > 0) {
600
+ count--;
601
+ }
602
+
603
+ trailAny._trailHistoryHead = head;
604
+ trailAny._trailHistoryCount = count;
605
+ }
606
+
607
+ setQualityFactor(qualityFactor: number) {
608
+ this.qualityFactor = Math.max(0.1, Math.min(1, qualityFactor));
609
+ }
610
+
537
611
  private spawn(count: number, emissionState: EmissionState, matrix: Matrix4) {
538
612
  tempQ.setFromRotationMatrix(matrix);
539
613
  const translation = tempV;
540
614
  const scale = tempV2;
541
615
  matrix.decompose(translation, tempQ, scale);
616
+ const behaviors = this.behaviors;
617
+ const behaviorCount = behaviors.length;
618
+ const renderMode = this.rendererSettings.renderMode;
619
+ const isTrailMode = renderMode === RenderMode.Trail;
620
+ const timeRatio = emissionState.time / this.duration;
621
+ const trailSettings = isTrailMode ? (this.rendererEmitterSettings as TrailSettings) : undefined;
622
+ const followLocalOrigin = Boolean(trailSettings?.followLocalOrigin);
542
623
 
543
624
  for (let i = 0; i < count; i++) {
544
625
  emissionState.burstParticleIndex = i;
545
626
  this.particleNum++;
546
- while (this.particles.length < this.particleNum) {
547
- if (this.rendererSettings.renderMode === RenderMode.Trail) {
627
+ if (this.particles.length < this.particleNum) {
628
+ if (isTrailMode) {
548
629
  this.particles.push(new TrailParticle());
549
630
  } else {
550
631
  this.particles.push(new SpriteParticle());
@@ -557,15 +638,15 @@ export class ParticleSystem implements IParticleSystem {
557
638
  this.startColor.genColor(particle.memory, particle.startColor, this.emissionState.time);
558
639
  particle.color.copy(particle.startColor);
559
640
  this.startSpeed.startGen(particle.memory);
560
- particle.startSpeed = this.startSpeed.genValue(particle.memory, emissionState.time / this.duration);
641
+ particle.startSpeed = this.startSpeed.genValue(particle.memory, timeRatio);
561
642
  this.startLife.startGen(particle.memory);
562
- particle.life = this.startLife.genValue(particle.memory, emissionState.time / this.duration);
643
+ particle.life = this.startLife.genValue(particle.memory, timeRatio);
563
644
  particle.age = 0;
564
645
  this.startSize.startGen(particle.memory);
565
646
  if (this.startSize.type === 'vec3function') {
566
- (this.startSize as Vector3Generator).genValue(particle.memory, particle.startSize, emissionState.time / this.duration);
647
+ (this.startSize as Vector3Generator).genValue(particle.memory, particle.startSize, timeRatio);
567
648
  } else {
568
- const size = (this.startSize as FunctionValueGenerator).genValue(particle.memory, emissionState.time / this.duration);
649
+ const size = (this.startSize as FunctionValueGenerator).genValue(particle.memory, timeRatio);
569
650
  particle.startSize.set(size, size, size);
570
651
  }
571
652
  this.startTileIndex.startGen(particle.memory);
@@ -581,33 +662,40 @@ export class ParticleSystem implements IParticleSystem {
581
662
  ) {
582
663
  const sprite = particle as SpriteParticle;
583
664
  this.startRotation.startGen(particle.memory);
584
- if (this.rendererSettings.renderMode === RenderMode.Mesh) {
665
+ if (renderMode === RenderMode.Mesh) {
585
666
  if (!(sprite.rotation instanceof Quaternion)) {
586
667
  sprite.rotation = new Quaternion();
587
668
  }
588
669
  if (this.startRotation.type === 'rotation') {
589
- this.startRotation.genValue(particle.memory, sprite.rotation as Quaternion, 1, emissionState.time / this.duration);
670
+ this.startRotation.genValue(particle.memory, sprite.rotation as Quaternion, 1, timeRatio);
590
671
  } else {
591
- (sprite.rotation as Quaternion).setFromAxisAngle(UP, this.startRotation.genValue(sprite.memory, emissionState.time / this.duration));
672
+ (sprite.rotation as Quaternion).setFromAxisAngle(UP, this.startRotation.genValue(sprite.memory, timeRatio));
592
673
  }
593
674
  } else {
594
675
  if (this.startRotation.type === 'rotation') {
595
676
  sprite.rotation = 0;
596
677
  } else {
597
- sprite.rotation = this.startRotation.genValue(sprite.memory, emissionState.time / this.duration);
678
+ sprite.rotation = this.startRotation.genValue(sprite.memory, timeRatio);
598
679
  }
599
680
  }
600
- } else if (this.rendererSettings.renderMode === RenderMode.Trail) {
681
+ } else if (isTrailMode && trailSettings) {
601
682
  const trail = particle as TrailParticle;
602
- (this.rendererEmitterSettings as TrailSettings).startLength.startGen(trail.memory);
603
- trail.length = (this.rendererEmitterSettings as TrailSettings).startLength.genValue(trail.memory, emissionState.time / this.duration);
683
+ trailSettings.startLength.startGen(trail.memory);
684
+ trail.length = trailSettings.startLength.genValue(trail.memory, timeRatio);
685
+ if (this.useFastTrailHistory) {
686
+ this.ensureTrailHistoryCapacity(trail, Math.max(1, Math.ceil(trail.length)));
687
+ this.resetTrailHistory(trail);
688
+ }
604
689
  }
605
690
 
606
691
  this.emitterShape.initialize(particle, emissionState);
607
692
 
608
- if (this.rendererSettings.renderMode === RenderMode.Trail && (this.rendererEmitterSettings as TrailSettings).followLocalOrigin) {
693
+ if (isTrailMode && followLocalOrigin) {
609
694
  const trail = particle as TrailParticle;
610
- trail.localPosition = new Vector3().copy(trail.position);
695
+ if (!trail.localPosition) {
696
+ trail.localPosition = new Vector3();
697
+ }
698
+ trail.localPosition.copy(trail.position);
611
699
  }
612
700
 
613
701
  if (this.worldSpace) {
@@ -624,8 +712,8 @@ export class ParticleSystem implements IParticleSystem {
624
712
  }
625
713
  }
626
714
 
627
- for (let j = 0; j < this.behaviors.length; j++) {
628
- this.behaviors[j].initialize(particle, this);
715
+ for (let j = 0; j < behaviorCount; j++) {
716
+ behaviors[j].initialize(particle, this);
629
717
  }
630
718
  }
631
719
  }
@@ -697,49 +785,66 @@ export class ParticleSystem implements IParticleSystem {
697
785
  this.emit(delta, this.emissionState, this.emitter.matrixWorld);
698
786
  }
699
787
 
788
+ const particles = this.particles;
789
+ const behaviorCount = this.behaviors.length;
790
+ const particleCount = this.particleNum;
700
791
  this.emitterShape.update(this, delta);
701
- for (let j = 0; j < this.behaviors.length; j++) {
702
- this.behaviors[j].frameUpdate(delta);
703
- for (let i = 0; i < this.particleNum; i++) {
704
- if (!this.particles[i].died) {
705
- this.behaviors[j].update(this.particles[i], delta);
792
+ for (let j = 0; j < behaviorCount; j++) {
793
+ const behavior = this.behaviors[j];
794
+ behavior.frameUpdate(delta);
795
+ for (let i = 0; i < particleCount; i++) {
796
+ const particle = particles[i];
797
+ if (!particle.died) {
798
+ behavior.update(particle, delta);
706
799
  }
707
800
  }
708
801
  }
709
802
 
710
- for (let i = 0; i < this.particleNum; i++) {
711
- if (
712
- (this.rendererEmitterSettings as TrailSettings).followLocalOrigin &&
713
- (this.particles[i] as TrailParticle).localPosition
714
- ) {
715
- this.particles[i].position.copy((this.particles[i] as TrailParticle).localPosition!);
716
- if (this.particles[i].parentMatrix) {
717
- this.particles[i].position.applyMatrix4(this.particles[i].parentMatrix!);
803
+ const followLocalOrigin =
804
+ this.rendererSettings.renderMode === RenderMode.Trail &&
805
+ (this.rendererEmitterSettings as TrailSettings).followLocalOrigin;
806
+ const emitterMatrix = this.emitter.matrixWorld;
807
+ for (let i = 0; i < particleCount; i++) {
808
+ const particle = particles[i];
809
+ if (followLocalOrigin && (particle as TrailParticle).localPosition) {
810
+ particle.position.copy((particle as TrailParticle).localPosition!);
811
+ if (particle.parentMatrix) {
812
+ particle.position.applyMatrix4(particle.parentMatrix);
718
813
  } else {
719
- this.particles[i].position.applyMatrix4(this.emitter.matrixWorld);
814
+ particle.position.applyMatrix4(emitterMatrix);
720
815
  }
721
816
  } else {
722
- this.particles[i].position.addScaledVector(this.particles[i].velocity, delta * this.particles[i].speedModifier);
817
+ particle.position.addScaledVector(particle.velocity, delta * particle.speedModifier);
723
818
  }
724
- this.particles[i].age += delta;
819
+ particle.age += delta;
725
820
  }
726
821
 
727
822
  if (this.rendererSettings.renderMode === RenderMode.Trail) {
728
- for (let i = 0; i < this.particleNum; i++) {
729
- (this.particles[i] as TrailParticle).update();
823
+ for (let i = 0; i < particleCount; i++) {
824
+ const trailParticle = particles[i] as TrailParticle;
825
+ if (this.useFastTrailHistory) {
826
+ this.updateFastTrailHistory(trailParticle);
827
+ } else {
828
+ trailParticle.update();
829
+ }
730
830
  }
731
831
  }
732
832
 
733
- for (let i = 0; i < this.particleNum; i++) {
734
- const particle = this.particles[i];
735
- if (particle.died && (!(particle instanceof TrailParticle) || particle.previous.length === 0)) {
736
- this.particles[i] = this.particles[this.particleNum - 1];
737
- this.particles[this.particleNum - 1] = particle;
738
- this.particleNum--;
833
+ let liveParticleCount = this.particleNum;
834
+ for (let i = 0; i < liveParticleCount; i++) {
835
+ const particle = particles[i];
836
+ if (
837
+ particle.died &&
838
+ (!(particle instanceof TrailParticle) || this.getTrailHistoryCount(particle as TrailParticle) === 0)
839
+ ) {
840
+ particles[i] = particles[liveParticleCount - 1];
841
+ particles[liveParticleCount - 1] = particle;
842
+ liveParticleCount--;
739
843
  i--;
740
844
  this.fire({type: 'particleDied', particleSystem: this, particle: particle});
741
845
  }
742
846
  }
847
+ this.particleNum = liveParticleCount;
743
848
  }
744
849
 
745
850
  emit(delta: number, emissionState: EmissionState, emitterMatrix: Matrix4) {
@@ -756,17 +861,22 @@ export class ParticleSystem implements IParticleSystem {
756
861
  }
757
862
 
758
863
  this.normalMatrix.getNormalMatrix(emitterMatrix);
864
+ const emissionBursts = this.emissionBursts;
865
+ const emissionBurstCount = emissionBursts.length;
866
+ const qualityFactor = this.qualityFactor;
759
867
 
760
868
  const totalSpawn = Math.ceil(emissionState.waitEmiting);
761
869
  this.spawn(totalSpawn, emissionState, emitterMatrix);
762
870
  emissionState.waitEmiting -= totalSpawn;
763
871
 
764
872
  while (
765
- emissionState.burstIndex < this.emissionBursts.length &&
766
- this.emissionBursts[emissionState.burstIndex].time <= emissionState.time
873
+ emissionState.burstIndex < emissionBurstCount &&
874
+ emissionBursts[emissionState.burstIndex].time <= emissionState.time
767
875
  ) {
768
- if (Math.random() < this.emissionBursts[emissionState.burstIndex].probability) {
769
- const count = this.emissionBursts[emissionState.burstIndex].count.genValue(this.memory, this.time);
876
+ const burst = emissionBursts[emissionState.burstIndex];
877
+ if (Math.random() < burst.probability) {
878
+ const rawCount = burst.count.genValue(this.memory, this.time);
879
+ const count = qualityFactor >= 0.999 ? rawCount : Math.floor(rawCount * qualityFactor);
770
880
  emissionState.isBursting = true;
771
881
  emissionState.burstParticleCount = count;
772
882
  this.spawn(count, emissionState, emitterMatrix);
@@ -776,12 +886,14 @@ export class ParticleSystem implements IParticleSystem {
776
886
  }
777
887
 
778
888
  if (!this.emitEnded) {
779
- emissionState.waitEmiting += delta * this.emissionOverTime.genValue(this.memory, emissionState.time / this.duration);
889
+ const timeRatio = emissionState.time / this.duration;
890
+ emissionState.waitEmiting += delta * this.emissionOverTime.genValue(this.memory, timeRatio) * qualityFactor;
780
891
 
781
- if (emissionState.previousWorldPos != undefined) {
892
+ const previousWorldPos = emissionState.previousWorldPos;
893
+ if (previousWorldPos !== undefined) {
782
894
  this.temp.set(emitterMatrix.elements[12], emitterMatrix.elements[13], emitterMatrix.elements[14]);
783
- emissionState.travelDistance += emissionState.previousWorldPos.distanceTo(this.temp);
784
- const emitPerMeter = this.emissionOverDistance.genValue(this.memory, emissionState.time / this.duration);
895
+ emissionState.travelDistance += previousWorldPos.distanceTo(this.temp);
896
+ const emitPerMeter = this.emissionOverDistance.genValue(this.memory, timeRatio) * qualityFactor;
785
897
  if (emissionState.travelDistance * emitPerMeter > 0) {
786
898
  const count = Math.floor(emissionState.travelDistance * emitPerMeter);
787
899
  emissionState.travelDistance -= count / emitPerMeter;
@@ -1031,13 +1143,18 @@ export class ParticleSystem implements IParticleSystem {
1031
1143
  uvs?: Float32Array;
1032
1144
  normals?: Float32Array;
1033
1145
  } {
1034
- if (entry?.positions && entry?.indices) {
1146
+ if (entry?.positions) {
1147
+ const positions =
1148
+ entry.positions instanceof Float32Array ? entry.positions : new Float32Array(entry.positions);
1149
+ const rawIndices =
1150
+ entry.indices instanceof Uint16Array || entry.indices instanceof Uint32Array
1151
+ ? entry.indices
1152
+ : entry.indices
1153
+ ? new Uint32Array(entry.indices)
1154
+ : undefined;
1035
1155
  return {
1036
- positions: entry.positions instanceof Float32Array ? entry.positions : new Float32Array(entry.positions),
1037
- indices:
1038
- entry.indices instanceof Uint16Array || entry.indices instanceof Uint32Array
1039
- ? entry.indices
1040
- : new Uint32Array(entry.indices),
1156
+ positions,
1157
+ indices: ensureTriangleIndices(positions, rawIndices),
1041
1158
  uvs: entry.uvs
1042
1159
  ? entry.uvs instanceof Float32Array
1043
1160
  ? entry.uvs
@@ -1052,11 +1169,16 @@ export class ParticleSystem implements IParticleSystem {
1052
1169
  }
1053
1170
 
1054
1171
  const data = entry?.data ?? entry;
1055
- if (data?.attributes?.position?.array && data?.index?.array) {
1056
- const indexType = data.index.type === 'Uint16Array' ? Uint16Array : Uint32Array;
1172
+ if (data?.attributes?.position?.array) {
1173
+ const positions = new Float32Array(data.attributes.position.array);
1174
+ let indices: Uint32Array | Uint16Array | undefined;
1175
+ if (data.index?.array) {
1176
+ const indexType = data.index.type === 'Uint16Array' ? Uint16Array : Uint32Array;
1177
+ indices = new indexType(data.index.array);
1178
+ }
1057
1179
  return {
1058
- positions: new Float32Array(data.attributes.position.array),
1059
- indices: new indexType(data.index.array),
1180
+ positions,
1181
+ indices: ensureTriangleIndices(positions, indices),
1060
1182
  uvs: data.attributes.uv?.array ? new Float32Array(data.attributes.uv.array) : undefined,
1061
1183
  normals: data.attributes.normal?.array ? new Float32Array(data.attributes.normal.array) : undefined,
1062
1184
  };
@@ -1071,6 +1193,7 @@ export class ParticleSystem implements IParticleSystem {
1071
1193
 
1072
1194
  addBehavior(behavior: Behavior) {
1073
1195
  this.behaviors.push(behavior);
1196
+ this.refreshTrailHistoryMode();
1074
1197
  }
1075
1198
 
1076
1199
  getRendererSettings(): VFXBatchSettings {
@@ -13,6 +13,7 @@ import {
13
13
  import {ParticleSystem} from './ParticleSystem';
14
14
  import {ParticleEmitter} from './ParticleEmitter';
15
15
  import {QuarksPrefab} from './QuarksPrefab';
16
+ import {ensureTriangleIndices} from './geometryUtil';
16
17
 
17
18
  export interface QuarksLoaderOptions {
18
19
  baseUrl?: string;
@@ -219,6 +220,8 @@ export class QuarksLoader {
219
220
  }
220
221
  }
221
222
 
223
+ indices = ensureTriangleIndices(positions, indices);
224
+
222
225
  return {positions, indices, uvs, normals};
223
226
  }
224
227