diff --git a/packages/dev/core/src/Particles/solidParticle.ts b/packages/dev/core/src/Particles/solidParticle.ts index d92d63bd38b..e5347de4ff2 100644 --- a/packages/dev/core/src/Particles/solidParticle.ts +++ b/packages/dev/core/src/Particles/solidParticle.ts @@ -67,6 +67,15 @@ export class SolidParticle { * Is the particle visible or not ? */ public isVisible: boolean = true; + /** + * Defines how long will the life of the particle be. + * Set to Infinity for particles that should never die (default behavior for SolidParticleSystem). + */ + public lifeTime = Infinity; + /** + * The current age of the particle. + */ + public age = 0; /** * Index of this particle in the global "positions" array (Internal use) * @internal @@ -223,6 +232,8 @@ export class SolidParticle { target.isVisible = this.isVisible; target.parentId = this.parentId; target.cullingStrategy = this.cullingStrategy; + target.lifeTime = this.lifeTime; + target.age = this.age; if (this.materialIndex !== null) { target.materialIndex = this.materialIndex; } diff --git a/packages/dev/core/src/Particles/solidParticleSystem.ts b/packages/dev/core/src/Particles/solidParticleSystem.ts index fa9a0a8a10b..e58d170e507 100644 --- a/packages/dev/core/src/Particles/solidParticleSystem.ts +++ b/packages/dev/core/src/Particles/solidParticleSystem.ts @@ -7,6 +7,7 @@ import { Mesh } from "../Meshes/mesh"; import { CreateDisc } from "../Meshes/Builders/discBuilder"; import { EngineStore } from "../Engines/engineStore"; import type { Scene, IDisposable } from "../scene"; +import type { Observer } from "../Misc/observable"; import { DepthSortedParticle, SolidParticle, ModelShape, SolidParticleVertex } from "./solidParticle"; import type { TargetCamera } from "../Cameras/targetCamera"; import { BoundingInfo } from "../Culling/boundingInfo"; @@ -152,6 +153,15 @@ export class SolidParticleSystem implements IDisposable { protected _autoUpdateSubMeshes: boolean = false; protected _tmpVertex: SolidParticleVertex; protected _recomputeInvisibles: boolean = false; + protected _started: boolean = false; + protected _stopped: boolean = false; + protected _onBeforeRenderObserver: Nullable> = null; + /** + * The overall motion speed (0.01 is default update speed, faster updates = faster animation) + */ + public updateSpeed = 0.01; + /** @internal */ + protected _scaledUpdateSpeed: number; /** * Creates a SPS (Solid Particle System) object. @@ -1171,9 +1181,23 @@ export class SolidParticleSystem implements IDisposable { colorIndex = vpos * 4; uvIndex = vpos * 2; + // Calculate scaled update speed based on animation ratio (for FPS independence) + if (this._started && !this._stopped) { + this._scaledUpdateSpeed = this.updateSpeed * (this._scene?.getAnimationRatio() || 1); + } + for (let p = start; p <= end; p++) { const particle = this.particles[p]; + // Update particle age and check lifetime + if (this._started && !this._stopped) { + particle.age += this._scaledUpdateSpeed; + // Only check lifetime if it's finite (Infinity means particle never dies) + if (isFinite(particle.lifeTime) && particle.age >= particle.lifeTime) { + particle.alive = false; + } + } + // call to custom user function to update the particle properties this.updateParticle(particle); @@ -1538,6 +1562,7 @@ export class SolidParticleSystem implements IDisposable { * Disposes the SPS. */ public dispose(): void { + this.stop(); this.mesh.dispose(); this.vars = null; // drop references to internal big arrays for the GC @@ -2055,4 +2080,69 @@ export class SolidParticleSystem implements IDisposable { */ // eslint-disable-next-line @typescript-eslint/no-unused-vars public afterUpdateParticles(start?: number, stop?: number, update?: boolean): void {} + + /** + * Starts the particle system and begins to emit. + * This will call buildMesh(), initParticles(), setParticles() and register the update loop. + * @param delay defines the delay in milliseconds before starting the system (0 by default) + */ + public start(delay = 0): void { + if (this._started) { + return; + } + + if (delay > 0) { + setTimeout(() => { + this.start(0); + }, delay); + return; + } + + this.buildMesh(); + this.initParticles(); + this.setParticles(); + + this._started = true; + this._stopped = false; + + // Register update loop + if (this._scene) { + this._onBeforeRenderObserver = this._scene.onBeforeRenderObservable.add(() => { + if (this._started && !this._stopped) { + this.setParticles(); + } + }); + } + } + + /** + * Stops the particle system. + */ + public stop(): void { + if (this._stopped) { + return; + } + + this._stopped = true; + + // Unregister update loop + if (this._onBeforeRenderObserver && this._scene) { + this._scene.onBeforeRenderObservable.remove(this._onBeforeRenderObserver); + this._onBeforeRenderObserver = null; + } + } + + /** + * Gets if the particle system is started + */ + public get started(): boolean { + return this._started; + } + + /** + * Gets if the particle system is stopped + */ + public get stopped(): boolean { + return this._stopped; + } }