src/components/Particles.js
import VerticesRenderer from './VerticesRenderer';
import parser from '../utils/particleSystemParser';
// base vertex layout:
// float: life;
// float: timeScale;
// vec2: vertex position;
// vec2: tex coord;
const processors = new Map();
/**
* Particles renderer.
*
* @example
* const component = new Particles();
* component.deserialize({ shader: 'shaders/particles.json', processor: 'particles/fire.ps' });
*/
export default class Particles extends VerticesRenderer {
/**
* Component factory.
*
* @return {Particles} Component instance.
*/
static factory() {
return new Particles();
}
/** @type {*} */
static get propsTypes() {
return {
visible: VerticesRenderer.propsTypes.visible,
shader: VerticesRenderer.propsTypes.shader,
overrideUniforms: VerticesRenderer.propsTypes.overrideUniforms,
overrideSamplers: VerticesRenderer.propsTypes.overrideSamplers,
layers: VerticesRenderer.propsTypes.layers,
capacity: 'integer',
processor: 'string',
overrideParams: 'map(number)',
constantBurst: 'boolean',
constantBurstDelay: 'number',
constantBurstCount: 'integer'
};
}
/**
* Register new particles processor.
*
* @param {string} id - Processor id.
* @param {string} contents - Processor code.
*
* @example
* const code = 'attribute x: 0;\nattribute y: 1;\nprogram:\ny -= deltaTime * life;';
* Particles.registerProcessor('fire', code);
*/
static registerProcessor(id, contents) {
if (typeof id !== 'string') {
throw new Error('`id` is not type of String!');
}
if (typeof contents !== 'string') {
throw new Error('`contents` is not type of String!');
}
if (processors.has(id)) {
throw new Error(`There is already registered processor: ${id}`);
}
const p = parser.parse(contents);
const { attributes, params, code } = p;
for (const key in attributes) {
if (key === 'life' ||
key === 'timeScale' ||
key === 'deltaTime' ||
key === 'ps_View' ||
key === 'this'
) {
throw new Error(`Cannot use attribute name: ${key}`);
}
}
for (const key in params) {
if (key === 'life' ||
key === 'timeScale' ||
key === 'deltaTime' ||
key === 'ps_View' ||
key === 'this'
) {
throw new Error(`Cannot use param name: ${key}`);
}
}
let size = 0;
let processor = 'const life=ps_View[0];const timeScale=ps_View[1];if(life<=0)return false;';
for (const key in attributes) {
const offset = attributes[key];
size = Math.max(size, offset + 1);
processor += `let ${key}=ps_View[${offset + 6}];`;
}
const vsize = size + 6;
processor += code + `;ps_View[0]=ps_View[${vsize}]=ps_View[${vsize * 2}]=ps_View[${vsize * 3}]=life-timeScale*deltaTime;`;
for (const key in attributes) {
const offset = attributes[key];
processor += `ps_View[${offset + 6}]=ps_View[${offset + 6 + vsize}]=ps_View[${offset + 6 + vsize * 2}]=ps_View[${offset + 6 + vsize * 3}]=${key};`;
}
processor += 'return true;';
processors.set(id, {
size,
params,
processor: new Function('ps_View', 'deltaTime', processor)
});
}
/**
* Unregister existing processor.
*
* @param {string} id - processor id
*
* @example
* Particles.unregisterProcessor('fire');
*/
static unregisterProcessor(id) {
if (typeof id !== 'string') {
throw new Error('`id` is not type of String!');
}
if (!processors.has(id)) {
throw new Error(`There is no registered processor: ${id}`);
}
processors.delete(id);
}
/** @type {number} */
get capacity() {
return this._capacity;
}
/** @type {number} */
set capacity(value) {
if (typeof value !== 'number') {
throw new Error('`value` is not type of Number!');
}
this._capacity = value | 0;
this._rebuildParticlesData();
}
/** @type {string|null} */
get processor() {
return this._processor;
}
/** @type {string|null} */
set processor(value) {
if (!value) {
this._processor = null;
this._processorAction = null;
this._viewSize = 0;
return;
}
if (typeof value !== 'string') {
throw new Error('`value` is not type of String!');
}
if (!processors.has(value)) {
throw new Error(`There is no registered processor: ${value}`);
}
const { _overrideParams } = this;
const p = processors.get(value);
const { size, params, processor } = p;
const pms = {};
for (const key in params) {
pms[key] = params[key] || 0;
}
for (const key in _overrideParams) {
pms[key] = _overrideParams[key] || 0;
}
this._processor = value;
this._processorAction = processor;
this._viewSize = size;
this._params = pms;
this._rebuildParticlesData();
}
/** @type {*} */
get overrideParams() {
return { ...this._overrideParams };
}
/** @type {*} */
set overrideParams(value) {
this._overrideParams = {};
for (const key in value) {
const v = value[key];
if (typeof v !== 'number') {
throw new Error(`\`value[${key}]\` is not type of Number!`);
}
this._overrideParams[key] = v;
}
this.processor = this.processor;
}
/** @type {Function|null} */
get burstGenerator() {
return this._burstGenerator;
}
/** @type {Function|null} */
set burstGenerator(value) {
if (!!value && !(value instanceof Function)) {
throw new Error('`value` is not type of Function!');
}
this._burstGenerator = value;
}
/** @type {boolean} */
get constantBurst() {
return this._constantBurst;
}
/** @type {boolean} */
set constantBurst(value) {
if (typeof value !== 'boolean') {
throw new Error('`value` is not type of Boolean!');
}
this._constantBurst = value;
this._constantBurstTimeout = 0;
}
/** @type {number} */
get constantBurstDelay() {
return this._constantBurstDelay;
}
/** @type {number} */
set constantBurstDelay(value) {
if (typeof value !== 'number') {
throw new Error('`value` is not type of Number!');
}
this._constantBurstDelay = value;
}
/** @type {number} */
get constantBurstCount() {
return this._constantBurstCount;
}
/** @type {number} */
set constantBurstCount(value) {
if (typeof value !== 'number') {
throw new Error('`value` is not type of Number!');
}
this._constantBurstCount = value | 0;
}
/** @type {number} */
get activeCount() {
return this._active;
}
/**
* Constructor.
*/
constructor() {
super();
this._capacity = 0;
this._views = null;
this._processor = null;
this._processorAction = null;
this._viewSize = 0;
this._overrideParams = null;
this._burstGenerator = null;
this._constantBurst = false;
this._constantBurstDelay = 1;
this._constantBurstCount = -1;
this._constantBurstTimeout = 0;
this._params = null;
this._cursor = 0;
this._active = 0;
this._emitParticle = this.emitParticle.bind(this);
this._rebuildParticlesData();
}
/**
* @override
*/
dispose() {
super.dispose();
this._views = null;
this._processor = null;
this._processorAction = null;
this._overrideParams = null;
this._params = null;
}
/**
* Emit single particle.
*
* @param {number} life - Initial life value.
* @param {number} timeScale - Particle life-cycle time scale.
* @param {number} size - Particle size.
* @param {*} args - Particle attributes.
*
* @return {boolean} True if emitted, false otherwise.
*
* @example
* component.emitParticle(1, 0.5, 10);
*/
emitParticle(life, timeScale, size, ...args) {
if (typeof life !== 'number') {
throw new Error('`life` is not type of Number!');
}
if (typeof timeScale !== 'number') {
throw new Error('`timeScale` is not type of Number!');
}
const index = this._findFreeIndex();
if (index < 0) {
return false;
}
++this._active;
const { _viewSize } = this;
const hs = size * 0.5;
const vs = _viewSize + 6;
const vs2 = vs + vs;
const vs3 = vs2 + vs;
const view = this._views[index];
view[0] = view[vs] = view[vs2] = view[vs3] = life;
view[1] = view[vs + 1] = view[vs2 + 1] = view[vs3 + 1] = timeScale;
view[2] = -hs;
view[3] = -hs;
view[4] = 0;
view[5] = 0;
view[vs + 2] = hs;
view[vs + 3] = -hs;
view[vs + 4] = 1;
view[vs + 5] = 0;
view[vs2 + 2] = hs;
view[vs2 + 3] = hs;
view[vs2 + 4] = 1;
view[vs2 + 5] = 1;
view[vs3 + 2] = -hs;
view[vs3 + 3] = hs;
view[vs3 + 4] = 0;
view[vs3 + 5] = 1;
let i = 6;
for (const arg of args) {
view[i] = view[vs + i] = view[vs2 + i] = view[vs3 + i] = arg;
++i;
}
return true;
}
/**
* Emit burst of particles using burst generator function.
*
* @param {number} count - Number of particles to emit.
* @param {*} args - Particles attributes.
*
* @example
* component.emitBurst(100);
*/
emitBurst(count, ...args) {
const { _burstGenerator } = this;
if (!_burstGenerator) {
throw new Error('Burst generator is not provided!');
}
if (count < 0) {
count = this._capacity;
}
while (count-- > 0) {
_burstGenerator.call(this, this._emitParticle, ...args);
}
}
/**
* Set particles parameter.
*
* @param {string} id - Parameter id.
* @param {number} value - Parameter value.
*
* @example
* component.setParam('alpha', 0.5);
*/
setParam(id, value) {
if (typeof id !== 'string') {
throw new Error('`id` is not type of String!');
}
if (typeof value !== 'number') {
throw new Error('`value` is not type of Number!');
}
const { _params } = this;
if (!(id in _params)) {
throw new Error(`Unknown param: ${id}`);
}
this._params[id] = value;
}
/**
* @override
*/
onAction(name, ...args) {
if (name === 'update') {
return this.onUpdate(...args);
} else {
return super.onAction(name, ...args);
}
}
/**
* @override
*/
onUpdate(deltaTime) {
deltaTime *= 0.001;
if (this._constantBurst) {
if (this._constantBurstTimeout <= 0) {
this.emitBurst(this._constantBurstCount);
this._constantBurstTimeout = this._constantBurstDelay;
}
this._constantBurstTimeout -= deltaTime;
}
const { _processorAction, _views } = this;
if (!!_processorAction) {
let active = 0;
for (const view of _views) {
if (this._processorAction.call(this._params, view, deltaTime)) {
++active;
}
}
this._active = active;
this.reuploadData();
}
}
_rebuildParticlesData() {
const { _capacity, _viewSize } = this;
const vsize = _viewSize + 6;
const vertices = this.vertices = new Float32Array(vsize * 4 * _capacity);
const views = this._views = [];
let offset = 0;
let index = 0;
const indices = [];
for (let i = 0; i < _capacity; ++i) {
views.push(vertices.subarray(offset, offset + vsize * 4));
indices.push(
index,
index + 1,
index + 2,
index + 2,
index + 3,
index
);
offset += vsize * 4;
index += 4;
}
this.indices = indices;
this.verticesUsage = VerticesRenderer.BufferUsage.DYNAMIC;
this._cursor = 0;
}
_findFreeIndex() {
const { _capacity, _active, _views } = this;
if (_active >= _capacity) {
return -1;
}
let view = null;
for (let i = 0; i < _capacity; ++i) {
view = _views[this._cursor];
if (view[0] <= 0) {
return this._cursor;
}
++this._cursor;
if (this._cursor >= _capacity) {
this._cursor = 0;
}
}
return -1;
}
}