@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,194 lines (1,193 loc) • 104 kB
JavaScript
/** This file must only contain pure code and pure imports */
import { ColorGradient, GradientHelper } from "../Misc/gradients.js";
import { Observable } from "../Misc/observable.pure.js";
import { Vector3, Matrix, TmpVectors } from "../Maths/math.vector.pure.js";
import { Color4, TmpColors } from "../Maths/math.color.pure.js";
import { Lerp } from "../Maths/math.scalar.functions.js";
import { VertexBuffer, Buffer } from "../Buffers/buffer.pure.js";
import { BaseParticleSystem } from "./baseParticleSystem.pure.js";
import { ParticleSystem } from "./particleSystem.pure.js";
import { Attractor } from "./attractor.js";
import { Logger } from "../Misc/logger.js";
import { BoxParticleEmitter } from "../Particles/EmitterTypes/boxParticleEmitter.js";
import { Scene } from "../scene.pure.js";
import { ImageProcessingConfiguration } from "../Materials/imageProcessingConfiguration.pure.js";
import { RawTexture } from "../Materials/Textures/rawTexture.js";
import { EngineStore } from "../Engines/engineStore.js";
import { RegisterAnimatable } from "../Animations/animatable.pure.js";
import { CustomParticleEmitter } from "./EmitterTypes/customParticleEmitter.js";
import { AbstractEngine } from "../Engines/abstractEngine.pure.js";
import { DrawWrapper } from "../Materials/drawWrapper.js";
import { GetClass } from "../Misc/typeStore.js";
import { _IsSideEffectImplemented } from "../Misc/devTools.js";
import { AddClipPlaneUniforms, BindClipPlane, PrepareStringDefinesForClipPlanes } from "../Materials/clipPlaneMaterialHelper.js";
import { BindFogParameters, BindLogDepth } from "../Materials/materialHelper.functions.js";
import { MeshParticleEmitter } from "./EmitterTypes/meshParticleEmitter.js";
/**
* This represents a GPU particle system in Babylon
* This is the fastest particle system in Babylon as it uses the GPU to update the individual particle data
* @see https://www.babylonjs-playground.com/#PU4WYI#4
*/
export class GPUParticleSystem extends BaseParticleSystem {
/**
* Whether the particle buffer needs to store the initial emission direction.
* True when particles are not billboarded (they orient by direction) or when
* using stretched-local billboard mode (stretches along initial direction).
* @internal
*/
get _needsInitialDirection() {
return !this._isBillboardBased || this.billboardMode === ParticleSystem.BILLBOARDMODE_STRETCHED_LOCAL;
}
/**
* Gets a boolean indicating if the GPU particles can be rendered on current browser
*/
static get IsSupported() {
if (!EngineStore.LastCreatedEngine) {
return false;
}
const caps = EngineStore.LastCreatedEngine.getCaps();
return caps.supportTransformFeedbacks || caps.supportComputeShaders;
}
_createIndexBuffer() {
this._linesIndexBufferUseInstancing = this._engine.createIndexBuffer(new Uint32Array([0, 1, 1, 3, 3, 2, 2, 0, 0, 3]), undefined, "GPUParticleSystemLinesIndexBuffer");
}
/**
* Gets the maximum number of particles active at the same time.
* @returns The max number of active particles.
*/
getCapacity() {
return this._capacity;
}
/**
* Gets whether emit rate control is enabled.
* When true, the GPU particle system limits the number of active particles
* to approximately emitRate * maxLifeTime (matching CPU particle behavior)
* and uses a circular buffer to recycle particle slots.
* When false (default), all dead particles are recycled immediately,
* which is the legacy GPU particle behavior.
*/
get emitRateControl() {
return this._emitRateControl;
}
/**
* Gets or set the number of active particles
* The value cannot be greater than "capacity" (if it is, it will be limited to "capacity").
*/
get maxActiveParticleCount() {
return this._maxActiveParticleCount;
}
set maxActiveParticleCount(value) {
this._maxActiveParticleCount = Math.min(value, this._capacity);
}
/**
* Gets or set the number of active particles
* @deprecated Please use maxActiveParticleCount instead.
*/
get activeParticleCount() {
return this.maxActiveParticleCount;
}
set activeParticleCount(value) {
this.maxActiveParticleCount = value;
}
/**
* Add an attractor to the particle system. Attractors are used to change the direction of the particles in the system.
* @param attractor - The attractor to add to the particle system
*/
addAttractor(attractor) {
if (this._attractors.length >= this.maxAttractors) {
Logger.Warn(`GPU particle system supports a maximum of ${this.maxAttractors} attractors. Ignoring additional attractor.`);
return;
}
super.addAttractor(attractor);
}
/** Gets or sets the current flow map */
get flowMap() {
return this._flowMap;
}
set flowMap(value) {
if (this._flowMap === value) {
return;
}
this._flowMap = value;
}
/**
* Is this system ready to be used/rendered
* @returns true if the system is ready
*/
isReady() {
if (!this.emitter ||
(this._imageProcessingConfiguration && !this._imageProcessingConfiguration.isReady()) ||
(this._flowMap && !this._flowMap.isReady()) ||
!this.particleTexture ||
!this.particleTexture.isReady() ||
this._rebuildingAfterContextLost) {
return false;
}
if (this.blendMode !== ParticleSystem.BLENDMODE_MULTIPLYADD) {
if (!this._getWrapper(this.blendMode).effect.isReady()) {
return false;
}
}
else {
if (!this._getWrapper(ParticleSystem.BLENDMODE_MULTIPLY).effect.isReady()) {
return false;
}
if (!this._getWrapper(ParticleSystem.BLENDMODE_ADD).effect.isReady()) {
return false;
}
}
if (!this._platform.isUpdateBufferCreated()) {
this._recreateUpdateEffect();
return false;
}
return this._platform.isUpdateBufferReady();
}
/**
* 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;
}
/**
* Gets if the system has been stopped. (Note: rendering is still happening but the system is frozen)
* @returns True if it has been stopped, otherwise false.
*/
isStopped() {
return this._stopped;
}
/**
* Gets a boolean indicating that the system is stopping
* @returns true if the system is currently stopping
*/
isStopping() {
return false; // Stop is immediate on GPU
}
/**
* Gets the number of particles active at the same time.
* @returns The number of active particles.
*/
getActiveCount() {
return this._currentActiveCount;
}
/**
* 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._preWarmDone = false;
// 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();
}
}
// 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.
*/
stop() {
if (this._stopped) {
return;
}
this.onStoppedObservable.notifyObservers(this);
this._stopped = true;
}
/**
* Remove all active particles
*/
reset() {
this._releaseBuffers();
this._platform.releaseVertexBuffers();
this._currentActiveCount = 0;
this._targetIndex = 0;
this._writePointer = 0;
this._emitIndex = 0;
this._emitCount = 0;
this._accumulatedCount = 0;
}
/**
* Returns the string "GPUParticleSystem"
* @returns a string containing the class name
*/
getClassName() {
return "GPUParticleSystem";
}
/**
* 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;
}
/**
* 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 "gpuRenderParticles";
}
/**
* Gets the vertex buffers used by the particle system
* Should be called after render() has been called for the current frame so that the buffers returned are the ones that have been updated
* in the current frame (there's a ping-pong between two sets of buffers - for a given frame, one set is used as the source and the other as the destination)
*/
get vertexBuffers() {
// We return the other buffers than those corresponding to this._targetIndex because it is assumed vertexBuffers will be called in the current frame
// after render() has been called, meaning that the buffers have already been swapped and this._targetIndex points to the buffers that will be updated
// in the next frame (and which are the sources in this frame) and (this._targetIndex ^ 1) points to the buffers that have been updated this frame
// (and that will be the source buffers in the next frame)
return this._renderVertexBuffers[this._targetIndex ^ 1];
}
/**
* Gets the index buffer used by the particle system (null for GPU particle systems)
*/
get indexBuffer() {
return null;
}
_removeGradientAndTexture(gradient, gradients, texture) {
super._removeGradientAndTexture(gradient, gradients, texture);
this._releaseBuffers();
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 optional second color to be used to produce a random color per particle at the gradient (lerped with color1 using a per-particle random value)
* @returns the current particle system
*/
addColorGradient(gradient, color1, color2) {
if (!this._colorGradients) {
this._colorGradients = [];
}
const colorGradient = new ColorGradient(gradient, color1, color2);
this._colorGradients.push(colorGradient);
this._refreshColorGradient(true);
this._releaseBuffers();
return this;
}
_refreshColorGradient(reorder = false) {
if (this._colorGradients) {
if (reorder) {
this._colorGradients.sort((a, b) => {
if (a.gradient < b.gradient) {
return -1;
}
else if (a.gradient > b.gradient) {
return 1;
}
return 0;
});
}
// Recompute whether any stop uses a color2 range. Done here (not inside _createColorGradientTexture)
// so the flag is available to both define generation (fillDefines) and render vertex buffer layout
// (_createVertexBuffers), which can run before the texture is recreated.
this._hasColorGradientColor2 = false;
for (const g of this._colorGradients) {
if (g.color2) {
this._hasColorGradientColor2 = true;
break;
}
}
if (this._colorGradientsTexture) {
this._colorGradientsTexture.dispose();
this._colorGradientsTexture = null;
}
}
else {
this._hasColorGradientColor2 = false;
}
}
/** Force the system to rebuild all gradients that need to be resync */
forceRefreshGradients() {
this._refreshColorGradient();
this._refreshFactorGradient(this._sizeGradients, "_sizeGradientsTexture");
this._refreshFactorGradient(this._angularSpeedGradients, "_angularSpeedGradientsTexture");
this._refreshFactorGradient(this._velocityGradients, "_velocityGradientsTexture");
this._refreshFactorGradient(this._limitVelocityGradients, "_limitVelocityGradientsTexture");
this._refreshFactorGradient(this._dragGradients, "_dragGradientsTexture");
this.reset();
}
/**
* Remove a specific color gradient
* @param gradient defines the gradient to remove
* @returns the current particle system
*/
removeColorGradient(gradient) {
this._removeGradientAndTexture(gradient, this._colorGradients, this._colorGradientsTexture);
this._colorGradientsTexture = null;
// The set of remaining gradients may no longer contain a color2; recompute the flag.
this._refreshColorGradient();
return this;
}
/**
* Resets the draw wrappers cache
*/
resetDrawCache() {
for (const blendMode in this._drawWrappers) {
const drawWrapper = this._drawWrappers[blendMode];
drawWrapper.drawContext?.reset();
}
}
/**
* 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);
this._refreshFactorGradient(this._sizeGradients, "_sizeGradientsTexture");
this._releaseBuffers();
return this;
}
/**
* Remove a specific size gradient
* @param gradient defines the gradient to remove
* @returns the current particle system
*/
removeSizeGradient(gradient) {
this._removeGradientAndTexture(gradient, this._sizeGradients, this._sizeGradientsTexture);
this._sizeGradientsTexture = null;
return this;
}
_refreshFactorGradient(factorGradients, textureName) {
if (!factorGradients) {
return;
}
const that = this;
if (that[textureName]) {
that[textureName].dispose();
that[textureName] = null;
}
}
/**
* 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);
this._refreshFactorGradient(this._angularSpeedGradients, "_angularSpeedGradientsTexture");
this._releaseBuffers();
return this;
}
/**
* Remove a specific angular speed gradient
* @param gradient defines the gradient to remove
* @returns the current particle system
*/
removeAngularSpeedGradient(gradient) {
this._removeGradientAndTexture(gradient, this._angularSpeedGradients, this._angularSpeedGradientsTexture);
this._angularSpeedGradientsTexture = null;
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);
this._refreshFactorGradient(this._velocityGradients, "_velocityGradientsTexture");
this._releaseBuffers();
return this;
}
/**
* Remove a specific velocity gradient
* @param gradient defines the gradient to remove
* @returns the current particle system
*/
removeVelocityGradient(gradient) {
this._removeGradientAndTexture(gradient, this._velocityGradients, this._velocityGradientsTexture);
this._velocityGradientsTexture = null;
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);
this._refreshFactorGradient(this._limitVelocityGradients, "_limitVelocityGradientsTexture");
this._releaseBuffers();
return this;
}
/**
* Remove a specific limit velocity gradient
* @param gradient defines the gradient to remove
* @returns the current particle system
*/
removeLimitVelocityGradient(gradient) {
this._removeGradientAndTexture(gradient, this._limitVelocityGradients, this._limitVelocityGradientsTexture);
this._limitVelocityGradientsTexture = null;
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);
this._refreshFactorGradient(this._dragGradients, "_dragGradientsTexture");
this._releaseBuffers();
return this;
}
/**
* Remove a specific drag gradient
* @param gradient defines the gradient to remove
* @returns the current particle system
*/
removeDragGradient(gradient) {
this._removeGradientAndTexture(gradient, this._dragGradients, this._dragGradientsTexture);
this._dragGradientsTexture = null;
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 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
*/
addStartSizeGradient(gradient, factor, factor2) {
if (!this._startSizeGradients) {
this._startSizeGradients = [];
}
const hadGradients = this._startSizeGradients.length > 0;
this._addFactorGradient(this._startSizeGradients, gradient, factor, factor2);
if (!hadGradients) {
this._resetEffect();
}
return this;
}
/**
* Remove a specific start size gradient
* @param gradient defines the gradient to remove
* @returns the current particle system
*/
removeStartSizeGradient(gradient) {
const hadGradients = this._startSizeGradients && this._startSizeGradients.length > 0;
this._removeFactorGradient(this._startSizeGradients, gradient);
if (hadGradients && (!this._startSizeGradients || this._startSizeGradients.length === 0)) {
this._resetEffect();
}
return this;
}
/**
* Not supported by GPUParticleSystem
* @returns the current particle system
*/
addColorRemapGradient() {
// Do nothing as start size is not supported by GPUParticleSystem
return this;
}
/**
* Not supported by GPUParticleSystem
* @returns the current particle system
*/
removeColorRemapGradient() {
// Do nothing as start size is not supported by GPUParticleSystem
return this;
}
/**
* Not supported by GPUParticleSystem
* @returns the current particle system
*/
addAlphaRemapGradient() {
// Do nothing as start size is not supported by GPUParticleSystem
return this;
}
/**
* Not supported by GPUParticleSystem
* @returns the current particle system
*/
removeAlphaRemapGradient() {
// Do nothing as start size is not supported by GPUParticleSystem
return this;
}
/**
* Not supported by GPUParticleSystem
* @returns the current particle system
*/
addRampGradient() {
//Not supported by GPUParticleSystem
return this;
}
/**
* Not supported by GPUParticleSystem
* @returns the current particle system
*/
removeRampGradient() {
//Not supported by GPUParticleSystem
return this;
}
/**
* Not supported by GPUParticleSystem
* @returns the list of ramp gradients
*/
getRampGradients() {
return null;
}
/**
* Not supported by GPUParticleSystem
* 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() {
//Not supported by GPUParticleSystem
return false;
}
set useRampGradients(value) {
//Not supported by GPUParticleSystem
}
/**
* Adds a new life time 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 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 = [];
}
const hadGradients = this._lifeTimeGradients.length > 0;
this._addFactorGradient(this._lifeTimeGradients, gradient, factor, factor2);
if (!hadGradients) {
this._resetEffect();
}
return this;
}
/**
* Remove a specific life time gradient
* @param gradient defines the gradient to remove
* @returns the current particle system
*/
removeLifeTimeGradient(gradient) {
const hadGradients = this._lifeTimeGradients && this._lifeTimeGradients.length > 0;
this._removeFactorGradient(this._lifeTimeGradients, gradient);
if (hadGradients && (!this._lifeTimeGradients || this._lifeTimeGradients.length === 0)) {
this._resetEffect();
}
return this;
}
/**
* Instantiates a GPU 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 options The options used to create the system
* @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
*/
constructor(name, options, sceneOrEngine, customEffect = null, isAnimationSheetEnabled = false) {
RegisterAnimatable();
super(name);
/**
* The layer mask we are rendering the particles through.
*/
this.layerMask = 0x0fffffff;
this._accumulatedCount = 0;
this._writePointer = 0;
this._emitIndex = 0;
this._emitCount = 0;
this._renderVertexBuffers = [];
this._targetIndex = 0;
/** Set to true when any entry in `_colorGradients` has a `color2` (per-particle random color range). */
this._hasColorGradientColor2 = false;
this._currentRenderId = -1;
this._currentRenderingCameraUniqueId = -1;
this._started = false;
this._stopped = false;
this._timeDelta = 0;
/** Indicates that the update of particles is done in the animate function (and not in render). Default: false */
this.updateInAnimate = false;
this._actualFrame = 0;
this._rawTextureWidth = 256;
this._rebuildingAfterContextLost = false;
// Emit rate gradient caching (mirrors ThinParticleSystem)
this._currentEmitRateGradient = null;
this._currentEmitRate1 = 0;
this._currentEmitRate2 = 0;
// Start size gradient caching (mirrors ThinParticleSystem)
this._currentStartSizeGradient = null;
this._currentStartSize1 = 0;
this._currentStartSize2 = 0;
this._startSizeGradientFactor = 1.0;
// Life time gradient factor range for per-particle randomization in shader
this._lifeTimeGradientMin = 1.0;
this._lifeTimeGradientMax = 1.0;
/**
* Specifies if the particle system should be serialized
*/
this.doNotSerialize = false;
/**
* 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();
/**
* Forces the particle to write their depth information to the depth buffer. This can help preventing other draw calls
* to override the particles.
*/
this.forceDepthWrite = false;
this._preWarmDone = false;
/**
* Specifies if the particles are updated in emitter local space or world space.
*/
this.isLocal = false;
/** Indicates that the particle system is GPU based */
this.isGPU = true;
/**
* Gets or sets an object used to store user defined information for the particle system
*/
this.metadata = null;
/** Flow map */
/** @internal */
this._flowMap = null;
/**
* The strength of the flow map
*/
this.flowMapStrength = 1.0;
/** Mesh emitter textures */
/** @internal */
this._meshPositionTexture = null;
/** @internal */
this._meshNormalTexture = null;
/** @internal */
this._meshTriangleCount = 0;
/** @internal */
this._meshTextureWidth = 0;
// Track mesh emitter inputs for invalidation
this._meshEmitterMeshId = -1;
this._meshEmitterUsedNormals = false;
/** @internal */
this._onBeforeDrawParticlesObservable = null;
if (!sceneOrEngine || sceneOrEngine.getClassName() === "Scene") {
this._scene = sceneOrEngine || EngineStore.LastCreatedScene;
this._engine = this._scene.getEngine();
this.uniqueId = this._scene.getUniqueId();
this.layerMask = this._scene.defaultRenderableLayerMask;
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().supportComputeShaders) {
if (!GetClass("BABYLON.ComputeShaderParticleSystem")) {
throw new Error("The ComputeShaderParticleSystem class is not available! Make sure you have imported it.");
}
this._platform = new (GetClass("BABYLON.ComputeShaderParticleSystem"))(this, this._engine);
}
else {
if (!GetClass("BABYLON.WebGL2ParticleSystem")) {
throw new Error("The WebGL2ParticleSystem class is not available! Make sure you have imported it.");
}
this._platform = new (GetClass("BABYLON.WebGL2ParticleSystem"))(this, this._engine);
}
this._customWrappers = { 0: new DrawWrapper(this._engine) };
this._customWrappers[0].effect = customEffect;
this._drawWrappers = { 0: new DrawWrapper(this._engine) };
if (this._drawWrappers[0].drawContext) {
this._drawWrappers[0].drawContext.useInstancing = true;
}
this._createIndexBuffer();
// Setup the default processing configuration to the scene.
this._attachImageProcessingConfiguration(null);
options = options ?? {};
if (!options.randomTextureSize) {
delete options.randomTextureSize;
}
const fullOptions = {
capacity: 50000,
randomTextureSize: this._engine.getCaps().maxTextureSize,
...options,
};
const optionsAsNumber = options;
if (isFinite(optionsAsNumber)) {
fullOptions.capacity = optionsAsNumber;
}
this._capacity = fullOptions.capacity;
this._maxActiveParticleCount = fullOptions.capacity;
this._currentActiveCount = 0;
this._isAnimationSheetEnabled = isAnimationSheetEnabled;
this._emitRateControl = !!options.emitRateControl;
this.maxAttractors = options.maxAttractors ?? 8;
this.particleEmitterType = new BoxParticleEmitter();
// Random data
const maxTextureSize = Math.min(this._engine.getCaps().maxTextureSize, fullOptions.randomTextureSize);
let d = [];
for (let i = 0; i < maxTextureSize; ++i) {
d.push(Math.random());
d.push(Math.random());
d.push(Math.random());
d.push(Math.random());
}
this._randomTexture = new RawTexture(new Float32Array(d), maxTextureSize, 1, 5, sceneOrEngine, false, false, 1, 1);
this._randomTexture.name = "GPUParticleSystem_random1";
this._randomTexture.wrapU = 1;
this._randomTexture.wrapV = 1;
d = [];
for (let i = 0; i < maxTextureSize; ++i) {
d.push(Math.random());
d.push(Math.random());
d.push(Math.random());
d.push(Math.random());
}
this._randomTexture2 = new RawTexture(new Float32Array(d), maxTextureSize, 1, 5, sceneOrEngine, false, false, 1, 1);
this._randomTexture2.name = "GPUParticleSystem_random2";
this._randomTexture2.wrapU = 1;
this._randomTexture2.wrapV = 1;
this._randomTextureSize = maxTextureSize;
}
_reset() {
this._releaseBuffers();
}
_createVertexBuffers(updateBuffer, renderBuffer, spriteSource) {
const renderVertexBuffers = {};
renderVertexBuffers["position"] = renderBuffer.createVertexBuffer("position", 0, 3, this._attributesStrideSize, true);
let offset = 3;
renderVertexBuffers["age"] = renderBuffer.createVertexBuffer("age", offset, 1, this._attributesStrideSize, true);
offset += 1;
renderVertexBuffers["size"] = renderBuffer.createVertexBuffer("size", offset, 3, this._attributesStrideSize, true);
offset += 3;
renderVertexBuffers["life"] = renderBuffer.createVertexBuffer("life", offset, 1, this._attributesStrideSize, true);
offset += 1;
if (this._hasColorGradientColor2) {
// Expose `seed` to the render shader so it can pick a stable per-particle mix factor between
// the color1 and color2 rows of the color gradient texture.
renderVertexBuffers["seed"] = renderBuffer.createVertexBuffer("seed", offset, 4, this._attributesStrideSize, true);
}
offset += 4; // seed
if (this.billboardMode === ParticleSystem.BILLBOARDMODE_STRETCHED || this.billboardMode === ParticleSystem.BILLBOARDMODE_STRETCHED_LOCAL) {
renderVertexBuffers["direction"] = renderBuffer.createVertexBuffer("direction", offset, 3, this._attributesStrideSize, true);
}
offset += 3; // direction
if (this._platform.alignDataInBuffer) {
offset += 1;
}
if (this.particleEmitterType instanceof CustomParticleEmitter) {
offset += 3;
if (this._platform.alignDataInBuffer) {
offset += 1;
}
}
if (!this._colorGradientsTexture) {
renderVertexBuffers["color"] = renderBuffer.createVertexBuffer("color", offset, 4, this._attributesStrideSize, true);
offset += 4;
}
if (this._needsInitialDirection) {
renderVertexBuffers["initialDirection"] = renderBuffer.createVertexBuffer("initialDirection", offset, 3, this._attributesStrideSize, true);
offset += 3;
if (this._platform.alignDataInBuffer) {
offset += 1;
}
}
if (this.noiseTexture) {
renderVertexBuffers["noiseCoordinates1"] = renderBuffer.createVertexBuffer("noiseCoordinates1", offset, 3, this._attributesStrideSize, true);
offset += 3;
if (this._platform.alignDataInBuffer) {
offset += 1;
}
renderVertexBuffers["noiseCoordinates2"] = renderBuffer.createVertexBuffer("noiseCoordinates2", offset, 3, this._attributesStrideSize, true);
offset += 3;
if (this._platform.alignDataInBuffer) {
offset += 1;
}
}
renderVertexBuffers["angle"] = renderBuffer.createVertexBuffer("angle", offset, 1, this._attributesStrideSize, true);
if (this._angularSpeedGradientsTexture) {
offset++;
}
else {
offset += 2;
}
if (this._isAnimationSheetEnabled) {
renderVertexBuffers["cellIndex"] = renderBuffer.createVertexBuffer("cellIndex", offset, 1, this._attributesStrideSize, true);
offset += 1;
if (this.spriteRandomStartCell) {
renderVertexBuffers["cellStartOffset"] = renderBuffer.createVertexBuffer("cellStartOffset", offset, 1, this._attributesStrideSize, true);
}
}
renderVertexBuffers["offset"] = spriteSource.createVertexBuffer("offset", 0, 2);
renderVertexBuffers["uv"] = spriteSource.createVertexBuffer("uv", 2, 2);
this._renderVertexBuffers.push(renderVertexBuffers);
this._platform.createVertexBuffers(updateBuffer, renderVertexBuffers);
this.resetDrawCache();
}
_initialize(force = false) {
if (this._buffer0 && !force) {
return;
}
const engine = this._engine;
const data = [];
this._attributesStrideSize = 21;
this._targetIndex = 0;
if (this._platform.alignDataInBuffer) {
this._attributesStrideSize += 1;
}
if (this.particleEmitterType instanceof CustomParticleEmitter) {
this._attributesStrideSize += 3;
if (this._platform.alignDataInBuffer) {
this._attributesStrideSize += 1;
}
}
if (this._needsInitialDirection) {
this._attributesStrideSize += 3;
if (this._platform.alignDataInBuffer) {
this._attributesStrideSize += 1;
}
}
if (this._colorGradientsTexture) {
this._attributesStrideSize -= 4;
}
if (this._angularSpeedGradientsTexture) {
this._attributesStrideSize -= 1;
}
if (this._isAnimationSheetEnabled) {
this._attributesStrideSize += 1;
if (this.spriteRandomStartCell) {
this._attributesStrideSize += 1;
}
}
if (this.noiseTexture) {
this._attributesStrideSize += 6;
if (this._platform.alignDataInBuffer) {
this._attributesStrideSize += 2;
}
}
if (this._platform.alignDataInBuffer) {
this._attributesStrideSize += 3 - ((this._attributesStrideSize + 3) & 3); // round to multiple of 4
}
const usingCustomEmitter = this.particleEmitterType instanceof CustomParticleEmitter;
const tmpVector = TmpVectors.Vector3[0];
let offset = 0;
for (let particleIndex = 0; particleIndex < this._capacity; particleIndex++) {
// position
data.push(0.0);
data.push(0.0);
data.push(0.0);
// Age
data.push(0.0); // create the particle as a dead one to create a new one at start
// Size
data.push(0.0);
data.push(0.0);
data.push(0.0);
// life
data.push(0.0);
// Seed
data.push(Math.random());
data.push(Math.random());
data.push(Math.random());
data.push(Math.random());
// direction
if (usingCustomEmitter) {
this.particleEmitterType.particleDestinationGenerator(particleIndex, null, tmpVector);
data.push(tmpVector.x);
data.push(tmpVector.y);
data.push(tmpVector.z);
}
else {
data.push(0.0);
data.push(0.0);
data.push(0.0);
}
if (this._platform.alignDataInBuffer) {
data.push(0.0); // dummy0
}
offset += 16; // position, age, size, life, seed, direction, dummy0
if (usingCustomEmitter) {
this.particleEmitterType.particlePositionGenerator(particleIndex, null, tmpVector);
data.push(tmpVector.x);
data.push(tmpVector.y);
data.push(tmpVector.z);
if (this._platform.alignDataInBuffer) {
data.push(0.0); // dummy1
}
offset += 4;
}
if (!this._colorGradientsTexture) {
// color
data.push(0.0);
data.push(0.0);
data.push(0.0);
data.push(0.0);
offset += 4;
}
if (this._needsInitialDirection) {
// initialDirection
data.push(0.0);
data.push(0.0);
data.push(0.0);
if (this._platform.alignDataInBuffer) {
data.push(0.0); // dummy2
}
offset += 4;
}
if (this.noiseTexture) {
// Random coordinates for reading into noise texture
data.push(Math.random());
data.push(Math.random());
data.push(Math.random());
if (this._platform.alignDataInBuffer) {
data.push(0.0); // dummy3
}
data.push(Math.random());
data.push(Math.random());
data.push(Math.random());
if (this._platform.alignDataInBuffer) {
data.push(0.0); // dummy4
}
offset += 8;
}
// angle
data.push(0.0);
offset += 1;
if (!this._angularSpeedGradientsTexture) {
data.push(0.0);
offset += 1;
}
if (this._isAnimationSheetEnabled) {
data.push(0.0);
offset += 1;
if (this.spriteRandomStartCell) {
data.push(0.0);
offset += 1;
}
}
if (this._platform.alignDataInBuffer) {
let numDummies = 3 - ((offset + 3) & 3);
offset += numDummies;
while (numDummies-- > 0) {
data.push(0.0);
}
}
}
// Sprite data
const spriteData = new Float32Array([0.5, 0.5, 1, 1, -0.5, 0.5, 0, 1, 0.5, -0.5, 1, 0, -0.5, -0.5, 0, 0]);
const bufferData1 = this._platform.createParticleBuffer(data);
const bufferData2 = this._platform.createParticleBuffer(data);
// Buffers
this._buffer0 = new Buffer(engine, bufferData1, false, this._attributesStrideSize);
this._buffer1 = new Buffer(engine, bufferData2, false, this._attributesStrideSize);
this._spriteBuffer = new Buffer(engine, spriteData, false, 4);
// Update & Render vertex buffers
this._renderVertexBuffers = [];
this._createVertexBuffers(this._buffer0, this._buffer1, this._spriteBuffer);
this._createVertexBuffers(this._buffer1, this._buffer0, this._spriteBuffer);
// Links
this._sourceBuffer = this._buffer0;
this._targetBuffer = this._buffer1;
}
/**
* Forces the update effect to be recreated on the next render.
*/
_resetEffect() {
this._cachedUpdateDefines = "";
}
/** @internal */
_recreateUpdateEffect() {
this._createColorGradientTexture();
this._createSizeGradientTexture();
this._createAngularSpeedGradientTexture();
this._createVelocityGradientTexture();
this._createLimitVelocityGradientTexture();
this._createDragGradientTexture();
this._createMeshEmitterTextures();
let defines = this.particleEmitterType ? this.particleEmitterType.getEffectDefines() : "";
if (this._isBillboardBased) {
// Stretched local needs initialDirection in the buffer, which requires !BILLBOARD in the update shader.
// The render shader still uses BILLBOARD — that's handled separately in fillDefines().
if (this.billboardMode !== ParticleSystem.BILLBOARDMODE_STRETCHED_LOCAL) {
defines += "\n#define BILLBOARD";
}
}
if (this._colorGradientsTexture) {
defines += "\n#define COLORGRADIENTS";
}
if (this._sizeGradientsTexture) {
defines += "\n#define SIZEGRADIENTS";
}
if (this._angularSpeedGradientsTexture) {
defines += "\n#define ANGULARSPEEDGRADIENTS";
}
if (this._velocityGradientsTexture) {
defines += "\n#define VELOCITYGRADIENTS";
}
if (this._limitVelocityGradientsTexture) {
defines += "\n#define LIMITVELOCITYGRADIENTS";
}
if (this._dragGradientsTexture) {
defines += "\n#define DRAGGRADIENTS";
}
if (this._flowMap) {
defines += "\n#define FLOWMAP";
}
if (this.isAnimationSheetEnabled) {
defines += "\n#define ANIMATESHEET";
if (this.spriteRandomStartCell) {
defines += "\n#define ANIMATESHEETRANDOMSTART";
}
}
if (this.noiseTexture) {
defines += "\n#define NOISE";
}
if (this.isLocal) {
defines += "\n#define LOCAL";
}
if (this._attractors.length > 0) {
defines += "\n#define ATTRACTORS";
defines += "\n#define MAX_ATTRACTORS " + this.maxAttractors;
}
if (this._emitRateControl) {
defines += "\n#define EMITRATECTRL";
}
if (this._startSizeGradients && this._startSizeGradients.length > 0) {
defines += "\n#define STARTSIZEGRADIENTS";
}
if (this._lifeTimeGradients && this._lifeTimeGradients.length > 0) {
defines += "\n#define LIFETIMEGRADIENTS";
}
if (this.particleEmitterType instanceof MeshParticleEmitter && this._meshPositionTexture) {
defines += "\n#define MESHEMITTER";
if (this._meshNormalTexture) {
defines += "\n#define MESHNORMALS";
}
}
if (this._platform.isUpdateBufferCreated() && this._cachedUpdateDefines === defines) {
return this._platform.isUpdateBufferReady();
}
this._cachedUpdateDefines = defines;
this._updateBuffer = this._platform.createUpdateBuffer(defines);
return this._platform.isUpdateBufferReady();
}
/**
* @internal
*/
_getWrapper(blendMode) {
const customWrapper = this._getCustomDrawWrapper(blendMode);
if (customWrapper?.effect) {
return customWrapper;
}
const defines = [];
this.fillDefines(defines, blendMode);
// Effect
let drawWrapper = this._drawWrappers[blendMode];
if (!drawWrapper) {
drawWrapper = new DrawWrapper(this._engine);
if (drawWrapper.drawContext) {
drawWrapper.drawContext.useInstancing = true;
}
this._drawWrappers[blendMode] = drawWrapper;
}
const join = defines.join("\n");
if (drawWrapper.defines !== join) {
const attributes = [];
const uniforms = [];
const samplers = [];
this.fillUniformsAttributesAndSamplerNames(uniforms, attributes, samplers);
drawWrapper.setEffect(this._engine.createEffect("gpuRenderParticles", attributes, uniforms, samplers, join), join);
}
return drawWrapper;
}
/**
* @internal
*/
static _GetAttributeNamesOrOptions(hasColorGradients = false, isAnimationSheetEnabled = false, isBillboardBased = false, isBillboardStretched = false, isBillboardStretchedLocal = false, hasColorGradientColor2 = false) {
const attributeNamesOrOptions = [VertexBuffer.PositionKind, "age", "life", "size", "angle"];
if (!hasColorGradients) {
attributeNamesOrOptions.push(VertexBuffer.ColorKind);
}
else if (hasColorGradientColor2) {
// When packing a color1/color2 range into the gradient texture, the render shader needs the
// particle's persiste