UNPKG

@babylonjs/core

Version:

Getting started? Play directly with the Babylon.js API using our [playground](https://playground.babylonjs.com/). It also contains a lot of samples to learn how to use it.

1,242 lines (1,241 loc) 77.1 kB
import { FactorGradient, ColorGradient, Color3Gradient, GradientHelper } from "../Misc/gradients.js"; import { Observable } from "../Misc/observable.js"; import { Vector3, Matrix, TmpVectors } from "../Maths/math.vector.js"; import { VertexBuffer, Buffer } from "../Buffers/buffer.js"; import { RawTexture } from "../Materials/Textures/rawTexture.js"; import { EngineStore } from "../Engines/engineStore.js"; import { BaseParticleSystem } from "./baseParticleSystem.js"; import { Particle } from "./particle.js"; import { DrawWrapper } from "../Materials/drawWrapper.js"; import { Color4, Color3, TmpColors } from "../Maths/math.color.js"; import "../Engines/Extensions/engine.alpha.js"; import { AddClipPlaneUniforms, PrepareStringDefinesForClipPlanes, BindClipPlane } from "../Materials/clipPlaneMaterialHelper.js"; import { BindFogParameters, BindLogDepth } from "../Materials/materialHelper.functions.js"; import { BoxParticleEmitter } from "./EmitterTypes/boxParticleEmitter.js"; import { Lerp } from "../Maths/math.scalar.functions.js"; import { PrepareSamplersForImageProcessing, PrepareUniformsForImageProcessing } from "../Materials/imageProcessingConfiguration.functions.js"; import { _CreateAngleData, _CreateAngleGradientsData, _CreateColorData, _CreateColorDeadData, _CreateColorGradientsData, _CreateCustomDirectionData, _CreateCustomPositionData, _CreateDirectionData, _CreateDragData, _CreateEmitPowerData, _CreateIsLocalData, _CreateLifeGradientsData, _CreateLifetimeData, _CreateLimitVelocityGradients, _CreateNoiseData, _CreatePositionData, _CreateRampData, _CreateSheetData, _CreateSizeData, _CreateSizeGradientsData, _CreateStartSizeGradientsData, _CreateVelocityGradients, _ProcessAngularSpeed, _ProcessAngularSpeedGradients, _ProcessColor, _ProcessColorGradients, _ProcessDirection, _ProcessDragGradients, _ProcessGravity, _ProcessLimitVelocityGradients, _ProcessNoise, _ProcessPosition, _ProcessRemapGradients, _ProcessSizeGradients, _ProcessVelocityGradients, } from "./thinParticleSystem.function.js"; import { _ConnectAfter, _ConnectBefore, _RemoveFromQueue } from "./Queue/executionQueue.js"; /** * This represents a thin particle system in Babylon. * Particles are often small sprites used to simulate hard-to-reproduce phenomena like fire, smoke, water, or abstract visual effects like magic glitter and faery dust. * Particles can take different shapes while emitted like box, sphere, cone or you can write your custom function. * This thin version contains a limited subset of the total features in order to provide users with a way to get particles but with a smaller footprint * @example https://doc.babylonjs.com/features/featuresDeepDive/particles/particle_system/particle_system_intro */ export class ThinParticleSystem extends BaseParticleSystem { /** * This function can be defined to specify initial direction for every new particle. * It by default use the emitterType defined function */ get startDirectionFunction() { return this._startDirectionFunction; } set startDirectionFunction(value) { if (this._startDirectionFunction === value) { return; } this._startDirectionFunction = value; if (value) { this._directionProcessing.process = _CreateCustomDirectionData; } else { this._directionProcessing.process = _CreateDirectionData; } } /** * This function can be defined to specify initial position for every new particle. * It by default use the emitterType defined function */ get startPositionFunction() { return this._startPositionFunction; } set startPositionFunction(value) { if (this._startPositionFunction === value) { return; } this._startPositionFunction = value; if (value) { this._positionCreation.process = _CreateCustomPositionData; } else { this._positionCreation.process = _CreatePositionData; } } /** * Sets a callback that will be triggered when the system is disposed */ set onDispose(callback) { if (this._onDisposeObserver) { this.onDisposeObservable.remove(this._onDisposeObserver); } this._onDisposeObserver = this.onDisposeObservable.add(callback); } /** * Gets a boolean indicating that the particle system was disposed */ get isDisposed() { return this._isDisposed; } /** Gets or sets a boolean indicating that ramp gradients must be used * @see https://doc.babylonjs.com/features/featuresDeepDive/particles/particle_system/particle_system_intro#ramp-gradients */ get useRampGradients() { return this._useRampGradients; } set useRampGradients(value) { if (this._useRampGradients === value) { return; } this._useRampGradients = value; this._resetEffect(); if (value) { this._rampCreation = { process: _CreateRampData, previousItem: null, nextItem: null, }; _ConnectAfter(this._rampCreation, this._colorDeadCreation); this._remapGradientProcessing = { process: _ProcessRemapGradients, previousItem: null, nextItem: null, }; _ConnectAfter(this._remapGradientProcessing, this._gravityProcessing); } else { _RemoveFromQueue(this._rampCreation); _RemoveFromQueue(this._remapGradientProcessing); } } /** * Specifies if the particles are updated in emitter local space or world space */ get isLocal() { return this._isLocal; } set isLocal(value) { if (this._isLocal === value) { return; } this._isLocal = value; if (value) { this._isLocalCreation = { process: _CreateIsLocalData, previousItem: null, nextItem: null, }; _ConnectAfter(this._isLocalCreation, this._positionCreation); } else { _RemoveFromQueue(this._isLocalCreation); } } /** * Gets the current list of active particles */ get particles() { return this._particles; } /** * Gets the shader language used in this material. */ get shaderLanguage() { return this._shaderLanguage; } /** @internal */ get _isAnimationSheetEnabled() { return this._animationSheetEnabled; } set _isAnimationSheetEnabled(value) { if (this._animationSheetEnabled === value) { return; } this._animationSheetEnabled = value; if (value) { this._sheetCreation = { process: _CreateSheetData, previousItem: null, nextItem: null, }; _ConnectAfter(this._sheetCreation, this._colorDeadCreation); } else { _RemoveFromQueue(this._sheetCreation); } this._reset(); } /** * Gets the number of particles active at the same time. * @returns The number of active particles. */ getActiveCount() { return this._particles.length; } /** * Returns the string "ParticleSystem" * @returns a string containing the class name */ getClassName() { return "ParticleSystem"; } /** * Gets a boolean indicating that the system is stopping * @returns true if the system is currently stopping */ isStopping() { return this._stopped && this.isAlive(); } /** * Gets the custom effect used to render the particles * @param blendMode Blend mode for which the effect should be retrieved * @returns The effect */ getCustomEffect(blendMode = 0) { return this._customWrappers[blendMode]?.effect ?? this._customWrappers[0].effect; } _getCustomDrawWrapper(blendMode = 0) { return this._customWrappers[blendMode] ?? this._customWrappers[0]; } /** * Sets the custom effect used to render the particles * @param effect The effect to set * @param blendMode Blend mode for which the effect should be set */ setCustomEffect(effect, blendMode = 0) { this._customWrappers[blendMode] = new DrawWrapper(this._engine); this._customWrappers[blendMode].effect = effect; if (this._customWrappers[blendMode].drawContext) { this._customWrappers[blendMode].drawContext.useInstancing = this._useInstancing; } } /** * Observable that will be called just before the particles are drawn */ get onBeforeDrawParticlesObservable() { if (!this._onBeforeDrawParticlesObservable) { this._onBeforeDrawParticlesObservable = new Observable(); } return this._onBeforeDrawParticlesObservable; } /** * Gets the name of the particle vertex shader */ get vertexShaderName() { return "particles"; } /** * Gets the vertex buffers used by the particle system */ get vertexBuffers() { return this._vertexBuffers; } /** * Gets the index buffer used by the particle system (or null if no index buffer is used (if _useInstancing=true)) */ get indexBuffer() { return this._indexBuffer; } get noiseTexture() { return this._noiseTexture; } set noiseTexture(value) { if (this.noiseTexture === value) { return; } this._noiseTexture = value; if (!value) { _RemoveFromQueue(this._noiseCreation); _RemoveFromQueue(this._noiseProcessing); return; } this._noiseCreation = { process: _CreateNoiseData, previousItem: null, nextItem: null, }; _ConnectAfter(this._noiseCreation, this._colorDeadCreation); this._noiseProcessing = { process: _ProcessNoise, previousItem: null, nextItem: null, }; _ConnectAfter(this._noiseProcessing, this._positionProcessing); } /** * Instantiates a particle system. * Particles are often small sprites used to simulate hard-to-reproduce phenomena like fire, smoke, water, or abstract visual effects like magic glitter and faery dust. * @param name The name of the particle system * @param capacity The max number of particles alive at the same time * @param sceneOrEngine The scene the particle system belongs to or the engine to use if no scene * @param customEffect a custom effect used to change the way particles are rendered by default * @param isAnimationSheetEnabled Must be true if using a spritesheet to animate the particles texture * @param epsilon Offset used to render the particles * @param noUpdateQueue If true, the particle system will start with an empty update queue */ constructor(name, capacity, sceneOrEngine, customEffect = null, isAnimationSheetEnabled = false, epsilon = 0.01, noUpdateQueue = false) { super(name); /** @internal */ this._emitterInverseWorldMatrix = Matrix.Identity(); this._startDirectionFunction = null; this._startPositionFunction = null; /** * @internal */ this._inheritedVelocityOffset = new Vector3(); /** * An event triggered when the system is disposed */ this.onDisposeObservable = new Observable(); /** * An event triggered when the system is stopped */ this.onStoppedObservable = new Observable(); /** * An event triggered when the system is started */ this.onStartedObservable = new Observable(); /** @internal */ this._noiseTextureSize = null; /** @internal */ this._noiseTextureData = null; this._particles = new Array(); this._stockParticles = new Array(); this._newPartsExcess = 0; this._vertexBuffers = {}; /** @internal */ this._scaledColorStep = new Color4(0, 0, 0, 0); /** @internal */ this._colorDiff = new Color4(0, 0, 0, 0); /** @internal */ this._scaledDirection = Vector3.Zero(); /** @internal */ this._scaledGravity = Vector3.Zero(); this._currentRenderId = -1; this._useInstancing = false; this._isDisposed = false; this._started = false; this._stopped = false; /** @internal */ this._actualFrame = 0; /** @internal */ this._currentEmitRate1 = 0; /** @internal */ this._currentEmitRate2 = 0; /** @internal */ this._currentStartSize1 = 0; /** @internal */ this._currentStartSize2 = 0; /** Indicates that the update of particles is done in the animate function */ this.updateInAnimate = true; this._rawTextureWidth = 256; this._useRampGradients = false; /** @internal */ this._updateQueueStart = null; this._startSizeCreation = null; this._createQueueStart = null; this._isLocal = false; /** Indicates that the particle system is CPU based */ this.isGPU = false; /** Shader language used by the material */ this._shaderLanguage = 0 /* ShaderLanguage.GLSL */; /** @internal */ this._onBeforeDrawParticlesObservable = null; /** @internal */ this._emitFromParticle = (particle) => { // Do nothing }; // start of sub system methods /** * "Recycles" one of the particle by copying it back to the "stock" of particles and removing it from the active list. * Its lifetime will start back at 0. * @param particle */ this.recycleParticle = (particle) => { // move particle from activeParticle list to stock particles const lastParticle = this._particles.pop(); if (lastParticle !== particle) { lastParticle.copyTo(particle); } this._stockParticles.push(lastParticle); }; this._createParticle = () => { let particle; if (this._stockParticles.length !== 0) { particle = this._stockParticles.pop(); particle._reset(); } else { particle = new Particle(this); } this._prepareParticle(particle); return particle; }; /** * Gets or sets a boolean indicating that the particle system is paused (no animation will be done). */ this.paused = false; this._shadersLoaded = false; this._capacity = capacity; this._epsilon = epsilon; if (!sceneOrEngine || sceneOrEngine.getClassName() === "Scene") { this._scene = sceneOrEngine || EngineStore.LastCreatedScene; this._engine = this._scene.getEngine(); this.uniqueId = this._scene.getUniqueId(); this._scene.particleSystems.push(this); } else { this._engine = sceneOrEngine; this.defaultProjectionMatrix = Matrix.PerspectiveFovLH(0.8, 1, 0.1, 100, this._engine.isNDCHalfZRange); } if (this._engine.getCaps().vertexArrayObject) { this._vertexArrayObject = null; } // eslint-disable-next-line @typescript-eslint/no-floating-promises this._initShaderSourceAsync(); // Creation queue this._lifeTimeCreation = { process: _CreateLifetimeData, previousItem: null, nextItem: null, }; this._positionCreation = { process: _CreatePositionData, previousItem: null, nextItem: null, }; _ConnectAfter(this._positionCreation, this._lifeTimeCreation); this._directionCreation = { process: _CreateDirectionData, previousItem: null, nextItem: null, }; _ConnectAfter(this._directionCreation, this._positionCreation); this._emitPowerCreation = { process: _CreateEmitPowerData, previousItem: null, nextItem: null, }; _ConnectAfter(this._emitPowerCreation, this._directionCreation); this._sizeCreation = { process: _CreateSizeData, previousItem: null, nextItem: null, }; _ConnectAfter(this._sizeCreation, this._emitPowerCreation); this._angleCreation = { process: _CreateAngleData, previousItem: null, nextItem: null, }; _ConnectAfter(this._angleCreation, this._sizeCreation); this._colorCreation = { process: _CreateColorData, previousItem: null, nextItem: null, }; _ConnectAfter(this._colorCreation, this._angleCreation); this._colorDeadCreation = { process: _CreateColorDeadData, previousItem: null, nextItem: null, }; _ConnectAfter(this._colorDeadCreation, this._colorCreation); this._createQueueStart = this._lifeTimeCreation; // Processing queue if (!noUpdateQueue) { this._colorProcessing = { process: _ProcessColor, previousItem: null, nextItem: null, }; this._angularSpeedProcessing = { process: _ProcessAngularSpeed, previousItem: null, nextItem: null, }; _ConnectAfter(this._angularSpeedProcessing, this._colorProcessing); this._directionProcessing = { process: _ProcessDirection, previousItem: null, nextItem: null, }; _ConnectAfter(this._directionProcessing, this._angularSpeedProcessing); this._positionProcessing = { process: _ProcessPosition, previousItem: null, nextItem: null, }; _ConnectAfter(this._positionProcessing, this._directionProcessing); this._gravityProcessing = { process: _ProcessGravity, previousItem: null, nextItem: null, }; _ConnectAfter(this._gravityProcessing, this._positionProcessing); this._updateQueueStart = this._colorProcessing; } this._isAnimationSheetEnabled = isAnimationSheetEnabled; // Setup the default processing configuration to the scene. this._attachImageProcessingConfiguration(null); // eslint-disable-next-line @typescript-eslint/naming-convention this._customWrappers = { 0: new DrawWrapper(this._engine) }; this._customWrappers[0].effect = customEffect; this._drawWrappers = []; this._useInstancing = this._engine.getCaps().instancedArrays; this._createIndexBuffer(); this._createVertexBuffers(); // Default emitter type this.particleEmitterType = new BoxParticleEmitter(); // Update this.updateFunction = (particles) => { if (this.noiseTexture) { // We need to get texture data back to CPU this._noiseTextureSize = this.noiseTexture.getSize(); // eslint-disable-next-line @typescript-eslint/no-floating-promises, github/no-then this.noiseTexture.getContent()?.then((data) => { this._noiseTextureData = data; }); } const sameParticleArray = particles === this._particles; for (let index = 0; index < particles.length; index++) { const particle = particles[index]; this._tempScaledUpdateSpeed = this._scaledUpdateSpeed; const previousAge = particle.age; particle.age += this._tempScaledUpdateSpeed; // Evaluate step to death if (particle.age > particle.lifeTime) { const diff = particle.age - previousAge; const oldDiff = particle.lifeTime - previousAge; this._tempScaledUpdateSpeed = (oldDiff * this._tempScaledUpdateSpeed) / diff; particle.age = particle.lifeTime; } this._ratio = particle.age / particle.lifeTime; this._directionScale = this._tempScaledUpdateSpeed; // Processing queue let currentQueueItem = this._updateQueueStart; while (currentQueueItem) { currentQueueItem.process(particle, this); currentQueueItem = currentQueueItem.nextItem; } if (this._isAnimationSheetEnabled && !noUpdateQueue) { particle.updateCellIndex(); } // Update the position of the attached sub-emitters to match their attached particle particle._inheritParticleInfoToSubEmitters(); if (particle.age >= particle.lifeTime) { // Recycle by swapping with last particle this._emitFromParticle(particle); if (particle._attachedSubEmitters) { for (const subEmitter of particle._attachedSubEmitters) { subEmitter.particleSystem.disposeOnStop = true; subEmitter.particleSystem.stop(); } particle._attachedSubEmitters = null; } this.recycleParticle(particle); if (sameParticleArray) { index--; } continue; } } }; } serialize(serializeTexture) { throw new Error("Method not implemented."); } /** * Clones the particle system. * @param name The name of the cloned object * @param newEmitter The new emitter to use * @param cloneTexture Also clone the textures if true */ clone(name, newEmitter, cloneTexture = false) { throw new Error("Method not implemented."); } _addFactorGradient(factorGradients, gradient, factor, factor2) { const newGradient = new FactorGradient(gradient, factor, factor2); factorGradients.push(newGradient); factorGradients.sort((a, b) => { if (a.gradient < b.gradient) { return -1; } else if (a.gradient > b.gradient) { return 1; } return 0; }); } _removeFactorGradient(factorGradients, gradient) { if (!factorGradients) { return; } let index = 0; for (const factorGradient of factorGradients) { if (factorGradient.gradient === gradient) { factorGradients.splice(index, 1); break; } index++; } } _syncLifeTimeCreation() { if (this.targetStopDuration && this._lifeTimeGradients && this._lifeTimeGradients.length > 0) { this._lifeTimeCreation.process = _CreateLifeGradientsData; return; } this._lifeTimeCreation.process = _CreateLifetimeData; } _syncStartSizeCreation() { if (this._startSizeGradients && this._startSizeGradients[0] && this.targetStopDuration) { if (!this._startSizeCreation) { this._startSizeCreation = { process: _CreateStartSizeGradientsData, previousItem: null, nextItem: null, }; _ConnectAfter(this._startSizeCreation, this._sizeCreation); } return; } if (this._startSizeCreation) { _RemoveFromQueue(this._startSizeCreation); this._startSizeCreation = null; } } get targetStopDuration() { return this._targetStopDuration; } set targetStopDuration(value) { if (this.targetStopDuration === value) { return; } this._targetStopDuration = value; this._syncLifeTimeCreation(); this._syncStartSizeCreation(); } /** * Adds a new life time gradient * @param gradient defines the gradient to use (between 0 and 1) * @param factor defines the life time factor to affect to the specified gradient * @param factor2 defines an additional factor used to define a range ([factor, factor2]) with main value to pick the final value from * @returns the current particle system */ addLifeTimeGradient(gradient, factor, factor2) { if (!this._lifeTimeGradients) { this._lifeTimeGradients = []; } this._addFactorGradient(this._lifeTimeGradients, gradient, factor, factor2); this._syncLifeTimeCreation(); return this; } /** * Remove a specific life time gradient * @param gradient defines the gradient to remove * @returns the current particle system */ removeLifeTimeGradient(gradient) { this._removeFactorGradient(this._lifeTimeGradients, gradient); this._syncLifeTimeCreation(); return this; } /** * Adds a new size gradient * @param gradient defines the gradient to use (between 0 and 1) * @param factor defines the size factor to affect to the specified gradient * @param factor2 defines an additional factor used to define a range ([factor, factor2]) with main value to pick the final value from * @returns the current particle system */ addSizeGradient(gradient, factor, factor2) { if (!this._sizeGradients) { this._sizeGradients = []; } if (this._sizeGradients.length === 0) { this._sizeCreation.process = _CreateSizeGradientsData; this._sizeGradientProcessing = { process: _ProcessSizeGradients, previousItem: null, nextItem: null, }; _ConnectBefore(this._sizeGradientProcessing, this._gravityProcessing); } this._addFactorGradient(this._sizeGradients, gradient, factor, factor2); return this; } /** * Remove a specific size gradient * @param gradient defines the gradient to remove * @returns the current particle system */ removeSizeGradient(gradient) { this._removeFactorGradient(this._sizeGradients, gradient); if (this._sizeGradients?.length === 0) { _RemoveFromQueue(this._sizeGradientProcessing); this._sizeCreation.process = _CreateSizeData; } return this; } /** * Adds a new color remap gradient * @param gradient defines the gradient to use (between 0 and 1) * @param min defines the color remap minimal range * @param max defines the color remap maximal range * @returns the current particle system */ addColorRemapGradient(gradient, min, max) { if (!this._colorRemapGradients) { this._colorRemapGradients = []; } this._addFactorGradient(this._colorRemapGradients, gradient, min, max); return this; } /** * Remove a specific color remap gradient * @param gradient defines the gradient to remove * @returns the current particle system */ removeColorRemapGradient(gradient) { this._removeFactorGradient(this._colorRemapGradients, gradient); return this; } /** * Adds a new alpha remap gradient * @param gradient defines the gradient to use (between 0 and 1) * @param min defines the alpha remap minimal range * @param max defines the alpha remap maximal range * @returns the current particle system */ addAlphaRemapGradient(gradient, min, max) { if (!this._alphaRemapGradients) { this._alphaRemapGradients = []; } this._addFactorGradient(this._alphaRemapGradients, gradient, min, max); return this; } /** * Remove a specific alpha remap gradient * @param gradient defines the gradient to remove * @returns the current particle system */ removeAlphaRemapGradient(gradient) { this._removeFactorGradient(this._alphaRemapGradients, gradient); return this; } /** * Adds a new angular speed gradient * @param gradient defines the gradient to use (between 0 and 1) * @param factor defines the angular speed to affect to the specified gradient * @param factor2 defines an additional factor used to define a range ([factor, factor2]) with main value to pick the final value from * @returns the current particle system */ addAngularSpeedGradient(gradient, factor, factor2) { if (!this._angularSpeedGradients) { this._angularSpeedGradients = []; } if (this._angularSpeedGradients.length === 0) { this._angleCreation.process = _CreateAngleGradientsData; this._angularSpeedGradientProcessing = { process: _ProcessAngularSpeedGradients, previousItem: null, nextItem: null, }; _ConnectBefore(this._angularSpeedGradientProcessing, this._angularSpeedProcessing); } this._addFactorGradient(this._angularSpeedGradients, gradient, factor, factor2); return this; } /** * Remove a specific angular speed gradient * @param gradient defines the gradient to remove * @returns the current particle system */ removeAngularSpeedGradient(gradient) { this._removeFactorGradient(this._angularSpeedGradients, gradient); if (this._angularSpeedGradients?.length === 0) { this._angleCreation.process = _CreateAngleData; _RemoveFromQueue(this._angularSpeedGradientProcessing); } return this; } /** * Adds a new velocity gradient * @param gradient defines the gradient to use (between 0 and 1) * @param factor defines the velocity to affect to the specified gradient * @param factor2 defines an additional factor used to define a range ([factor, factor2]) with main value to pick the final value from * @returns the current particle system */ addVelocityGradient(gradient, factor, factor2) { if (!this._velocityGradients) { this._velocityGradients = []; } if (this._velocityGradients.length === 0) { this._velocityCreation = { process: _CreateVelocityGradients, previousItem: null, nextItem: null, }; _ConnectAfter(this._velocityCreation, this._angleCreation); this._velocityGradientProcessing = { process: _ProcessVelocityGradients, previousItem: null, nextItem: null, }; _ConnectBefore(this._velocityGradientProcessing, this._directionProcessing); } this._addFactorGradient(this._velocityGradients, gradient, factor, factor2); return this; } /** * Remove a specific velocity gradient * @param gradient defines the gradient to remove * @returns the current particle system */ removeVelocityGradient(gradient) { this._removeFactorGradient(this._velocityGradients, gradient); if (this._velocityGradients?.length === 0) { _RemoveFromQueue(this._velocityCreation); _RemoveFromQueue(this._velocityGradientProcessing); } return this; } /** * Adds a new limit velocity gradient * @param gradient defines the gradient to use (between 0 and 1) * @param factor defines the limit velocity value to affect to the specified gradient * @param factor2 defines an additional factor used to define a range ([factor, factor2]) with main value to pick the final value from * @returns the current particle system */ addLimitVelocityGradient(gradient, factor, factor2) { if (!this._limitVelocityGradients) { this._limitVelocityGradients = []; } if (this._limitVelocityGradients.length === 0) { this._limitVelocityCreation = { process: _CreateLimitVelocityGradients, previousItem: null, nextItem: null, }; _ConnectAfter(this._limitVelocityCreation, this._angleCreation); this._limitVelocityGradientProcessing = { process: _ProcessLimitVelocityGradients, previousItem: null, nextItem: null, }; _ConnectAfter(this._limitVelocityGradientProcessing, this._directionProcessing); } this._addFactorGradient(this._limitVelocityGradients, gradient, factor, factor2); return this; } /** * Remove a specific limit velocity gradient * @param gradient defines the gradient to remove * @returns the current particle system */ removeLimitVelocityGradient(gradient) { this._removeFactorGradient(this._limitVelocityGradients, gradient); if (this._limitVelocityGradients?.length === 0) { _RemoveFromQueue(this._limitVelocityCreation); _RemoveFromQueue(this._limitVelocityGradientProcessing); } return this; } /** * Adds a new drag gradient * @param gradient defines the gradient to use (between 0 and 1) * @param factor defines the drag value to affect to the specified gradient * @param factor2 defines an additional factor used to define a range ([factor, factor2]) with main value to pick the final value from * @returns the current particle system */ addDragGradient(gradient, factor, factor2) { if (!this._dragGradients) { this._dragGradients = []; } if (this._dragGradients.length === 0) { this._dragCreation = { process: _CreateDragData, previousItem: null, nextItem: null, }; _ConnectBefore(this._dragCreation, this._colorDeadCreation); this._dragGradientProcessing = { process: _ProcessDragGradients, previousItem: null, nextItem: null, }; _ConnectBefore(this._dragGradientProcessing, this._positionProcessing); } this._addFactorGradient(this._dragGradients, gradient, factor, factor2); return this; } /** * Remove a specific drag gradient * @param gradient defines the gradient to remove * @returns the current particle system */ removeDragGradient(gradient) { this._removeFactorGradient(this._dragGradients, gradient); if (this._dragGradients?.length === 0) { _RemoveFromQueue(this._dragCreation); _RemoveFromQueue(this._dragGradientProcessing); } return this; } /** * Adds a new emit rate gradient (please note that this will only work if you set the targetStopDuration property) * @param gradient defines the gradient to use (between 0 and 1) * @param factor defines the emit rate value to affect to the specified gradient * @param factor2 defines an additional factor used to define a range ([factor, factor2]) with main value to pick the final value from * @returns the current particle system */ addEmitRateGradient(gradient, factor, factor2) { if (!this._emitRateGradients) { this._emitRateGradients = []; } this._addFactorGradient(this._emitRateGradients, gradient, factor, factor2); return this; } /** * Remove a specific emit rate gradient * @param gradient defines the gradient to remove * @returns the current particle system */ removeEmitRateGradient(gradient) { this._removeFactorGradient(this._emitRateGradients, gradient); return this; } /** * Adds a new start size gradient (please note that this will only work if you set the targetStopDuration property) * @param gradient defines the gradient to use (between 0 and 1) * @param factor defines the start size value to affect to the specified gradient * @param factor2 defines an additional factor used to define a range ([factor, factor2]) with main value to pick the final value from * @returns the current particle system */ addStartSizeGradient(gradient, factor, factor2) { if (!this._startSizeGradients) { this._startSizeGradients = []; } this._addFactorGradient(this._startSizeGradients, gradient, factor, factor2); this._syncStartSizeCreation(); return this; } /** * Remove a specific start size gradient * @param gradient defines the gradient to remove * @returns the current particle system */ removeStartSizeGradient(gradient) { this._removeFactorGradient(this._startSizeGradients, gradient); this._syncStartSizeCreation(); return this; } _createRampGradientTexture() { if (!this._rampGradients || !this._rampGradients.length || this._rampGradientsTexture || !this._scene) { return; } const data = new Uint8Array(this._rawTextureWidth * 4); const tmpColor = TmpColors.Color3[0]; for (let x = 0; x < this._rawTextureWidth; x++) { const ratio = x / this._rawTextureWidth; GradientHelper.GetCurrentGradient(ratio, this._rampGradients, (currentGradient, nextGradient, scale) => { Color3.LerpToRef(currentGradient.color, nextGradient.color, scale, tmpColor); data[x * 4] = tmpColor.r * 255; data[x * 4 + 1] = tmpColor.g * 255; data[x * 4 + 2] = tmpColor.b * 255; data[x * 4 + 3] = 255; }); } this._rampGradientsTexture = RawTexture.CreateRGBATexture(data, this._rawTextureWidth, 1, this._scene, false, false, 1); } /** * Gets the current list of ramp gradients. * You must use addRampGradient and removeRampGradient to update this list * @returns the list of ramp gradients */ getRampGradients() { return this._rampGradients; } /** Force the system to rebuild all gradients that need to be resync */ forceRefreshGradients() { this._syncRampGradientTexture(); } _syncRampGradientTexture() { if (!this._rampGradients) { return; } this._rampGradients.sort((a, b) => { if (a.gradient < b.gradient) { return -1; } else if (a.gradient > b.gradient) { return 1; } return 0; }); if (this._rampGradientsTexture) { this._rampGradientsTexture.dispose(); this._rampGradientsTexture = null; } this._createRampGradientTexture(); } /** * Adds a new ramp gradient used to remap particle colors * @param gradient defines the gradient to use (between 0 and 1) * @param color defines the color to affect to the specified gradient * @returns the current particle system */ addRampGradient(gradient, color) { if (!this._rampGradients) { this._rampGradients = []; } const rampGradient = new Color3Gradient(gradient, color); this._rampGradients.push(rampGradient); this._syncRampGradientTexture(); return this; } /** * Remove a specific ramp gradient * @param gradient defines the gradient to remove * @returns the current particle system */ removeRampGradient(gradient) { this._removeGradientAndTexture(gradient, this._rampGradients, this._rampGradientsTexture); this._rampGradientsTexture = null; if (this._rampGradients && this._rampGradients.length > 0) { this._createRampGradientTexture(); } return this; } /** * Adds a new color gradient * @param gradient defines the gradient to use (between 0 and 1) * @param color1 defines the color to affect to the specified gradient * @param color2 defines an additional color used to define a range ([color, color2]) with main color to pick the final color from * @returns this particle system */ addColorGradient(gradient, color1, color2) { if (!this._colorGradients) { this._colorGradients = []; } if (this._colorGradients.length === 0) { this._colorCreation.process = _CreateColorGradientsData; this._colorProcessing.process = _ProcessColorGradients; } const colorGradient = new ColorGradient(gradient, color1, color2); this._colorGradients.push(colorGradient); this._colorGradients.sort((a, b) => { if (a.gradient < b.gradient) { return -1; } else if (a.gradient > b.gradient) { return 1; } return 0; }); return this; } /** * Remove a specific color gradient * @param gradient defines the gradient to remove * @returns this particle system */ removeColorGradient(gradient) { if (!this._colorGradients) { return this; } let index = 0; for (const colorGradient of this._colorGradients) { if (colorGradient.gradient === gradient) { this._colorGradients.splice(index, 1); break; } index++; } if (this._colorGradients.length === 0) { this._colorCreation.process = _CreateColorData; this._colorProcessing.process = _ProcessColor; } return this; } /** * Resets the draw wrappers cache */ resetDrawCache() { if (!this._drawWrappers) { return; } for (const drawWrappers of this._drawWrappers) { if (drawWrappers) { for (const drawWrapper of drawWrappers) { drawWrapper?.dispose(); } } } this._drawWrappers = []; } /** @internal */ _fetchR(u, v, width, height, pixels) { u = Math.abs(u) * 0.5 + 0.5; v = Math.abs(v) * 0.5 + 0.5; const wrappedU = (u * width) % width | 0; const wrappedV = (v * height) % height | 0; const position = (wrappedU + wrappedV * width) * 4; return pixels[position] / 255; } _reset() { this._resetEffect(); } _resetEffect() { if (this._vertexBuffer) { this._vertexBuffer.dispose(); this._vertexBuffer = null; } if (this._spriteBuffer) { this._spriteBuffer.dispose(); this._spriteBuffer = null; } if (this._vertexArrayObject) { this._engine.releaseVertexArrayObject(this._vertexArrayObject); this._vertexArrayObject = null; } this._createVertexBuffers(); } _createVertexBuffers() { this._vertexBufferSize = this._useInstancing ? 10 : 12; if (this._isAnimationSheetEnabled) { this._vertexBufferSize += 1; } if (!this._isBillboardBased || this.billboardMode === 8 || this.billboardMode === 9) { this._vertexBufferSize += 3; } if (this._useRampGradients) { this._vertexBufferSize += 4; } const engine = this._engine; const vertexSize = this._vertexBufferSize * (this._useInstancing ? 1 : 4); this._vertexData = new Float32Array(this._capacity * vertexSize); this._vertexBuffer = new Buffer(engine, this._vertexData, true, vertexSize); let dataOffset = 0; const positions = this._vertexBuffer.createVertexBuffer(VertexBuffer.PositionKind, dataOffset, 3, this._vertexBufferSize, this._useInstancing); this._vertexBuffers[VertexBuffer.PositionKind] = positions; dataOffset += 3; const colors = this._vertexBuffer.createVertexBuffer(VertexBuffer.ColorKind, dataOffset, 4, this._vertexBufferSize, this._useInstancing); this._vertexBuffers[VertexBuffer.ColorKind] = colors; dataOffset += 4; const options = this._vertexBuffer.createVertexBuffer("angle", dataOffset, 1, this._vertexBufferSize, this._useInstancing); this._vertexBuffers["angle"] = options; dataOffset += 1; const size = this._vertexBuffer.createVertexBuffer("size", dataOffset, 2, this._vertexBufferSize, this._useInstancing); this._vertexBuffers["size"] = size; dataOffset += 2; if (this._isAnimationSheetEnabled) { const cellIndexBuffer = this._vertexBuffer.createVertexBuffer("cellIndex", dataOffset, 1, this._vertexBufferSize, this._useInstancing); this._vertexBuffers["cellIndex"] = cellIndexBuffer; dataOffset += 1; } if (!this._isBillboardBased || this.billboardMode === 8 || this.billboardMode === 9) { const directionBuffer = this._vertexBuffer.createVertexBuffer("direction", dataOffset, 3, this._vertexBufferSize, this._useInstancing); this._vertexBuffers["direction"] = directionBuffer; dataOffset += 3; } if (this._useRampGradients) { const rampDataBuffer = this._vertexBuffer.createVertexBuffer("remapData", dataOffset, 4, this._vertexBufferSize, this._useInstancing); this._vertexBuffers["remapData"] = rampDataBuffer; dataOffset += 4; } let offsets; if (this._useInstancing) { const spriteData = new Float32Array([0, 0, 1, 0, 0, 1, 1, 1]); this._spriteBuffer = new Buffer(engine, spriteData, false, 2); offsets = this._spriteBuffer.createVertexBuffer("offset", 0, 2); } else { offsets = this._vertexBuffer.createVertexBuffer("offset", dataOffset, 2, this._vertexBufferSize, this._useInstancing); dataOffset += 2; } this._vertexBuffers["offset"] = offsets; this.resetDrawCache(); } _createIndexBuffer() { if (this._useInstancing) { this._linesIndexBufferUseInstancing = this._engine.createIndexBuffer(new Uint32Array([0, 1, 1, 3, 3, 2, 2, 0, 0, 3])); return; } const indices = []; const indicesWireframe = []; let index = 0; for (let count = 0; count < this._capacity; count++) { indices.push(index); indices.push(index + 1); indices.push(index + 2); indices.push(index); indices.push(index + 2); indices.push(index + 3); indicesWireframe.push(index, index + 1, index + 1, index + 2, index + 2, index + 3, index + 3, index, index, index + 3); index += 4; } this._indexBuffer = this._engine.createIndexBuffer(indices); this._linesIndexBuffer = this._engine.createIndexBuffer(indicesWireframe); } /** * Gets the maximum number of particles active at the same time. * @returns The max number of active particles. */ getCapacity() { return this._capacity; } /** * Gets whether there are still active particles in the system. * @returns True if it is alive, otherwise false. */ isAlive() { return this._alive; } /** * Gets if the system has been started. (Note: this will still be true after stop is called) * @returns True if it has been started, otherwise false. */ isStarted() { return this._started; } /** @internal */ _preStart() { // Do nothing } /** * Starts the particle system and begins to emit * @param delay defines the delay in milliseconds before starting the system (this.startDelay by default) */ start(delay = this.startDelay) { if (!this.targetStopDuration && this._hasTargetStopDurationDependantGradient()) { // eslint-disable-next-line no-throw-literal throw "Particle system started with a targetStopDuration dependant gradient (eg. startSizeGradients) but no targetStopDuration set"; } if (delay) { setTimeout(() => { this.start(0); }, delay); return; } this._started = true; this._stopped = false; this._actualFrame = 0; this._preStart(); // Reset emit gradient so it acts the same on every start if (this._emitRateGradients) { if (this._emitRateGradients.length > 0) { this._currentEmitRateGradient = this._emitRateGradients[0]; this._currentEmitRate1 = this._currentEmitRateGradient.getFactor(); this._currentEmitRate2 = this._currentEmitRate1; } if (this._emitRateGradients.length > 1) { this._currentEmitRate2 = this._emitRateGradients[1].getFactor(); } } // Reset start size gradient so it acts the same on every start if (this._startSizeGradients) { if (this._startSizeGradients.length >