UNPKG

playcanvas

Version:

PlayCanvas WebGL game engine

1,572 lines (1,571 loc) 55.6 kB
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'; /** * @import { CurveSet } from '../../../core/math/curve-set.js' * @import { Curve } from '../../../core/math/curve.js' * @import { Entity } from '../../entity.js' * @import { EventHandle } from '../../../core/event-handle.js' * @import { ParticleSystemComponentData } from './data.js' * @import { ParticleSystemComponentSystem } from './system.js' * @import { Texture } from '../../../platform/graphics/texture.js' * @import { Vec3 } from '../../../core/math/vec3.js' */ // properties that do not need rebuilding the particle system const SIMPLE_PROPERTIES = [ 'emitterExtents', 'emitterRadius', 'emitterExtentsInner', 'emitterRadiusInner', 'loop', 'initialVelocity', 'animSpeed', 'normalMap', 'particleNormal' ]; // properties that need rebuilding the particle system 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; /** * Used to simulate particles and produce renderable particle mesh on either CPU or GPU. GPU * simulation is generally much faster than its CPU counterpart, because it avoids slow CPU-GPU * synchronization and takes advantage of many GPU cores. However, it requires client to support * reasonable uniform count, reading from multiple textures in vertex shader and OES_texture_float * extension, including rendering into float textures. Most mobile devices fail to satisfy these * requirements, so it's not recommended to simulate thousands of particles on them. GPU version * also can't sort particles, so enabling sorting forces CPU mode too. Particle rotation is * specified by a single angle parameter: default billboard particles rotate around camera facing * axis, while mesh particles rotate around 2 different view-independent axes. Most of the * simulation parameters are specified with {@link Curve} or {@link CurveSet}. Curves are * interpolated based on each particle's lifetime, therefore parameters are able to change over * time. Most of the curve parameters can also be specified by 2 minimum/maximum curves, this way * each particle will pick a random value in-between. * * @hideconstructor * @category Graphics */ 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 */ this._requestedDepth = false, /** @private */ this._drawOrder = 0, /** * @type {EventHandle|null} * @private */ this._evtLayersChanged = null, /** * @type {EventHandle|null} * @private */ this._evtLayerAdded = null, /** * @type {EventHandle|null} * @private */ this._evtLayerRemoved = null, /** * @type {EventHandle|null} * @private */ 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); }); } // TODO: Remove this override in upgrading component /** * @type {ParticleSystemComponentData} * @ignore */ get data() { const record = this.system.store[this.entity.getGuid()]; return record ? record.data : null; } /** * 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 ParticleSystemComponent#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 { // don't trigger an asset load unless the component is enabled 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}`, (asset)=>{ this._bindColorMapAsset(asset); }); } } 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 { // don't trigger an asset load unless the component is enabled 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}`, (asset)=>{ this._bindNormalMapAsset(asset); }); } } 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 { // don't trigger an asset load unless the component is enabled 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) { // hack this for now // if the value being set is null, an asset or an asset id, then assume we are // setting the mesh asset, which will in turn update the mesh if (!newValue || newValue instanceof Asset || typeof newValue === 'number') { this.meshAsset = newValue; } else { this._onMeshChanged(newValue); } } _onMeshChanged(mesh) { if (mesh && !(mesh instanceof Mesh)) { // if mesh is a pc.Model, use the first meshInstance 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 { // don't trigger an asset load unless the component is enabled 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; // get data store once const data = this.data; // load any assets that haven't been loaded yet 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); } } } // WebGPU does not support particle systems, ignore them if (this.system.app.graphicsDevice.disableParticleSystem) { return; } if (!this.emitter) { let mesh = data.mesh; // mesh might be an asset id of an asset // that hasn't been loaded yet 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.animSp