playcanvas
Version:
Open-source WebGL/WebGPU 3D engine for the web
1,824 lines (1,823 loc) • 47.7 kB
JavaScript
var __defProp = Object.defineProperty;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
import { LAYERID_DEPTH } from "../../../scene/constants.js";
import { Mesh } from "../../../scene/mesh.js";
import { ParticleEmitter } from "../../../scene/particle-system/particle-emitter.js";
import { Asset } from "../../asset/asset.js";
import { Component } from "../component.js";
const SIMPLE_PROPERTIES = [
"emitterExtents",
"emitterRadius",
"emitterExtentsInner",
"emitterRadiusInner",
"loop",
"initialVelocity",
"animSpeed",
"normalMap",
"particleNormal"
];
const COMPLEX_PROPERTIES = [
"numParticles",
"lifetime",
"rate",
"rate2",
"startAngle",
"startAngle2",
"lighting",
"halfLambert",
"intensity",
"wrap",
"wrapBounds",
"depthWrite",
"noFog",
"sort",
"stretch",
"alignToMotion",
"preWarm",
"emitterShape",
"animTilesX",
"animTilesY",
"animStartFrame",
"animNumFrames",
"animNumAnimations",
"animIndex",
"randomizeAnimIndex",
"animLoop",
"colorMap",
"localSpace",
"screenSpace",
"orientation"
];
const GRAPH_PROPERTIES = [
"scaleGraph",
"scaleGraph2",
"colorGraph",
"colorGraph2",
"alphaGraph",
"alphaGraph2",
"velocityGraph",
"velocityGraph2",
"localVelocityGraph",
"localVelocityGraph2",
"rotationSpeedGraph",
"rotationSpeedGraph2",
"radialSpeedGraph",
"radialSpeedGraph2"
];
const ASSET_PROPERTIES = ["colorMapAsset", "normalMapAsset", "meshAsset", "renderAsset"];
let depthLayer;
class ParticleSystemComponent extends Component {
/**
* Create a new ParticleSystemComponent.
*
* @param {ParticleSystemComponentSystem} system - The ComponentSystem that created this Component.
* @param {Entity} entity - The Entity this Component is attached to.
*/
constructor(system, entity) {
super(system, entity);
/** @private */
__publicField(this, "_requestedDepth", false);
/** @private */
__publicField(this, "_drawOrder", 0);
/**
* @type {EventHandle|null}
* @private
*/
__publicField(this, "_evtLayersChanged", null);
/**
* @type {EventHandle|null}
* @private
*/
__publicField(this, "_evtLayerAdded", null);
/**
* @type {EventHandle|null}
* @private
*/
__publicField(this, "_evtLayerRemoved", null);
/**
* @type {EventHandle|null}
* @private
*/
__publicField(this, "_evtSetMeshes", null);
this.on("set_colorMapAsset", this.onSetColorMapAsset, this);
this.on("set_normalMapAsset", this.onSetNormalMapAsset, this);
this.on("set_meshAsset", this.onSetMeshAsset, this);
this.on("set_mesh", this.onSetMesh, this);
this.on("set_renderAsset", this.onSetRenderAsset, this);
this.on("set_loop", this.onSetLoop, this);
this.on("set_blendType", this.onSetBlendType, this);
this.on("set_depthSoftening", this.onSetDepthSoftening, this);
this.on("set_layers", this.onSetLayers, this);
SIMPLE_PROPERTIES.forEach((prop) => {
this.on(`set_${prop}`, this.onSetSimpleProperty, this);
});
COMPLEX_PROPERTIES.forEach((prop) => {
this.on(`set_${prop}`, this.onSetComplexProperty, this);
});
GRAPH_PROPERTIES.forEach((prop) => {
this.on(`set_${prop}`, this.onSetGraphProperty, this);
});
}
/**
* Sets the enabled state of the component.
*
* @type {boolean}
*/
set enabled(arg) {
this._setValue("enabled", arg);
}
/**
* Gets the enabled state of the component.
*
* @type {boolean}
*/
get enabled() {
return this.data.enabled;
}
/**
* Sets whether the particle system plays automatically on creation. If set to false, it is
* necessary to call {@link play} for the particle system to play. Defaults to true.
*
* @type {boolean}
*/
set autoPlay(arg) {
this._setValue("autoPlay", arg);
}
/**
* Gets whether the particle system plays automatically on creation.
*
* @type {boolean}
*/
get autoPlay() {
return this.data.autoPlay;
}
/**
* Sets the maximum number of simulated particles.
*
* @type {number}
*/
set numParticles(arg) {
this._setValue("numParticles", arg);
}
/**
* Gets the maximum number of simulated particles.
*
* @type {number}
*/
get numParticles() {
return this.data.numParticles;
}
/**
* Sets the length of time in seconds between a particle's birth and its death.
*
* @type {number}
*/
set lifetime(arg) {
this._setValue("lifetime", arg);
}
/**
* Gets the length of time in seconds between a particle's birth and its death.
*
* @type {number}
*/
get lifetime() {
return this.data.lifetime;
}
/**
* Sets the minimal interval in seconds between particle births.
*
* @type {number}
*/
set rate(arg) {
this._setValue("rate", arg);
}
/**
* Gets the minimal interval in seconds between particle births.
*
* @type {number}
*/
get rate() {
return this.data.rate;
}
/**
* Sets the maximal interval in seconds between particle births.
*
* @type {number}
*/
set rate2(arg) {
this._setValue("rate2", arg);
}
/**
* Gets the maximal interval in seconds between particle births.
*
* @type {number}
*/
get rate2() {
return this.data.rate2;
}
/**
* Sets the minimal initial Euler angle of a particle.
*
* @type {number}
*/
set startAngle(arg) {
this._setValue("startAngle", arg);
}
/**
* Gets the minimal initial Euler angle of a particle.
*
* @type {number}
*/
get startAngle() {
return this.data.startAngle;
}
/**
* Sets the maximal initial Euler angle of a particle.
*
* @type {number}
*/
set startAngle2(arg) {
this._setValue("startAngle2", arg);
}
/**
* Gets the maximal initial Euler angle of a particle.
*
* @type {number}
*/
get startAngle2() {
return this.data.startAngle2;
}
/**
* Sets whether the particle system loops.
*
* @type {boolean}
*/
set loop(arg) {
this._setValue("loop", arg);
}
/**
* Gets whether the particle system loops.
*
* @type {boolean}
*/
get loop() {
return this.data.loop;
}
/**
* Sets whether the particle system will be initialized as though it has already completed a
* full cycle. This only works with looping particle systems.
*
* @type {boolean}
*/
set preWarm(arg) {
this._setValue("preWarm", arg);
}
/**
* Gets whether the particle system will be initialized as though it has already completed a
* full cycle.
*
* @type {boolean}
*/
get preWarm() {
return this.data.preWarm;
}
/**
* Sets whether particles will be lit by ambient and directional lights.
*
* @type {boolean}
*/
set lighting(arg) {
this._setValue("lighting", arg);
}
/**
* Gets whether particles will be lit by ambient and directional lights.
*
* @type {boolean}
*/
get lighting() {
return this.data.lighting;
}
/**
* Sets whether Half Lambert lighting is enabled. Enabling Half Lambert lighting avoids
* particles looking too flat in shadowed areas. It is a completely non-physical lighting model
* but can give more pleasing visual results.
*
* @type {boolean}
*/
set halfLambert(arg) {
this._setValue("halfLambert", arg);
}
/**
* Gets whether Half Lambert lighting is enabled.
*
* @type {boolean}
*/
get halfLambert() {
return this.data.halfLambert;
}
/**
* Sets the color multiplier.
*
* @type {number}
*/
set intensity(arg) {
this._setValue("intensity", arg);
}
/**
* Gets the color multiplier.
*
* @type {number}
*/
get intensity() {
return this.data.intensity;
}
/**
* Sets whether depth writes is enabled. If enabled, the particles will write to the depth
* buffer. If disabled, the depth buffer is left unchanged and particles will be guaranteed to
* overwrite one another in the order in which they are rendered.
*
* @type {boolean}
*/
set depthWrite(arg) {
this._setValue("depthWrite", arg);
}
/**
* Gets whether depth writes is enabled.
*
* @type {boolean}
*/
get depthWrite() {
return this.data.depthWrite;
}
/**
* Sets whether fogging is ignored.
*
* @type {boolean}
*/
set noFog(arg) {
this._setValue("noFog", arg);
}
/**
* Gets whether fogging is ignored.
*
* @type {boolean}
*/
get noFog() {
return this.data.noFog;
}
/**
* Sets whether depth softening is enabled. Controls fading of particles near their
* intersections with scene geometry. This effect, when it's non-zero, requires scene depth map
* to be rendered. Multiple depth-dependent effects can share the same map, but if you only use
* it for particles, bear in mind that it can double engine draw calls.
*
* @type {number}
*/
set depthSoftening(arg) {
this._setValue("depthSoftening", arg);
}
/**
* Gets whether depth softening is enabled.
*
* @type {number}
*/
get depthSoftening() {
return this.data.depthSoftening;
}
/**
* Sets the particle sorting mode. Forces CPU simulation, so be careful.
*
* - {@link PARTICLESORT_NONE}: No sorting, particles are drawn in arbitrary order. Can be
* simulated on GPU.
* - {@link PARTICLESORT_DISTANCE}: Sorting based on distance to the camera. CPU only.
* - {@link PARTICLESORT_NEWER_FIRST}: Newer particles are drawn first. CPU only.
* - {@link PARTICLESORT_OLDER_FIRST}: Older particles are drawn first. CPU only.
*
* @type {number}
*/
set sort(arg) {
this._setValue("sort", arg);
}
/**
* Gets the particle sorting mode.
*
* @type {number}
*/
get sort() {
return this.data.sort;
}
/**
* Sets how particles are blended when being written to the currently active render target.
* Can be:
*
* - {@link BLEND_SUBTRACTIVE}: Subtract the color of the source fragment from the destination
* fragment and write the result to the frame buffer.
* - {@link BLEND_ADDITIVE}: Add the color of the source fragment to the destination fragment and
* write the result to the frame buffer.
* - {@link BLEND_NORMAL}: Enable simple translucency for materials such as glass. This is
* equivalent to enabling a source blend mode of {@link BLENDMODE_SRC_ALPHA} and
* a destination
* blend mode of {@link BLENDMODE_ONE_MINUS_SRC_ALPHA}.
* - {@link BLEND_NONE}: Disable blending.
* - {@link BLEND_PREMULTIPLIED}: Similar to {@link BLEND_NORMAL} expect
* the source fragment is
* assumed to have already been multiplied by the source alpha value.
* - {@link BLEND_MULTIPLICATIVE}: Multiply the color of the source fragment by the color of the
* destination fragment and write the result to the frame buffer.
* - {@link BLEND_ADDITIVEALPHA}: Same as {@link BLEND_ADDITIVE} except
* the source RGB is
* multiplied by the source alpha.
*
* @type {number}
*/
set blendType(arg) {
this._setValue("blendType", arg);
}
/**
* Gets how particles are blended when being written to the currently active render target.
*
* @type {number}
*/
get blendType() {
return this.data.blendType;
}
/**
* Sets how much particles are stretched in their direction of motion. This is a value in world
* units that controls the amount by which particles are stretched based on their velocity.
* Particles are stretched from their center towards their previous position.
*
* @type {number}
*/
set stretch(arg) {
this._setValue("stretch", arg);
}
/**
* Gets how much particles are stretched in their direction of motion.
*
* @type {number}
*/
get stretch() {
return this.data.stretch;
}
/**
* Sets whether particles are oriented in their direction of motion or not.
*
* @type {boolean}
*/
set alignToMotion(arg) {
this._setValue("alignToMotion", arg);
}
/**
* Gets whether particles are oriented in their direction of motion or not.
*
* @type {boolean}
*/
get alignToMotion() {
return this.data.alignToMotion;
}
/**
* Sets the shape of the emitter. Defines the bounds inside which particles are spawned. Also
* affects the direction of initial velocity.
*
* - {@link EMITTERSHAPE_BOX}: Box shape parameterized by emitterExtents. Initial velocity is
* directed towards local Z axis.
* - {@link EMITTERSHAPE_SPHERE}: Sphere shape parameterized by emitterRadius. Initial velocity is
* directed outwards from the center.
*
* @type {number}
*/
set emitterShape(arg) {
this._setValue("emitterShape", arg);
}
/**
* Gets the shape of the emitter.
*
* @type {number}
*/
get emitterShape() {
return this.data.emitterShape;
}
/**
* Sets the extents of a local space bounding box within which particles are spawned at random
* positions. This only applies to particle system with the shape `EMITTERSHAPE_BOX`.
*
* @type {Vec3}
*/
set emitterExtents(arg) {
this._setValue("emitterExtents", arg);
}
/**
* Gets the extents of a local space bounding box within which particles are spawned at random
* positions.
*
* @type {Vec3}
*/
get emitterExtents() {
return this.data.emitterExtents;
}
/**
* Sets the exception of extents of a local space bounding box within which particles are not
* spawned. It is aligned to the center of emitterExtents. This only applies to particle system
* with the shape `EMITTERSHAPE_BOX`.
*
* @type {Vec3}
*/
set emitterExtentsInner(arg) {
this._setValue("emitterExtentsInner", arg);
}
/**
* Gets the exception of extents of a local space bounding box within which particles are not
* spawned.
*
* @type {Vec3}
*/
get emitterExtentsInner() {
return this.data.emitterExtentsInner;
}
/**
* Sets the radius within which particles are spawned at random positions. This only applies to
* particle system with the shape `EMITTERSHAPE_SPHERE`.
*
* @type {number}
*/
set emitterRadius(arg) {
this._setValue("emitterRadius", arg);
}
/**
* Gets the radius within which particles are spawned at random positions.
*
* @type {number}
*/
get emitterRadius() {
return this.data.emitterRadius;
}
/**
* Sets the inner radius within which particles are not spawned. This only applies to particle
* system with the shape `EMITTERSHAPE_SPHERE`.
*
* @type {number}
*/
set emitterRadiusInner(arg) {
this._setValue("emitterRadiusInner", arg);
}
/**
* Gets the inner radius within which particles are not spawned.
*
* @type {number}
*/
get emitterRadiusInner() {
return this.data.emitterRadiusInner;
}
/**
* Sets the magnitude of the initial emitter velocity. Direction is given by emitter shape.
*
* @type {number}
*/
set initialVelocity(arg) {
this._setValue("initialVelocity", arg);
}
/**
* Gets the magnitude of the initial emitter velocity.
*
* @type {number}
*/
get initialVelocity() {
return this.data.initialVelocity;
}
/**
* Sets whether particles wrap based on the set wrap bounds.
*
* @type {boolean}
*/
set wrap(arg) {
this._setValue("wrap", arg);
}
/**
* Gets whether particles wrap based on the set wrap bounds.
*
* @type {boolean}
*/
get wrap() {
return this.data.wrap;
}
/**
* Sets the wrap bounds of the particle system. This is half extents of a world space box
* volume centered on the owner entity's position. If a particle crosses the boundary of one
* side of the volume, it teleports to the opposite side.
*
* @type {Vec3}
*/
set wrapBounds(arg) {
this._setValue("wrapBounds", arg);
}
/**
* Gets the wrap bounds of the particle system.
*
* @type {Vec3}
*/
get wrapBounds() {
return this.data.wrapBounds;
}
/**
* Sets whether particles move with respect to the emitter's transform rather then world space.
*
* @type {boolean}
*/
set localSpace(arg) {
this._setValue("localSpace", arg);
}
/**
* Gets whether particles move with respect to the emitter's transform rather then world space.
*
* @type {boolean}
*/
get localSpace() {
return this.data.localSpace;
}
/**
* Sets whether particles are rendered in 2D screen space. This needs to be set when particle
* system is part of hierarchy with {@link ScreenComponent} as its ancestor, and allows
* particle system to integrate with the rendering of {@link ElementComponent}s. Note that an
* entity with ParticleSystem component cannot be parented directly to {@link ScreenComponent},
* but has to be a child of a {@link ElementComponent}, for example {@link LayoutGroupComponent}.
*
* @type {boolean}
*/
set screenSpace(arg) {
this._setValue("screenSpace", arg);
}
/**
* Gets whether particles are rendered in 2D screen space.
*
* @type {boolean}
*/
get screenSpace() {
return this.data.screenSpace;
}
/**
* Sets the {@link Asset} used to set the colorMap.
*
* @type {Asset}
*/
set colorMapAsset(arg) {
this._setValue("colorMapAsset", arg);
}
/**
* Gets the {@link Asset} used to set the colorMap.
*
* @type {Asset}
*/
get colorMapAsset() {
return this.data.colorMapAsset;
}
/**
* Sets the {@link Asset} used to set the normalMap.
*
* @type {Asset}
*/
set normalMapAsset(arg) {
this._setValue("normalMapAsset", arg);
}
/**
* Gets the {@link Asset} used to set the normalMap.
*
* @type {Asset}
*/
get normalMapAsset() {
return this.data.normalMapAsset;
}
/**
* Sets the polygonal mesh to be used as a particle. Only first vertex/index buffer is used.
* Vertex buffer must contain local position at first 3 floats of each vertex.
*
* @type {Mesh}
*/
set mesh(arg) {
this._setValue("mesh", arg);
}
/**
* Gets the polygonal mesh to be used as a particle.
*
* @type {Mesh}
*/
get mesh() {
return this.data.mesh;
}
/**
* Sets the {@link Asset} used to set the mesh.
*
* @type {Asset}
*/
set meshAsset(arg) {
this._setValue("meshAsset", arg);
}
/**
* Gets the {@link Asset} used to set the mesh.
*
* @type {Asset}
*/
get meshAsset() {
return this.data.meshAsset;
}
/**
* Sets the Render {@link Asset} used to set the mesh.
*
* @type {Asset}
*/
set renderAsset(arg) {
this._setValue("renderAsset", arg);
}
/**
* Gets the Render {@link Asset} used to set the mesh.
*
* @type {Asset}
*/
get renderAsset() {
return this.data.renderAsset;
}
/**
* Sets the particle orientation mode. Can be:
*
* - {@link PARTICLEORIENTATION_SCREEN}: Particles are facing camera.
* - {@link PARTICLEORIENTATION_WORLD}: User defined world space normal (particleNormal) to set
* planes orientation.
* - {@link PARTICLEORIENTATION_EMITTER}: Similar to previous, but the normal is affected by
* emitter (entity) transformation.
*
* @type {number}
*/
set orientation(arg) {
this._setValue("orientation", arg);
}
/**
* Gets the particle orientation mode.
*
* @type {number}
*/
get orientation() {
return this.data.orientation;
}
/**
* Sets the particle normal. This only applies to particle system with the orientation modes
* `PARTICLEORIENTATION_WORLD` and `PARTICLEORIENTATION_EMITTER`.
*
* @type {Vec3}
*/
set particleNormal(arg) {
this._setValue("particleNormal", arg);
}
/**
* Gets the particle normal.
*
* @type {Vec3}
*/
get particleNormal() {
return this.data.particleNormal;
}
/**
* Sets the local space velocity graph.
*
* @type {CurveSet}
*/
set localVelocityGraph(arg) {
this._setValue("localVelocityGraph", arg);
}
/**
* Gets the local space velocity graph.
*
* @type {CurveSet}
*/
get localVelocityGraph() {
return this.data.localVelocityGraph;
}
/**
* Sets the second velocity graph. If not null, particles pick random values between
* localVelocityGraph and localVelocityGraph2.
*
* @type {CurveSet}
*/
set localVelocityGraph2(arg) {
this._setValue("localVelocityGraph2", arg);
}
/**
* Gets the second velocity graph.
*
* @type {CurveSet}
*/
get localVelocityGraph2() {
return this.data.localVelocityGraph2;
}
/**
* Sets the world space velocity graph.
*
* @type {CurveSet}
*/
set velocityGraph(arg) {
this._setValue("velocityGraph", arg);
}
/**
* Gets the world space velocity graph.
*
* @type {CurveSet}
*/
get velocityGraph() {
return this.data.velocityGraph;
}
/**
* Sets the second world space velocity graph. If not null, particles pick random values
* between velocityGraph and velocityGraph2.
*
* @type {CurveSet}
*/
set velocityGraph2(arg) {
this._setValue("velocityGraph2", arg);
}
/**
* Gets the second world space velocity graph.
*
* @type {CurveSet}
*/
get velocityGraph2() {
return this.data.velocityGraph2;
}
/**
* Sets the rotation speed graph.
*
* @type {Curve}
*/
set rotationSpeedGraph(arg) {
this._setValue("rotationSpeedGraph", arg);
}
/**
* Gets the rotation speed graph.
*
* @type {Curve}
*/
get rotationSpeedGraph() {
return this.data.rotationSpeedGraph;
}
/**
* Sets the second rotation speed graph. If not null, particles pick random values between
* rotationSpeedGraph and rotationSpeedGraph2.
*
* @type {Curve}
*/
set rotationSpeedGraph2(arg) {
this._setValue("rotationSpeedGraph2", arg);
}
/**
* Gets the second rotation speed graph.
*
* @type {Curve}
*/
get rotationSpeedGraph2() {
return this.data.rotationSpeedGraph2;
}
/**
* Sets the radial speed graph. Velocity vector points from emitter origin to particle position.
*
* @type {Curve}
*/
set radialSpeedGraph(arg) {
this._setValue("radialSpeedGraph", arg);
}
/**
* Gets the radial speed graph.
*
* @type {Curve}
*/
get radialSpeedGraph() {
return this.data.radialSpeedGraph;
}
/**
* Sets the second radial speed graph. If not null, particles pick random values between
* radialSpeedGraph and radialSpeedGraph2. Velocity vector points from emitter origin to
* particle position.
*
* @type {Curve}
*/
set radialSpeedGraph2(arg) {
this._setValue("radialSpeedGraph2", arg);
}
/**
* Gets the second radial speed graph.
*
* @type {Curve}
*/
get radialSpeedGraph2() {
return this.data.radialSpeedGraph2;
}
/**
* Sets the scale graph.
*
* @type {Curve}
*/
set scaleGraph(arg) {
this._setValue("scaleGraph", arg);
}
/**
* Gets the scale graph.
*
* @type {Curve}
*/
get scaleGraph() {
return this.data.scaleGraph;
}
/**
* Sets the second scale graph. If not null, particles pick random values between `scaleGraph`
* and `scaleGraph2`.
*
* @type {Curve}
*/
set scaleGraph2(arg) {
this._setValue("scaleGraph2", arg);
}
/**
* Gets the second scale graph.
*
* @type {Curve}
*/
get scaleGraph2() {
return this.data.scaleGraph2;
}
/**
* Sets the color graph.
*
* @type {CurveSet}
*/
set colorGraph(arg) {
this._setValue("colorGraph", arg);
}
/**
* Gets the color graph.
*
* @type {CurveSet}
*/
get colorGraph() {
return this.data.colorGraph;
}
/**
* Sets the second color graph. If not null, particles pick random values between `colorGraph`
* and `colorGraph2`.
*
* @type {CurveSet}
*/
set colorGraph2(arg) {
this._setValue("colorGraph2", arg);
}
/**
* Gets the second color graph.
*
* @type {CurveSet}
*/
get colorGraph2() {
return this.data.colorGraph2;
}
/**
* Sets the alpha graph.
*
* @type {Curve}
*/
set alphaGraph(arg) {
this._setValue("alphaGraph", arg);
}
/**
* Gets the alpha graph.
*
* @type {Curve}
*/
get alphaGraph() {
return this.data.alphaGraph;
}
/**
* Sets the second alpha graph. If not null, particles pick random values between `alphaGraph`
* and `alphaGraph2`.
*
* @type {Curve}
*/
set alphaGraph2(arg) {
this._setValue("alphaGraph2", arg);
}
/**
* Gets the second alpha graph.
*
* @type {Curve}
*/
get alphaGraph2() {
return this.data.alphaGraph2;
}
/**
* Sets the color map texture to apply to all particles in the system. If no texture is
* assigned, a default spot texture is used.
*
* @type {Texture}
*/
set colorMap(arg) {
this._setValue("colorMap", arg);
}
/**
* Gets the color map texture to apply to all particles in the system.
*
* @type {Texture}
*/
get colorMap() {
return this.data.colorMap;
}
/**
* Sets the normal map texture to apply to all particles in the system. If no texture is
* assigned, an approximate spherical normal is calculated for each vertex.
*
* @type {Texture}
*/
set normalMap(arg) {
this._setValue("normalMap", arg);
}
/**
* Gets the normal map texture to apply to all particles in the system.
*
* @type {Texture}
*/
get normalMap() {
return this.data.normalMap;
}
/**
* Sets the number of horizontal tiles in the sprite sheet.
*
* @type {number}
*/
set animTilesX(arg) {
this._setValue("animTilesX", arg);
}
/**
* Gets the number of horizontal tiles in the sprite sheet.
*
* @type {number}
*/
get animTilesX() {
return this.data.animTilesX;
}
/**
* Sets the number of vertical tiles in the sprite sheet.
*
* @type {number}
*/
set animTilesY(arg) {
this._setValue("animTilesY", arg);
}
/**
* Gets the number of vertical tiles in the sprite sheet.
*
* @type {number}
*/
get animTilesY() {
return this.data.animTilesY;
}
/**
* Sets the sprite sheet frame that the animation should begin playing from. Indexed from the
* start of the current animation.
*
* @type {number}
*/
set animStartFrame(arg) {
this._setValue("animStartFrame", arg);
}
/**
* Gets the sprite sheet frame that the animation should begin playing from.
*
* @type {number}
*/
get animStartFrame() {
return this.data.animStartFrame;
}
/**
* Sets the number of sprite sheet frames in the current sprite sheet animation. The number of
* animations multiplied by number of frames should be a value less than `animTilesX`
* multiplied by `animTilesY`.
*
* @type {number}
*/
set animNumFrames(arg) {
this._setValue("animNumFrames", arg);
}
/**
* Gets the number of sprite sheet frames in the current sprite sheet animation.
*
* @type {number}
*/
get animNumFrames() {
return this.data.animNumFrames;
}
/**
* Sets the number of sprite sheet animations contained within the current sprite sheet. The
* number of animations multiplied by number of frames should be a value less than `animTilesX`
* multiplied by `animTilesY`.
*
* @type {number}
*/
set animNumAnimations(arg) {
this._setValue("animNumAnimations", arg);
}
/**
* Gets the number of sprite sheet animations contained within the current sprite sheet.
*
* @type {number}
*/
get animNumAnimations() {
return this.data.animNumAnimations;
}
/**
* Sets the index of the animation to play. When `animNumAnimations` is greater than 1, the
* sprite sheet animation index determines which animation the particle system should play.
*
* @type {number}
*/
set animIndex(arg) {
this._setValue("animIndex", arg);
}
/**
* Gets the index of the animation to play.
*
* @type {number}
*/
get animIndex() {
return this.data.animIndex;
}
/**
* Sets whether each particle emitted by the system will play a random animation from the
* sprite sheet, up to `animNumAnimations`.
*
* @type {boolean}
*/
set randomizeAnimIndex(arg) {
this._setValue("randomizeAnimIndex", arg);
}
/**
* Gets whether each particle emitted by the system will play a random animation from the
* sprite sheet, up to `animNumAnimations`.
*
* @type {boolean}
*/
get randomizeAnimIndex() {
return this.data.randomizeAnimIndex;
}
/**
* Sets the sprite sheet animation speed. 1 = particle lifetime, 2 = double the particle
* lifetime, etc.
*
* @type {number}
*/
set animSpeed(arg) {
this._setValue("animSpeed", arg);
}
/**
* Gets the sprite sheet animation speed.
*
* @type {number}
*/
get animSpeed() {
return this.data.animSpeed;
}
/**
* Sets whether the sprite sheet animation plays once or loops continuously.
*
* @type {boolean}
*/
set animLoop(arg) {
this._setValue("animLoop", arg);
}
/**
* Gets whether the sprite sheet animation plays once or loops continuously.
*
* @type {boolean}
*/
get animLoop() {
return this.data.animLoop;
}
/**
* Sets the array of layer IDs ({@link Layer#id}) to which this particle system should belong.
* Don't push/pop/splice or modify this array. If you want to change it, set a new one instead.
*
* @type {number[]}
*/
set layers(arg) {
this._setValue("layers", arg);
}
/**
* Gets the array of layer IDs ({@link Layer#id}) to which this particle system belongs.
*
* @type {number[]}
*/
get layers() {
return this.data.layers;
}
/**
* Sets the draw order of the component. A higher value means that the component will be
* rendered on top of other components in the same layer. This is not used unless the layer's
* sort order is set to {@link SORTMODE_MANUAL}.
*
* @type {number}
*/
set drawOrder(drawOrder) {
this._drawOrder = drawOrder;
if (this.emitter) {
this.emitter.drawOrder = drawOrder;
}
}
/**
* Gets the draw order of the component.
*
* @type {number}
*/
get drawOrder() {
return this._drawOrder;
}
/** @ignore */
_setValue(name, value) {
const data = this.data;
const oldValue = data[name];
data[name] = value;
this.fire("set", name, oldValue, value);
}
addMeshInstanceToLayers() {
if (!this.emitter) return;
for (let i = 0; i < this.layers.length; i++) {
const layer = this.system.app.scene.layers.getLayerById(this.layers[i]);
if (!layer) continue;
layer.addMeshInstances([this.emitter.meshInstance]);
this.emitter._layer = layer;
}
}
removeMeshInstanceFromLayers() {
if (!this.emitter) return;
for (let i = 0; i < this.layers.length; i++) {
const layer = this.system.app.scene.layers.getLayerById(this.layers[i]);
if (!layer) continue;
layer.removeMeshInstances([this.emitter.meshInstance]);
}
}
onSetLayers(name, oldValue, newValue) {
if (!this.emitter) return;
for (let i = 0; i < oldValue.length; i++) {
const layer = this.system.app.scene.layers.getLayerById(oldValue[i]);
if (!layer) continue;
layer.removeMeshInstances([this.emitter.meshInstance]);
}
if (!this.enabled || !this.entity.enabled) return;
for (let i = 0; i < newValue.length; i++) {
const layer = this.system.app.scene.layers.getLayerById(newValue[i]);
if (!layer) continue;
layer.addMeshInstances([this.emitter.meshInstance]);
}
}
onLayersChanged(oldComp, newComp) {
this.addMeshInstanceToLayers();
oldComp.off("add", this.onLayerAdded, this);
oldComp.off("remove", this.onLayerRemoved, this);
newComp.on("add", this.onLayerAdded, this);
newComp.on("remove", this.onLayerRemoved, this);
}
onLayerAdded(layer) {
if (!this.emitter) return;
const index = this.layers.indexOf(layer.id);
if (index < 0) return;
layer.addMeshInstances([this.emitter.meshInstance]);
}
onLayerRemoved(layer) {
if (!this.emitter) return;
const index = this.layers.indexOf(layer.id);
if (index < 0) return;
layer.removeMeshInstances([this.emitter.meshInstance]);
}
_bindColorMapAsset(asset) {
asset.on("load", this._onColorMapAssetLoad, this);
asset.on("unload", this._onColorMapAssetUnload, this);
asset.on("remove", this._onColorMapAssetRemove, this);
asset.on("change", this._onColorMapAssetChange, this);
if (asset.resource) {
this._onColorMapAssetLoad(asset);
} else {
if (!this.enabled || !this.entity.enabled) return;
this.system.app.assets.load(asset);
}
}
_unbindColorMapAsset(asset) {
asset.off("load", this._onColorMapAssetLoad, this);
asset.off("unload", this._onColorMapAssetUnload, this);
asset.off("remove", this._onColorMapAssetRemove, this);
asset.off("change", this._onColorMapAssetChange, this);
}
_onColorMapAssetLoad(asset) {
this.colorMap = asset.resource;
}
_onColorMapAssetUnload(asset) {
this.colorMap = null;
}
_onColorMapAssetRemove(asset) {
this._onColorMapAssetUnload(asset);
}
_onColorMapAssetChange(asset) {
}
onSetColorMapAsset(name, oldValue, newValue) {
const assets = this.system.app.assets;
if (oldValue) {
const asset = assets.get(oldValue);
if (asset) {
this._unbindColorMapAsset(asset);
}
}
if (newValue) {
if (newValue instanceof Asset) {
this.data.colorMapAsset = newValue.id;
newValue = newValue.id;
}
const asset = assets.get(newValue);
if (asset) {
this._bindColorMapAsset(asset);
} else {
assets.once(`add:${newValue}`, (asset2) => {
this._bindColorMapAsset(asset2);
});
}
} else {
this.colorMap = null;
}
}
_bindNormalMapAsset(asset) {
asset.on("load", this._onNormalMapAssetLoad, this);
asset.on("unload", this._onNormalMapAssetUnload, this);
asset.on("remove", this._onNormalMapAssetRemove, this);
asset.on("change", this._onNormalMapAssetChange, this);
if (asset.resource) {
this._onNormalMapAssetLoad(asset);
} else {
if (!this.enabled || !this.entity.enabled) return;
this.system.app.assets.load(asset);
}
}
_unbindNormalMapAsset(asset) {
asset.off("load", this._onNormalMapAssetLoad, this);
asset.off("unload", this._onNormalMapAssetUnload, this);
asset.off("remove", this._onNormalMapAssetRemove, this);
asset.off("change", this._onNormalMapAssetChange, this);
}
_onNormalMapAssetLoad(asset) {
this.normalMap = asset.resource;
}
_onNormalMapAssetUnload(asset) {
this.normalMap = null;
}
_onNormalMapAssetRemove(asset) {
this._onNormalMapAssetUnload(asset);
}
_onNormalMapAssetChange(asset) {
}
onSetNormalMapAsset(name, oldValue, newValue) {
const assets = this.system.app.assets;
if (oldValue) {
const asset = assets.get(oldValue);
if (asset) {
this._unbindNormalMapAsset(asset);
}
}
if (newValue) {
if (newValue instanceof Asset) {
this.data.normalMapAsset = newValue.id;
newValue = newValue.id;
}
const asset = assets.get(newValue);
if (asset) {
this._bindNormalMapAsset(asset);
} else {
assets.once(`add:${newValue}`, (asset2) => {
this._bindNormalMapAsset(asset2);
});
}
} else {
this.normalMap = null;
}
}
_bindMeshAsset(asset) {
asset.on("load", this._onMeshAssetLoad, this);
asset.on("unload", this._onMeshAssetUnload, this);
asset.on("remove", this._onMeshAssetRemove, this);
asset.on("change", this._onMeshAssetChange, this);
if (asset.resource) {
this._onMeshAssetLoad(asset);
} else {
if (!this.enabled || !this.entity.enabled) return;
this.system.app.assets.load(asset);
}
}
_unbindMeshAsset(asset) {
asset.off("load", this._onMeshAssetLoad, this);
asset.off("unload", this._onMeshAssetUnload, this);
asset.off("remove", this._onMeshAssetRemove, this);
asset.off("change", this._onMeshAssetChange, this);
}
_onMeshAssetLoad(asset) {
this._onMeshChanged(asset.resource);
}
_onMeshAssetUnload(asset) {
this.mesh = null;
}
_onMeshAssetRemove(asset) {
this._onMeshAssetUnload(asset);
}
_onMeshAssetChange(asset) {
}
onSetMeshAsset(name, oldValue, newValue) {
const assets = this.system.app.assets;
if (oldValue) {
const asset = assets.get(oldValue);
if (asset) {
this._unbindMeshAsset(asset);
}
}
if (newValue) {
if (newValue instanceof Asset) {
this.data.meshAsset = newValue.id;
newValue = newValue.id;
}
const asset = assets.get(newValue);
if (asset) {
this._bindMeshAsset(asset);
}
} else {
this._onMeshChanged(null);
}
}
onSetMesh(name, oldValue, newValue) {
if (!newValue || newValue instanceof Asset || typeof newValue === "number") {
this.meshAsset = newValue;
} else {
this._onMeshChanged(newValue);
}
}
_onMeshChanged(mesh) {
if (mesh && !(mesh instanceof Mesh)) {
if (mesh.meshInstances[0]) {
mesh = mesh.meshInstances[0].mesh;
} else {
mesh = null;
}
}
this.data.mesh = mesh;
if (this.emitter) {
this.emitter.mesh = mesh;
this.emitter.resetMaterial();
this.rebuild();
}
}
onSetRenderAsset(name, oldValue, newValue) {
const assets = this.system.app.assets;
if (oldValue) {
const asset = assets.get(oldValue);
if (asset) {
this._unbindRenderAsset(asset);
}
}
if (newValue) {
if (newValue instanceof Asset) {
this.data.renderAsset = newValue.id;
newValue = newValue.id;
}
const asset = assets.get(newValue);
if (asset) {
this._bindRenderAsset(asset);
}
} else {
this._onRenderChanged(null);
}
}
_bindRenderAsset(asset) {
asset.on("load", this._onRenderAssetLoad, this);
asset.on("unload", this._onRenderAssetUnload, this);
asset.on("remove", this._onRenderAssetRemove, this);
if (asset.resource) {
this._onRenderAssetLoad(asset);
} else {
if (!this.enabled || !this.entity.enabled) return;
this.system.app.assets.load(asset);
}
}
_unbindRenderAsset(asset) {
asset.off("load", this._onRenderAssetLoad, this);
asset.off("unload", this._onRenderAssetUnload, this);
asset.off("remove", this._onRenderAssetRemove, this);
this._evtSetMeshes?.off();
this._evtSetMeshes = null;
}
_onRenderAssetLoad(asset) {
this._onRenderChanged(asset.resource);
}
_onRenderAssetUnload(asset) {
this._onRenderChanged(null);
}
_onRenderAssetRemove(asset) {
this._onRenderAssetUnload(asset);
}
_onRenderChanged(render) {
if (!render) {
this._onMeshChanged(null);
return;
}
this._evtSetMeshes?.off();
this._evtSetMeshes = render.on("set:meshes", this._onRenderSetMeshes, this);
if (render.meshes) {
this._onRenderSetMeshes(render.meshes);
}
}
_onRenderSetMeshes(meshes) {
this._onMeshChanged(meshes && meshes[0]);
}
onSetLoop(name, oldValue, newValue) {
if (this.emitter) {
this.emitter[name] = newValue;
this.emitter.resetTime();
}
}
onSetBlendType(name, oldValue, newValue) {
if (this.emitter) {
this.emitter[name] = newValue;
this.emitter.material.blendType = newValue;
this.emitter.resetMaterial();
this.rebuild();
}
}
_requestDepth() {
if (this._requestedDepth) return;
if (!depthLayer) depthLayer = this.system.app.scene.layers.getLayerById(LAYERID_DEPTH);
if (depthLayer) {
depthLayer.incrementCounter();
this._requestedDepth = true;
}
}
_releaseDepth() {
if (!this._requestedDepth) return;
if (depthLayer) {
depthLayer.decrementCounter();
this._requestedDepth = false;
}
}
onSetDepthSoftening(name, oldValue, newValue) {
if (oldValue !== newValue) {
if (newValue) {
if (this.enabled && this.entity.enabled) this._requestDepth();
if (this.emitter) this.emitter[name] = newValue;
} else {
if (this.enabled && this.entity.enabled) this._releaseDepth();
if (this.emitter) this.emitter[name] = newValue;
}
if (this.emitter) {
this.reset();
this.emitter.resetMaterial();
this.rebuild();
}
}
}
onSetSimpleProperty(name, oldValue, newValue) {
if (this.emitter) {
this.emitter[name] = newValue;
this.emitter.resetMaterial();
}
}
onSetComplexProperty(name, oldValue, newValue) {
if (this.emitter) {
this.emitter[name] = newValue;
this.emitter.resetMaterial();
this.rebuild();
this.reset();
}
}
onSetGraphProperty(name, oldValue, newValue) {
if (this.emitter) {
this.emitter[name] = newValue;
this.emitter.rebuildGraphs();
this.emitter.resetMaterial();
}
}
onEnable() {
const scene = this.system.app.scene;
const layers = scene.layers;
const data = this.data;
for (let i = 0, len = ASSET_PROPERTIES.length; i < len; i++) {
let asset = data[ASSET_PROPERTIES[i]];
if (asset) {
if (!(asset instanceof Asset)) {
const id = parseInt(asset, 10);
if (id >= 0) {
asset = this.system.app.assets.get(asset);
} else {
continue;
}
}
if (asset && !asset.resource) {
this.system.app.assets.load(asset);
}
}
}
if (this.system.app.graphicsDevice.disableParticleSystem) {
return;
}
if (!this.emitter) {
let mesh = data.mesh;
if (!(mesh instanceof Mesh)) {
mesh = null;
}
this.emitter = new ParticleEmitter(this.system.app.graphicsDevice, {
numParticles: data.numParticles,
emitterExtents: data.emitterExtents,
emitterExtentsInner: data.emitterExtentsInner,
emitterRadius: data.emitterRadius,
emitterRadiusInner: data.emitterRadiusInner,
emitterShape: data.emitterShape,
initialVelocity: data.initialVelocity,
wrap: data.wrap,
localSpace: data.localSpace,
screenSpace: data.screenSpace,
wrapBounds: data.wrapBounds,
lifetime: data.lifetime,
rate: data.rate,
rate2: data.rate2,
orientation: data.orientation,
particleNormal: data.particleNormal,
animTilesX: data.animTilesX,
animTilesY: data.animTilesY,
animStartFrame: data.animStartFrame,
animNumFrames: data.animNumFrames,
animNumAnimations: data.animNumAnimations,
animIndex: data.animIndex,
randomizeAnimIndex: data.randomizeAnimIndex,
animSpeed: data.animSpeed,
animLoop: data.animLoop,
startAngle: data.startAngle,
startAngle2: data.startAngle2,
scaleGraph: data.scaleGraph,
scaleGraph2: data.scaleGraph2,
colorGraph: data.colorGraph,
colorGraph2: data.colorGraph2,
alphaGraph: data.alphaGraph,
alphaGraph2: data.alphaGraph2,
localVelocityGraph: data.localVelocityGraph,
localVelocityGraph2: data.localVelocityGraph2,
velocityGraph: data.velocityGraph,
velocityGraph2: data.velocityGraph2,
rotationSpeedGraph: data.rotationSpeedGraph,
rotationSpeedGraph2: data.rotationSpeedGraph2,
radialSpeedGraph: data.radialSpeedGraph,
radialSpeedGraph2: data.radialSpeedGraph2,
colorMap: data.colorMap,
normalMap: data.normalMap,
loop: data.loop,
preWarm: data.preWarm,
sort: data.sort,
stretch: data.stretch,
alignToMotion: data.alignToMotion,
lighting: data.lighting,
halfLambert: data.halfLambert,
intensity: data.intensity,
depthSoftening: data.depthSoftening,
scene: this.system.app.scene,
mesh,
depthWrite: data.depthWrite,
noFog: data.noFog,
node: this.entity,
blendType: data.blendType
});
this.emitter.meshInstance.node = this.entity;
this.emitter.drawOrder = this.drawOrder;
if (!data.autoPlay) {
this.pause();
this.emitter.meshInstance.visible = false;
}
}
if (this.emitter.colorMap) {
this.addMeshInstanceToLayers();
}
this._evtLayersChanged = scene.on("set:layers", this.onLayersChanged, this);
if (layers) {
this._evtLayerAdded = layers.on("add", this.onLayerAdded, this);
this._evtLayerRemoved = layers.on("remove", this.onLayerRemoved, this);
}
if (this.enabled && this.entity.enabled && data.depthSoftening) {
this._requestDepth();
}
}
onDisable() {
const scene = this.system.app.scene;
const layers = scene.layers;
this._evtLayersChanged?.off();
this._evtLayersChanged = null;
if (layers) {
this._evtLayerAdded?.off();
this._evtLayerAdded = null;
this._evtLayerRemoved?.off();
this._evtLayerRemoved = null;
}
if (this.emitter) {
this.removeMeshInstanceFromLayers();
if (this.data.depthSoftening) this._releaseDepth();
this.emitter.camera = null;
}
}
onBeforeRemove() {
if (this.enabled) {
this.enabled = false;
}
if (this.emitter) {
this.emitter.destroy();
this.emitter = null;
}
for (let i = 0; i < ASSET_PROPERTIES.length; i++) {
const prop = ASSET_PROPERTIES[i];
if (this.data[prop]) {
this[prop] = null;
}
}
this.off();
}
/**
* Resets particle state, doesn't affect playing.
*/
reset() {
if (this.emitter) {
this.emitter.reset();
}
}
/**
* Disables the emission of new particles, lets existing to finish their simulation.
*/
stop() {
if (this.emitter) {
this.emitter.loop = false;
this.emitter.resetTime();
this.emitter.addTime(0, true);
}
}
/**
* Freezes the simulation.
*/
pause() {
this.data.paused = true;
}
/**
* Unfreezes the simulation.
*/
unpause() {
this.data.paused = false;
}
/**
* Enables/unfreezes the simulation.
*/
play() {
this.data.paused = false;
if (this.emitter) {
this.emitter.meshInstance.visible = true;
this.emitter.loop = this.data.loop;
this.emitter.resetTime();
}
}
/**
* Checks if simulation is in progress.
*
* @returns {boolean} True if the particle system is currently playing and false otherwise.
*/
isPlaying() {
if (this.data.paused) {
return false;
}
if (this.emitter && this.emitter.loop) {
return true;
}
return Date.now() <= this.emitter.endTime;
}
/**
* Called by the Editor when the component is selected, to allow custom in Editor behavior.
*
* @private
*/
setInTools() {
const { emitter } = this;
if (emitter && !emitter.inTools) {
emitter.inTools = true;
this.rebuild();
}
}
/**
* Rebuilds all data used by this particle system.
*
* @private
*/
rebuild() {
const enabled = this.enabled;
this.enabled = false;
if (this.emitter) {
this.emitter.rebuild();
}
this.enabled = enabled;
}
}
export {
ParticleSystemComponent
};