@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,134 lines (1,133 loc) • 79.9 kB
JavaScript
import { FactorGradient, ColorGradient, Color3Gradient, GradientHelper } from "../Misc/gradients.js";
import { Observable } from "../Misc/observable.js";
import { Vector3, Matrix, Vector4, 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 { Clamp, Lerp, RandomRange } from "../Maths/math.scalar.functions.js";
import { PrepareSamplersForImageProcessing, PrepareUniformsForImageProcessing } from "../Materials/imageProcessingConfiguration.functions.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 {
/**
* 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 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();
}
/**
* 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;
}
/**
* 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;
}
/**
* 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
*/
constructor(name, capacity, sceneOrEngine, customEffect = null, isAnimationSheetEnabled = false, epsilon = 0.01) {
super(name);
this._emitterInverseWorldMatrix = Matrix.Identity();
/**
* @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();
this._particles = new Array();
this._stockParticles = new Array();
this._newPartsExcess = 0;
this._vertexBuffers = {};
this._scaledColorStep = new Color4(0, 0, 0, 0);
this._colorDiff = new Color4(0, 0, 0, 0);
this._scaledDirection = Vector3.Zero();
this._scaledGravity = Vector3.Zero();
this._currentRenderId = -1;
this._useInstancing = false;
this._started = false;
this._stopped = false;
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;
/**
* Specifies if the particles are updated in emitter local space or world space
*/
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;
};
this._shadersLoaded = false;
this._capacity = capacity;
this._epsilon = epsilon;
this._isAnimationSheetEnabled = isAnimationSheetEnabled;
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;
}
this._initShaderSourceAsync();
// 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();
let noiseTextureData = null;
// Update
this.updateFunction = (particles) => {
let noiseTextureSize = null;
if (this.noiseTexture) {
// We need to get texture data back to CPU
noiseTextureSize = this.noiseTexture.getSize();
this.noiseTexture.getContent()?.then((data) => {
noiseTextureData = data;
});
}
const sameParticleArray = particles === this._particles;
for (let index = 0; index < particles.length; index++) {
const particle = particles[index];
let scaledUpdateSpeed = this._scaledUpdateSpeed;
const previousAge = particle.age;
particle.age += scaledUpdateSpeed;
// Evaluate step to death
if (particle.age > particle.lifeTime) {
const diff = particle.age - previousAge;
const oldDiff = particle.lifeTime - previousAge;
scaledUpdateSpeed = (oldDiff * scaledUpdateSpeed) / diff;
particle.age = particle.lifeTime;
}
const ratio = particle.age / particle.lifeTime;
// Color
if (this._colorGradients && this._colorGradients.length > 0) {
GradientHelper.GetCurrentGradient(ratio, this._colorGradients, (currentGradient, nextGradient, scale) => {
if (currentGradient !== particle._currentColorGradient) {
particle._currentColor1.copyFrom(particle._currentColor2);
nextGradient.getColorToRef(particle._currentColor2);
particle._currentColorGradient = currentGradient;
}
Color4.LerpToRef(particle._currentColor1, particle._currentColor2, scale, particle.color);
});
}
else {
particle.colorStep.scaleToRef(scaledUpdateSpeed, this._scaledColorStep);
particle.color.addInPlace(this._scaledColorStep);
if (particle.color.a < 0) {
particle.color.a = 0;
}
}
// Angular speed
if (this._angularSpeedGradients && this._angularSpeedGradients.length > 0) {
GradientHelper.GetCurrentGradient(ratio, this._angularSpeedGradients, (currentGradient, nextGradient, scale) => {
if (currentGradient !== particle._currentAngularSpeedGradient) {
particle._currentAngularSpeed1 = particle._currentAngularSpeed2;
particle._currentAngularSpeed2 = nextGradient.getFactor();
particle._currentAngularSpeedGradient = currentGradient;
}
particle.angularSpeed = Lerp(particle._currentAngularSpeed1, particle._currentAngularSpeed2, scale);
});
}
particle.angle += particle.angularSpeed * scaledUpdateSpeed;
// Direction
let directionScale = scaledUpdateSpeed;
/// Velocity
if (this._velocityGradients && this._velocityGradients.length > 0) {
GradientHelper.GetCurrentGradient(ratio, this._velocityGradients, (currentGradient, nextGradient, scale) => {
if (currentGradient !== particle._currentVelocityGradient) {
particle._currentVelocity1 = particle._currentVelocity2;
particle._currentVelocity2 = nextGradient.getFactor();
particle._currentVelocityGradient = currentGradient;
}
directionScale *= Lerp(particle._currentVelocity1, particle._currentVelocity2, scale);
});
}
particle.direction.scaleToRef(directionScale, this._scaledDirection);
/// Limit velocity
if (this._limitVelocityGradients && this._limitVelocityGradients.length > 0) {
GradientHelper.GetCurrentGradient(ratio, this._limitVelocityGradients, (currentGradient, nextGradient, scale) => {
if (currentGradient !== particle._currentLimitVelocityGradient) {
particle._currentLimitVelocity1 = particle._currentLimitVelocity2;
particle._currentLimitVelocity2 = nextGradient.getFactor();
particle._currentLimitVelocityGradient = currentGradient;
}
const limitVelocity = Lerp(particle._currentLimitVelocity1, particle._currentLimitVelocity2, scale);
const currentVelocity = particle.direction.length();
if (currentVelocity > limitVelocity) {
particle.direction.scaleInPlace(this.limitVelocityDamping);
}
});
}
/// Drag
if (this._dragGradients && this._dragGradients.length > 0) {
GradientHelper.GetCurrentGradient(ratio, this._dragGradients, (currentGradient, nextGradient, scale) => {
if (currentGradient !== particle._currentDragGradient) {
particle._currentDrag1 = particle._currentDrag2;
particle._currentDrag2 = nextGradient.getFactor();
particle._currentDragGradient = currentGradient;
}
const drag = Lerp(particle._currentDrag1, particle._currentDrag2, scale);
this._scaledDirection.scaleInPlace(1.0 - drag);
});
}
if (this.isLocal && particle._localPosition) {
particle._localPosition.addInPlace(this._scaledDirection);
Vector3.TransformCoordinatesToRef(particle._localPosition, this._emitterWorldMatrix, particle.position);
}
else {
particle.position.addInPlace(this._scaledDirection);
}
// Noise
if (noiseTextureData && noiseTextureSize && particle._randomNoiseCoordinates1) {
const fetchedColorR = this._fetchR(particle._randomNoiseCoordinates1.x, particle._randomNoiseCoordinates1.y, noiseTextureSize.width, noiseTextureSize.height, noiseTextureData);
const fetchedColorG = this._fetchR(particle._randomNoiseCoordinates1.z, particle._randomNoiseCoordinates2.x, noiseTextureSize.width, noiseTextureSize.height, noiseTextureData);
const fetchedColorB = this._fetchR(particle._randomNoiseCoordinates2.y, particle._randomNoiseCoordinates2.z, noiseTextureSize.width, noiseTextureSize.height, noiseTextureData);
const force = TmpVectors.Vector3[0];
const scaledForce = TmpVectors.Vector3[1];
force.copyFromFloats((2 * fetchedColorR - 1) * this.noiseStrength.x, (2 * fetchedColorG - 1) * this.noiseStrength.y, (2 * fetchedColorB - 1) * this.noiseStrength.z);
force.scaleToRef(scaledUpdateSpeed, scaledForce);
particle.direction.addInPlace(scaledForce);
}
// Gravity
this.gravity.scaleToRef(scaledUpdateSpeed, this._scaledGravity);
particle.direction.addInPlace(this._scaledGravity);
// Size
if (this._sizeGradients && this._sizeGradients.length > 0) {
GradientHelper.GetCurrentGradient(ratio, this._sizeGradients, (currentGradient, nextGradient, scale) => {
if (currentGradient !== particle._currentSizeGradient) {
particle._currentSize1 = particle._currentSize2;
particle._currentSize2 = nextGradient.getFactor();
particle._currentSizeGradient = currentGradient;
}
particle.size = Lerp(particle._currentSize1, particle._currentSize2, scale);
});
}
// Remap data
if (this._useRampGradients) {
if (this._colorRemapGradients && this._colorRemapGradients.length > 0) {
GradientHelper.GetCurrentGradient(ratio, this._colorRemapGradients, (currentGradient, nextGradient, scale) => {
const min = Lerp(currentGradient.factor1, nextGradient.factor1, scale);
const max = Lerp(currentGradient.factor2, nextGradient.factor2, scale);
particle.remapData.x = min;
particle.remapData.y = max - min;
});
}
if (this._alphaRemapGradients && this._alphaRemapGradients.length > 0) {
GradientHelper.GetCurrentGradient(ratio, this._alphaRemapGradients, (currentGradient, nextGradient, scale) => {
const min = Lerp(currentGradient.factor1, nextGradient.factor1, scale);
const max = Lerp(currentGradient.factor2, nextGradient.factor2, scale);
particle.remapData.z = min;
particle.remapData.w = max - min;
});
}
}
if (this._isAnimationSheetEnabled) {
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) {
particle._attachedSubEmitters.forEach((subEmitter) => {
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++;
}
}
/**
* 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);
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);
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 = [];
}
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);
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 = [];
}
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);
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 = [];
}
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);
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 = [];
}
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);
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 = [];
}
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);
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);
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);
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 = [];
}
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++;
}
return this;
}
/**
* Resets the draw wrappers cache
*/
resetDrawCache() {
for (const drawWrappers of this._drawWrappers) {
if (drawWrappers) {
for (const drawWrapper of drawWrappers) {
drawWrapper?.dispose();
}
}
}
this._drawWrappers = [];
}
_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 > 0) {
this._currentStartSizeGradient = this._startSizeGradients[0];
this._currentStartSize1 = this._currentStartSizeGradient.getFactor();
this._currentStartSize2 = this._currentStartSize1;
}
if (this._startSizeGradients.length > 1) {
this._currentStartSize2 = this._startSizeGradients[1].getFactor();
}
}
if (this.preWarmCycles) {
if (this.emitter?.getClassName().indexOf("Mesh") !== -1) {
this.emitter.computeWorldMatrix(true);
}
const noiseTextureAsProcedural = this.noiseTexture;
if (noiseTextureAsProcedural && noiseTextureAsProcedural.onGeneratedObservable) {
noiseTextureAsProcedural.onGeneratedObservable.addOnce(() => {
setTimeout(() => {
for (let index = 0; index < this.preWarmCycles; index++) {
this.animate(true);
noiseTextureAsProcedural.render();
}
});
});
}
else {
for (let index = 0; index < this.preWarmCycles; index++) {
this.animate(true);
}
}
}
// Animations
if (this.beginAnimationOnStart && this.animations && this.animations.length > 0 && this._scene) {
this._scene.beginAnimation(this, this.beginAnimationFrom, this.beginAnimationTo, this.beginAnimationLoop);
}
}
/**
* Stops the particle system.
* @param stopSubEmitters if true it will stop the current system and all created sub-Systems if false it will stop the current root system only, this param is used by the root particle system only. The default value is true.
*/
stop(stopSubEmitters = true) {
if (this._stopped) {
return;
}
this.onStoppedObservable.notifyObservers(this);
this._stopped = true;
this._postStop(stopSubEmitters);
}
/** @internal */
_postStop(stopSubEmitters) {
// Do nothing
}
// Animation sheet
/**
* Remove all active particles
*/
reset() {
this._stockParticles.length = 0;
this._particles.length = 0;
}
/**
* @internal (for internal use only)
*/
_appendParticleVertex(index, particle, offsetX, offsetY) {
let offset = index * this._vertexBufferSize;
this._vertexData[offset++] = particle.position.x + this.worldOffset.x;
this._vertexData[offset++] = particle.position.y + this.worldOffset.y;
this._vertexData[offset++] = particle.position.z + this.worldOffset.z;
this._vertexData[offset++] = particle.color.r;
this._vertexData[offset++] = particle.color.g;
this._vertexData[offset++] = particle.color.b;
this._vertexData[offset++] = particle.color.a;
this._vertexData[offset++] = particle.angle;
this._vertexData[offset++] = particle.scale.x * particle.size;
this._vertexData[offset++] = particle.scale.y * particle.size;
if (this._isAnimationSheetEnabled) {
this._vertexData[offset++] = particle.cellIndex;
}
if (!this._isBillboardBased) {
if (particle._initialDirection) {
let initialDirection = particle._initialDirection;
if (this.isLocal) {
Vector3.TransformNormalToRef(initialDirection, this._emitterWorldMatrix, TmpVectors.Vector3[0]);
initialDirection = TmpVectors.Vector3[0];
}
if (initialDirection.x === 0 && initialDirection.z === 0) {
initialDirection.x = 0.001;
}
this._vertexData[offset++] = initialDirection.x;
this._vertexData[offset++] = initialDirection.y;
this._vertexData[offset++] = initialDirection.z;
}
else {
let direction = particle.direction;
if (this.isLocal) {
Vector3.TransformNormalToRef(direction, this._emitterWorldMatrix, TmpVectors.Vector3[0]);
direction = TmpVectors.Vector3[0];
}
if (direction.x === 0 && direction.z === 0) {
direction.x = 0.001;
}
this._vertexData[offset++] = direction.x;
this._vertexData[offset++] = direction.y;
this._vertexData[offset++] = direction.z;
}
}
else if (this.billboardMode === 8 || this.billboardMode === 9) {
this._vertexData[offset++] = particle.direction.x;
this._vertexData[offset++] = particle.direction.y;
this._vertexData[offset++] = particle.direction.z;
}
if (this._useRampGradients && particle.remapData) {
this._vertexData[offset++] = particle.remapData.x;
this._vertexData[offset++] = particle.remapData.y;
this._vertexData[offset++] = particle.remapData.z;
this._vertexData[offset++] = particle.remapData.w;
}
if (!this._useInstancing) {
if (this._isAnimationSheetEnabled) {
if (offsetX === 0) {
offsetX = this._epsilon;
}
else if (offsetX === 1) {
offsetX = 1 - this._epsilon;
}
if (offsetY === 0) {
offsetY = this._epsilon;
}
else if (offsetY === 1) {
offsetY = 1 - this._epsilon;
}
}
this._vertexData[offset++] = offsetX;
this._vertexData[offset++] = offsetY;
}
}
/** @internal */
_prepareParticle(particle) {
//Do nothing
}
_update(newParticles) {
// Update current
this._alive = this._particles.length > 0;
if (this.emitter.position) {
const emitterMesh = this.emitter;
this._emitterWorldMatrix = emitterMesh.getWorldMatrix();
}
else {
const emitterPosition = this.emitter;
this._emitterWorldMatrix =