diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index e59c66a..a390a1a 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -332,6 +332,43 @@ /* * This namespace has been removed and items have been moved to * the top-level `PIXI` object. + * @namespace PIXI.particles + * @deprecated since 5.0.0 + */ + PIXI.particles = {}; + + Object.defineProperties(PIXI.particles, { + /** + * @class PIXI.particles.ParticleContainer + * @deprecated since 5.0.0 + * @see PIXI.ParticleContainer + */ + ParticleContainer: { + get() + { + warn('PIXI.particles.ParticleContainer has moved to PIXI.ParticleContainer'); + + return PIXI.ParticleContainer; + }, + }, + /** + * @class PIXI.particles.ParticleRenderer + * @deprecated since 5.0.0 + * @see PIXI.ParticleRenderer + */ + ParticleRenderer: { + get() + { + warn('PIXI.particles.ParticleRenderer has moved to PIXI.ParticleRenderer'); + + return PIXI.ParticleRenderer; + }, + }, + }); + + /* + * This namespace has been removed and items have been moved to + * the top-level `PIXI` object. * @namespace PIXI.ticker * @deprecated since 5.0.0 */ diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index e59c66a..a390a1a 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -332,6 +332,43 @@ /* * This namespace has been removed and items have been moved to * the top-level `PIXI` object. + * @namespace PIXI.particles + * @deprecated since 5.0.0 + */ + PIXI.particles = {}; + + Object.defineProperties(PIXI.particles, { + /** + * @class PIXI.particles.ParticleContainer + * @deprecated since 5.0.0 + * @see PIXI.ParticleContainer + */ + ParticleContainer: { + get() + { + warn('PIXI.particles.ParticleContainer has moved to PIXI.ParticleContainer'); + + return PIXI.ParticleContainer; + }, + }, + /** + * @class PIXI.particles.ParticleRenderer + * @deprecated since 5.0.0 + * @see PIXI.ParticleRenderer + */ + ParticleRenderer: { + get() + { + warn('PIXI.particles.ParticleRenderer has moved to PIXI.ParticleRenderer'); + + return PIXI.ParticleRenderer; + }, + }, + }); + + /* + * This namespace has been removed and items have been moved to + * the top-level `PIXI` object. * @namespace PIXI.ticker * @deprecated since 5.0.0 */ diff --git a/bundles/pixi.js/src/index.js b/bundles/pixi.js/src/index.js index 52d7cb4..fb839a0 100644 --- a/bundles/pixi.js/src/index.js +++ b/bundles/pixi.js/src/index.js @@ -9,6 +9,7 @@ import * as loaders from '@pixi/loaders'; import * as math from '@pixi/math'; import * as mesh from '@pixi/mesh'; +import * as particles from '@pixi/particles'; import * as prepare from '@pixi/prepare'; import * as sprite from '@pixi/sprite'; import * as spriteAnimated from '@pixi/sprite-animated'; @@ -37,6 +38,7 @@ core.Renderer.registerPlugin('graphics', graphics.GraphicsRenderer); core.Renderer.registerPlugin('interaction', interaction.InteractionManager); core.Renderer.registerPlugin('mesh', mesh.MeshRenderer); +core.Renderer.registerPlugin('particle', particles.ParticleRenderer); core.Renderer.registerPlugin('prepare', prepare.Prepare); core.Renderer.registerPlugin('sprite', sprite.SpriteRenderer); core.Renderer.registerPlugin('tilingSprite', spriteTiling.TilingSpriteRenderer); @@ -118,6 +120,7 @@ loaders, math, mesh, + particles, sprite, spriteAnimated, spritesheet, @@ -141,6 +144,7 @@ export * from '@pixi/loaders'; export * from '@pixi/math'; export * from '@pixi/mesh'; +export * from '@pixi/particles'; export * from '@pixi/sprite'; export * from '@pixi/spritesheet'; export * from '@pixi/sprite-animated'; diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index e59c66a..a390a1a 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -332,6 +332,43 @@ /* * This namespace has been removed and items have been moved to * the top-level `PIXI` object. + * @namespace PIXI.particles + * @deprecated since 5.0.0 + */ + PIXI.particles = {}; + + Object.defineProperties(PIXI.particles, { + /** + * @class PIXI.particles.ParticleContainer + * @deprecated since 5.0.0 + * @see PIXI.ParticleContainer + */ + ParticleContainer: { + get() + { + warn('PIXI.particles.ParticleContainer has moved to PIXI.ParticleContainer'); + + return PIXI.ParticleContainer; + }, + }, + /** + * @class PIXI.particles.ParticleRenderer + * @deprecated since 5.0.0 + * @see PIXI.ParticleRenderer + */ + ParticleRenderer: { + get() + { + warn('PIXI.particles.ParticleRenderer has moved to PIXI.ParticleRenderer'); + + return PIXI.ParticleRenderer; + }, + }, + }); + + /* + * This namespace has been removed and items have been moved to + * the top-level `PIXI` object. * @namespace PIXI.ticker * @deprecated since 5.0.0 */ diff --git a/bundles/pixi.js/src/index.js b/bundles/pixi.js/src/index.js index 52d7cb4..fb839a0 100644 --- a/bundles/pixi.js/src/index.js +++ b/bundles/pixi.js/src/index.js @@ -9,6 +9,7 @@ import * as loaders from '@pixi/loaders'; import * as math from '@pixi/math'; import * as mesh from '@pixi/mesh'; +import * as particles from '@pixi/particles'; import * as prepare from '@pixi/prepare'; import * as sprite from '@pixi/sprite'; import * as spriteAnimated from '@pixi/sprite-animated'; @@ -37,6 +38,7 @@ core.Renderer.registerPlugin('graphics', graphics.GraphicsRenderer); core.Renderer.registerPlugin('interaction', interaction.InteractionManager); core.Renderer.registerPlugin('mesh', mesh.MeshRenderer); +core.Renderer.registerPlugin('particle', particles.ParticleRenderer); core.Renderer.registerPlugin('prepare', prepare.Prepare); core.Renderer.registerPlugin('sprite', sprite.SpriteRenderer); core.Renderer.registerPlugin('tilingSprite', spriteTiling.TilingSpriteRenderer); @@ -118,6 +120,7 @@ loaders, math, mesh, + particles, sprite, spriteAnimated, spritesheet, @@ -141,6 +144,7 @@ export * from '@pixi/loaders'; export * from '@pixi/math'; export * from '@pixi/mesh'; +export * from '@pixi/particles'; export * from '@pixi/sprite'; export * from '@pixi/spritesheet'; export * from '@pixi/sprite-animated'; diff --git a/packages/particles/README.md b/packages/particles/README.md index bca3a96..dca806d 100644 --- a/packages/particles/README.md +++ b/packages/particles/README.md @@ -9,5 +9,8 @@ ## Usage ```js -import '@pixi/particles'; +import {ParticleRenderer} from '@pixi/particles'; +import {Renderer} from '@pixi/core'; + +Renderer.registerPlugin('particle', ParticleRenderer); ``` \ No newline at end of file diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index e59c66a..a390a1a 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -332,6 +332,43 @@ /* * This namespace has been removed and items have been moved to * the top-level `PIXI` object. + * @namespace PIXI.particles + * @deprecated since 5.0.0 + */ + PIXI.particles = {}; + + Object.defineProperties(PIXI.particles, { + /** + * @class PIXI.particles.ParticleContainer + * @deprecated since 5.0.0 + * @see PIXI.ParticleContainer + */ + ParticleContainer: { + get() + { + warn('PIXI.particles.ParticleContainer has moved to PIXI.ParticleContainer'); + + return PIXI.ParticleContainer; + }, + }, + /** + * @class PIXI.particles.ParticleRenderer + * @deprecated since 5.0.0 + * @see PIXI.ParticleRenderer + */ + ParticleRenderer: { + get() + { + warn('PIXI.particles.ParticleRenderer has moved to PIXI.ParticleRenderer'); + + return PIXI.ParticleRenderer; + }, + }, + }); + + /* + * This namespace has been removed and items have been moved to + * the top-level `PIXI` object. * @namespace PIXI.ticker * @deprecated since 5.0.0 */ diff --git a/bundles/pixi.js/src/index.js b/bundles/pixi.js/src/index.js index 52d7cb4..fb839a0 100644 --- a/bundles/pixi.js/src/index.js +++ b/bundles/pixi.js/src/index.js @@ -9,6 +9,7 @@ import * as loaders from '@pixi/loaders'; import * as math from '@pixi/math'; import * as mesh from '@pixi/mesh'; +import * as particles from '@pixi/particles'; import * as prepare from '@pixi/prepare'; import * as sprite from '@pixi/sprite'; import * as spriteAnimated from '@pixi/sprite-animated'; @@ -37,6 +38,7 @@ core.Renderer.registerPlugin('graphics', graphics.GraphicsRenderer); core.Renderer.registerPlugin('interaction', interaction.InteractionManager); core.Renderer.registerPlugin('mesh', mesh.MeshRenderer); +core.Renderer.registerPlugin('particle', particles.ParticleRenderer); core.Renderer.registerPlugin('prepare', prepare.Prepare); core.Renderer.registerPlugin('sprite', sprite.SpriteRenderer); core.Renderer.registerPlugin('tilingSprite', spriteTiling.TilingSpriteRenderer); @@ -118,6 +120,7 @@ loaders, math, mesh, + particles, sprite, spriteAnimated, spritesheet, @@ -141,6 +144,7 @@ export * from '@pixi/loaders'; export * from '@pixi/math'; export * from '@pixi/mesh'; +export * from '@pixi/particles'; export * from '@pixi/sprite'; export * from '@pixi/spritesheet'; export * from '@pixi/sprite-animated'; diff --git a/packages/particles/README.md b/packages/particles/README.md index bca3a96..dca806d 100644 --- a/packages/particles/README.md +++ b/packages/particles/README.md @@ -9,5 +9,8 @@ ## Usage ```js -import '@pixi/particles'; +import {ParticleRenderer} from '@pixi/particles'; +import {Renderer} from '@pixi/core'; + +Renderer.registerPlugin('particle', ParticleRenderer); ``` \ No newline at end of file diff --git a/packages/particles/src/ParticleBuffer.js b/packages/particles/src/ParticleBuffer.js new file mode 100644 index 0000000..6306bda --- /dev/null +++ b/packages/particles/src/ParticleBuffer.js @@ -0,0 +1,268 @@ +// import { VertexArrayObject } from 'pixi-gl-core'; +import { createIndicesForQuads } from '@pixi/utils'; +import { GLBuffer } from 'pixi-gl-core'; + +/** + * @author Mat Groves + * + * Big thanks to the very clever Matt DesLauriers https://github.com/mattdesl/ + * for creating the original PixiJS version! + * Also a thanks to https://github.com/bchevalier for tweaking the tint and alpha so that + * they now share 4 bytes on the vertex buffer + * + * Heavily inspired by LibGDX's ParticleBuffer: + * https://github.com/libgdx/libgdx/blob/master/gdx/src/com/badlogic/gdx/graphics/g2d/ParticleBuffer.java + */ + +/** + * The particle buffer manages the static and dynamic buffers for a particle container. + * + * @class + * @private + * @memberof PIXI + */ +export default class ParticleBuffer +{ + /** + * @param {WebGLRenderingContext} gl - The rendering context. + * @param {object} properties - The properties to upload. + * @param {boolean[]} dynamicPropertyFlags - Flags for which properties are dynamic. + * @param {number} size - The size of the batch. + */ + constructor(gl, properties, dynamicPropertyFlags, size) + { + /** + * The current WebGL drawing context. + * + * @member {WebGLRenderingContext} + */ + this.gl = gl; + + /** + * The number of particles the buffer can hold + * + * @member {number} + */ + this.size = size; + + /** + * A list of the properties that are dynamic. + * + * @member {object[]} + */ + this.dynamicProperties = []; + + /** + * A list of the properties that are static. + * + * @member {object[]} + */ + this.staticProperties = []; + + for (let i = 0; i < properties.length; ++i) + { + let property = properties[i]; + + // Make copy of properties object so that when we edit the offset it doesn't + // change all other instances of the object literal + property = { + attribute: property.attribute, + size: property.size, + uploadFunction: property.uploadFunction, + unsignedByte: property.unsignedByte, + offset: property.offset, + }; + + if (dynamicPropertyFlags[i]) + { + this.dynamicProperties.push(property); + } + else + { + this.staticProperties.push(property); + } + } + + this.staticStride = 0; + this.staticBuffer = null; + this.staticData = null; + this.staticDataUint32 = null; + + this.dynamicStride = 0; + this.dynamicBuffer = null; + this.dynamicData = null; + this.dynamicDataUint32 = null; + + this.initBuffers(); + } + + /** + * Sets up the renderer context and necessary buffers. + * + * @private + */ + initBuffers() + { + const gl = this.gl; + let dynamicOffset = 0; + + /** + * Holds the indices of the geometry (quads) to draw + * + * @member {Uint16Array} + */ + this.indices = createIndicesForQuads(this.size); + this.indexBuffer = GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); + + this.dynamicStride = 0; + + for (let i = 0; i < this.dynamicProperties.length; ++i) + { + const property = this.dynamicProperties[i]; + + property.offset = dynamicOffset; + dynamicOffset += property.size; + this.dynamicStride += property.size; + } + + this.dynamicData = new Float32Array(this.size * this.dynamicStride * 4); + this.dynamicBuffer = GLBuffer.createVertexBuffer(gl, this.dynamicData, gl.STREAM_DRAW); + + // static // + let staticOffset = 0; + + this.staticStride = 0; + + for (let i = 0; i < this.staticProperties.length; ++i) + { + const property = this.staticProperties[i]; + + property.offset = staticOffset; + staticOffset += property.size; + this.staticStride += property.size; + } + + this.staticData = new Float32Array(this.size * this.staticStride * 4); + this.staticBuffer = GLBuffer.createVertexBuffer(gl, this.staticData, gl.STATIC_DRAW); + + // this.vao = new VertexArrayObject(gl) + // .addIndex(this.indexBuffer); + + for (let i = 0; i < this.dynamicProperties.length; ++i) + { + const property = this.dynamicProperties[i]; + + if (property.unsignedByte) + { + this.vao.addAttribute( + this.dynamicBuffer, + property.attribute, + gl.UNSIGNED_BYTE, + true, + this.dynamicStride * 4, + property.offset * 4 + ); + } + else + { + this.vao.addAttribute( + this.dynamicBuffer, + property.attribute, + gl.FLOAT, + false, + this.dynamicStride * 4, + property.offset * 4 + ); + } + } + + for (let i = 0; i < this.staticProperties.length; ++i) + { + const property = this.staticProperties[i]; + + if (property.unsignedByte) + { + this.vao.addAttribute( + this.staticBuffer, + property.attribute, + gl.UNSIGNED_BYTE, + true, + this.staticStride * 4, + property.offset * 4 + ); + } + else + { + this.vao.addAttribute( + this.staticBuffer, + property.attribute, + gl.FLOAT, + false, + this.staticStride * 4, + property.offset * 4 + ); + } + } + } + + /** + * Uploads the dynamic properties. + * + * @param {PIXI.DisplayObject[]} children - The children to upload. + * @param {number} startIndex - The index to start at. + * @param {number} amount - The number to upload. + */ + uploadDynamic(children, startIndex, amount) + { + for (let i = 0; i < this.dynamicProperties.length; i++) + { + const property = this.dynamicProperties[i]; + + property.uploadFunction(children, startIndex, amount, + property.unsignedByte ? this.dynamicDataUint32 : this.dynamicData, + this.dynamicStride, property.offset); + } + + this.dynamicBuffer.upload(); + } + + /** + * Uploads the static properties. + * + * @param {PIXI.DisplayObject[]} children - The children to upload. + * @param {number} startIndex - The index to start at. + * @param {number} amount - The number to upload. + */ + uploadStatic(children, startIndex, amount) + { + for (let i = 0; i < this.staticProperties.length; i++) + { + const property = this.staticProperties[i]; + + property.uploadFunction(children, startIndex, amount, + property.unsignedByte ? this.staticDataUint32 : this.staticData, + this.staticStride, property.offset); + } + + this.staticBuffer.upload(); + } + + /** + * Destroys the ParticleBuffer. + * + */ + destroy() + { + this.dynamicProperties = null; + this.dynamicBuffer.destroy(); + this.dynamicBuffer = null; + this.dynamicData = null; + this.dynamicDataUint32 = null; + + this.staticProperties = null; + this.staticBuffer.destroy(); + this.staticBuffer = null; + this.staticData = null; + this.staticDataUint32 = null; + } +} diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index e59c66a..a390a1a 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -332,6 +332,43 @@ /* * This namespace has been removed and items have been moved to * the top-level `PIXI` object. + * @namespace PIXI.particles + * @deprecated since 5.0.0 + */ + PIXI.particles = {}; + + Object.defineProperties(PIXI.particles, { + /** + * @class PIXI.particles.ParticleContainer + * @deprecated since 5.0.0 + * @see PIXI.ParticleContainer + */ + ParticleContainer: { + get() + { + warn('PIXI.particles.ParticleContainer has moved to PIXI.ParticleContainer'); + + return PIXI.ParticleContainer; + }, + }, + /** + * @class PIXI.particles.ParticleRenderer + * @deprecated since 5.0.0 + * @see PIXI.ParticleRenderer + */ + ParticleRenderer: { + get() + { + warn('PIXI.particles.ParticleRenderer has moved to PIXI.ParticleRenderer'); + + return PIXI.ParticleRenderer; + }, + }, + }); + + /* + * This namespace has been removed and items have been moved to + * the top-level `PIXI` object. * @namespace PIXI.ticker * @deprecated since 5.0.0 */ diff --git a/bundles/pixi.js/src/index.js b/bundles/pixi.js/src/index.js index 52d7cb4..fb839a0 100644 --- a/bundles/pixi.js/src/index.js +++ b/bundles/pixi.js/src/index.js @@ -9,6 +9,7 @@ import * as loaders from '@pixi/loaders'; import * as math from '@pixi/math'; import * as mesh from '@pixi/mesh'; +import * as particles from '@pixi/particles'; import * as prepare from '@pixi/prepare'; import * as sprite from '@pixi/sprite'; import * as spriteAnimated from '@pixi/sprite-animated'; @@ -37,6 +38,7 @@ core.Renderer.registerPlugin('graphics', graphics.GraphicsRenderer); core.Renderer.registerPlugin('interaction', interaction.InteractionManager); core.Renderer.registerPlugin('mesh', mesh.MeshRenderer); +core.Renderer.registerPlugin('particle', particles.ParticleRenderer); core.Renderer.registerPlugin('prepare', prepare.Prepare); core.Renderer.registerPlugin('sprite', sprite.SpriteRenderer); core.Renderer.registerPlugin('tilingSprite', spriteTiling.TilingSpriteRenderer); @@ -118,6 +120,7 @@ loaders, math, mesh, + particles, sprite, spriteAnimated, spritesheet, @@ -141,6 +144,7 @@ export * from '@pixi/loaders'; export * from '@pixi/math'; export * from '@pixi/mesh'; +export * from '@pixi/particles'; export * from '@pixi/sprite'; export * from '@pixi/spritesheet'; export * from '@pixi/sprite-animated'; diff --git a/packages/particles/README.md b/packages/particles/README.md index bca3a96..dca806d 100644 --- a/packages/particles/README.md +++ b/packages/particles/README.md @@ -9,5 +9,8 @@ ## Usage ```js -import '@pixi/particles'; +import {ParticleRenderer} from '@pixi/particles'; +import {Renderer} from '@pixi/core'; + +Renderer.registerPlugin('particle', ParticleRenderer); ``` \ No newline at end of file diff --git a/packages/particles/src/ParticleBuffer.js b/packages/particles/src/ParticleBuffer.js new file mode 100644 index 0000000..6306bda --- /dev/null +++ b/packages/particles/src/ParticleBuffer.js @@ -0,0 +1,268 @@ +// import { VertexArrayObject } from 'pixi-gl-core'; +import { createIndicesForQuads } from '@pixi/utils'; +import { GLBuffer } from 'pixi-gl-core'; + +/** + * @author Mat Groves + * + * Big thanks to the very clever Matt DesLauriers https://github.com/mattdesl/ + * for creating the original PixiJS version! + * Also a thanks to https://github.com/bchevalier for tweaking the tint and alpha so that + * they now share 4 bytes on the vertex buffer + * + * Heavily inspired by LibGDX's ParticleBuffer: + * https://github.com/libgdx/libgdx/blob/master/gdx/src/com/badlogic/gdx/graphics/g2d/ParticleBuffer.java + */ + +/** + * The particle buffer manages the static and dynamic buffers for a particle container. + * + * @class + * @private + * @memberof PIXI + */ +export default class ParticleBuffer +{ + /** + * @param {WebGLRenderingContext} gl - The rendering context. + * @param {object} properties - The properties to upload. + * @param {boolean[]} dynamicPropertyFlags - Flags for which properties are dynamic. + * @param {number} size - The size of the batch. + */ + constructor(gl, properties, dynamicPropertyFlags, size) + { + /** + * The current WebGL drawing context. + * + * @member {WebGLRenderingContext} + */ + this.gl = gl; + + /** + * The number of particles the buffer can hold + * + * @member {number} + */ + this.size = size; + + /** + * A list of the properties that are dynamic. + * + * @member {object[]} + */ + this.dynamicProperties = []; + + /** + * A list of the properties that are static. + * + * @member {object[]} + */ + this.staticProperties = []; + + for (let i = 0; i < properties.length; ++i) + { + let property = properties[i]; + + // Make copy of properties object so that when we edit the offset it doesn't + // change all other instances of the object literal + property = { + attribute: property.attribute, + size: property.size, + uploadFunction: property.uploadFunction, + unsignedByte: property.unsignedByte, + offset: property.offset, + }; + + if (dynamicPropertyFlags[i]) + { + this.dynamicProperties.push(property); + } + else + { + this.staticProperties.push(property); + } + } + + this.staticStride = 0; + this.staticBuffer = null; + this.staticData = null; + this.staticDataUint32 = null; + + this.dynamicStride = 0; + this.dynamicBuffer = null; + this.dynamicData = null; + this.dynamicDataUint32 = null; + + this.initBuffers(); + } + + /** + * Sets up the renderer context and necessary buffers. + * + * @private + */ + initBuffers() + { + const gl = this.gl; + let dynamicOffset = 0; + + /** + * Holds the indices of the geometry (quads) to draw + * + * @member {Uint16Array} + */ + this.indices = createIndicesForQuads(this.size); + this.indexBuffer = GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); + + this.dynamicStride = 0; + + for (let i = 0; i < this.dynamicProperties.length; ++i) + { + const property = this.dynamicProperties[i]; + + property.offset = dynamicOffset; + dynamicOffset += property.size; + this.dynamicStride += property.size; + } + + this.dynamicData = new Float32Array(this.size * this.dynamicStride * 4); + this.dynamicBuffer = GLBuffer.createVertexBuffer(gl, this.dynamicData, gl.STREAM_DRAW); + + // static // + let staticOffset = 0; + + this.staticStride = 0; + + for (let i = 0; i < this.staticProperties.length; ++i) + { + const property = this.staticProperties[i]; + + property.offset = staticOffset; + staticOffset += property.size; + this.staticStride += property.size; + } + + this.staticData = new Float32Array(this.size * this.staticStride * 4); + this.staticBuffer = GLBuffer.createVertexBuffer(gl, this.staticData, gl.STATIC_DRAW); + + // this.vao = new VertexArrayObject(gl) + // .addIndex(this.indexBuffer); + + for (let i = 0; i < this.dynamicProperties.length; ++i) + { + const property = this.dynamicProperties[i]; + + if (property.unsignedByte) + { + this.vao.addAttribute( + this.dynamicBuffer, + property.attribute, + gl.UNSIGNED_BYTE, + true, + this.dynamicStride * 4, + property.offset * 4 + ); + } + else + { + this.vao.addAttribute( + this.dynamicBuffer, + property.attribute, + gl.FLOAT, + false, + this.dynamicStride * 4, + property.offset * 4 + ); + } + } + + for (let i = 0; i < this.staticProperties.length; ++i) + { + const property = this.staticProperties[i]; + + if (property.unsignedByte) + { + this.vao.addAttribute( + this.staticBuffer, + property.attribute, + gl.UNSIGNED_BYTE, + true, + this.staticStride * 4, + property.offset * 4 + ); + } + else + { + this.vao.addAttribute( + this.staticBuffer, + property.attribute, + gl.FLOAT, + false, + this.staticStride * 4, + property.offset * 4 + ); + } + } + } + + /** + * Uploads the dynamic properties. + * + * @param {PIXI.DisplayObject[]} children - The children to upload. + * @param {number} startIndex - The index to start at. + * @param {number} amount - The number to upload. + */ + uploadDynamic(children, startIndex, amount) + { + for (let i = 0; i < this.dynamicProperties.length; i++) + { + const property = this.dynamicProperties[i]; + + property.uploadFunction(children, startIndex, amount, + property.unsignedByte ? this.dynamicDataUint32 : this.dynamicData, + this.dynamicStride, property.offset); + } + + this.dynamicBuffer.upload(); + } + + /** + * Uploads the static properties. + * + * @param {PIXI.DisplayObject[]} children - The children to upload. + * @param {number} startIndex - The index to start at. + * @param {number} amount - The number to upload. + */ + uploadStatic(children, startIndex, amount) + { + for (let i = 0; i < this.staticProperties.length; i++) + { + const property = this.staticProperties[i]; + + property.uploadFunction(children, startIndex, amount, + property.unsignedByte ? this.staticDataUint32 : this.staticData, + this.staticStride, property.offset); + } + + this.staticBuffer.upload(); + } + + /** + * Destroys the ParticleBuffer. + * + */ + destroy() + { + this.dynamicProperties = null; + this.dynamicBuffer.destroy(); + this.dynamicBuffer = null; + this.dynamicData = null; + this.dynamicDataUint32 = null; + + this.staticProperties = null; + this.staticBuffer.destroy(); + this.staticBuffer = null; + this.staticData = null; + this.staticDataUint32 = null; + } +} diff --git a/packages/particles/src/ParticleContainer.js b/packages/particles/src/ParticleContainer.js index c8ade96..9d25c5b 100644 --- a/packages/particles/src/ParticleContainer.js +++ b/packages/particles/src/ParticleContainer.js @@ -25,7 +25,7 @@ * * @class * @extends PIXI.Container - * @memberof PIXI.particles + * @memberof PIXI */ export default class ParticleContainer extends Container { diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index e59c66a..a390a1a 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -332,6 +332,43 @@ /* * This namespace has been removed and items have been moved to * the top-level `PIXI` object. + * @namespace PIXI.particles + * @deprecated since 5.0.0 + */ + PIXI.particles = {}; + + Object.defineProperties(PIXI.particles, { + /** + * @class PIXI.particles.ParticleContainer + * @deprecated since 5.0.0 + * @see PIXI.ParticleContainer + */ + ParticleContainer: { + get() + { + warn('PIXI.particles.ParticleContainer has moved to PIXI.ParticleContainer'); + + return PIXI.ParticleContainer; + }, + }, + /** + * @class PIXI.particles.ParticleRenderer + * @deprecated since 5.0.0 + * @see PIXI.ParticleRenderer + */ + ParticleRenderer: { + get() + { + warn('PIXI.particles.ParticleRenderer has moved to PIXI.ParticleRenderer'); + + return PIXI.ParticleRenderer; + }, + }, + }); + + /* + * This namespace has been removed and items have been moved to + * the top-level `PIXI` object. * @namespace PIXI.ticker * @deprecated since 5.0.0 */ diff --git a/bundles/pixi.js/src/index.js b/bundles/pixi.js/src/index.js index 52d7cb4..fb839a0 100644 --- a/bundles/pixi.js/src/index.js +++ b/bundles/pixi.js/src/index.js @@ -9,6 +9,7 @@ import * as loaders from '@pixi/loaders'; import * as math from '@pixi/math'; import * as mesh from '@pixi/mesh'; +import * as particles from '@pixi/particles'; import * as prepare from '@pixi/prepare'; import * as sprite from '@pixi/sprite'; import * as spriteAnimated from '@pixi/sprite-animated'; @@ -37,6 +38,7 @@ core.Renderer.registerPlugin('graphics', graphics.GraphicsRenderer); core.Renderer.registerPlugin('interaction', interaction.InteractionManager); core.Renderer.registerPlugin('mesh', mesh.MeshRenderer); +core.Renderer.registerPlugin('particle', particles.ParticleRenderer); core.Renderer.registerPlugin('prepare', prepare.Prepare); core.Renderer.registerPlugin('sprite', sprite.SpriteRenderer); core.Renderer.registerPlugin('tilingSprite', spriteTiling.TilingSpriteRenderer); @@ -118,6 +120,7 @@ loaders, math, mesh, + particles, sprite, spriteAnimated, spritesheet, @@ -141,6 +144,7 @@ export * from '@pixi/loaders'; export * from '@pixi/math'; export * from '@pixi/mesh'; +export * from '@pixi/particles'; export * from '@pixi/sprite'; export * from '@pixi/spritesheet'; export * from '@pixi/sprite-animated'; diff --git a/packages/particles/README.md b/packages/particles/README.md index bca3a96..dca806d 100644 --- a/packages/particles/README.md +++ b/packages/particles/README.md @@ -9,5 +9,8 @@ ## Usage ```js -import '@pixi/particles'; +import {ParticleRenderer} from '@pixi/particles'; +import {Renderer} from '@pixi/core'; + +Renderer.registerPlugin('particle', ParticleRenderer); ``` \ No newline at end of file diff --git a/packages/particles/src/ParticleBuffer.js b/packages/particles/src/ParticleBuffer.js new file mode 100644 index 0000000..6306bda --- /dev/null +++ b/packages/particles/src/ParticleBuffer.js @@ -0,0 +1,268 @@ +// import { VertexArrayObject } from 'pixi-gl-core'; +import { createIndicesForQuads } from '@pixi/utils'; +import { GLBuffer } from 'pixi-gl-core'; + +/** + * @author Mat Groves + * + * Big thanks to the very clever Matt DesLauriers https://github.com/mattdesl/ + * for creating the original PixiJS version! + * Also a thanks to https://github.com/bchevalier for tweaking the tint and alpha so that + * they now share 4 bytes on the vertex buffer + * + * Heavily inspired by LibGDX's ParticleBuffer: + * https://github.com/libgdx/libgdx/blob/master/gdx/src/com/badlogic/gdx/graphics/g2d/ParticleBuffer.java + */ + +/** + * The particle buffer manages the static and dynamic buffers for a particle container. + * + * @class + * @private + * @memberof PIXI + */ +export default class ParticleBuffer +{ + /** + * @param {WebGLRenderingContext} gl - The rendering context. + * @param {object} properties - The properties to upload. + * @param {boolean[]} dynamicPropertyFlags - Flags for which properties are dynamic. + * @param {number} size - The size of the batch. + */ + constructor(gl, properties, dynamicPropertyFlags, size) + { + /** + * The current WebGL drawing context. + * + * @member {WebGLRenderingContext} + */ + this.gl = gl; + + /** + * The number of particles the buffer can hold + * + * @member {number} + */ + this.size = size; + + /** + * A list of the properties that are dynamic. + * + * @member {object[]} + */ + this.dynamicProperties = []; + + /** + * A list of the properties that are static. + * + * @member {object[]} + */ + this.staticProperties = []; + + for (let i = 0; i < properties.length; ++i) + { + let property = properties[i]; + + // Make copy of properties object so that when we edit the offset it doesn't + // change all other instances of the object literal + property = { + attribute: property.attribute, + size: property.size, + uploadFunction: property.uploadFunction, + unsignedByte: property.unsignedByte, + offset: property.offset, + }; + + if (dynamicPropertyFlags[i]) + { + this.dynamicProperties.push(property); + } + else + { + this.staticProperties.push(property); + } + } + + this.staticStride = 0; + this.staticBuffer = null; + this.staticData = null; + this.staticDataUint32 = null; + + this.dynamicStride = 0; + this.dynamicBuffer = null; + this.dynamicData = null; + this.dynamicDataUint32 = null; + + this.initBuffers(); + } + + /** + * Sets up the renderer context and necessary buffers. + * + * @private + */ + initBuffers() + { + const gl = this.gl; + let dynamicOffset = 0; + + /** + * Holds the indices of the geometry (quads) to draw + * + * @member {Uint16Array} + */ + this.indices = createIndicesForQuads(this.size); + this.indexBuffer = GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); + + this.dynamicStride = 0; + + for (let i = 0; i < this.dynamicProperties.length; ++i) + { + const property = this.dynamicProperties[i]; + + property.offset = dynamicOffset; + dynamicOffset += property.size; + this.dynamicStride += property.size; + } + + this.dynamicData = new Float32Array(this.size * this.dynamicStride * 4); + this.dynamicBuffer = GLBuffer.createVertexBuffer(gl, this.dynamicData, gl.STREAM_DRAW); + + // static // + let staticOffset = 0; + + this.staticStride = 0; + + for (let i = 0; i < this.staticProperties.length; ++i) + { + const property = this.staticProperties[i]; + + property.offset = staticOffset; + staticOffset += property.size; + this.staticStride += property.size; + } + + this.staticData = new Float32Array(this.size * this.staticStride * 4); + this.staticBuffer = GLBuffer.createVertexBuffer(gl, this.staticData, gl.STATIC_DRAW); + + // this.vao = new VertexArrayObject(gl) + // .addIndex(this.indexBuffer); + + for (let i = 0; i < this.dynamicProperties.length; ++i) + { + const property = this.dynamicProperties[i]; + + if (property.unsignedByte) + { + this.vao.addAttribute( + this.dynamicBuffer, + property.attribute, + gl.UNSIGNED_BYTE, + true, + this.dynamicStride * 4, + property.offset * 4 + ); + } + else + { + this.vao.addAttribute( + this.dynamicBuffer, + property.attribute, + gl.FLOAT, + false, + this.dynamicStride * 4, + property.offset * 4 + ); + } + } + + for (let i = 0; i < this.staticProperties.length; ++i) + { + const property = this.staticProperties[i]; + + if (property.unsignedByte) + { + this.vao.addAttribute( + this.staticBuffer, + property.attribute, + gl.UNSIGNED_BYTE, + true, + this.staticStride * 4, + property.offset * 4 + ); + } + else + { + this.vao.addAttribute( + this.staticBuffer, + property.attribute, + gl.FLOAT, + false, + this.staticStride * 4, + property.offset * 4 + ); + } + } + } + + /** + * Uploads the dynamic properties. + * + * @param {PIXI.DisplayObject[]} children - The children to upload. + * @param {number} startIndex - The index to start at. + * @param {number} amount - The number to upload. + */ + uploadDynamic(children, startIndex, amount) + { + for (let i = 0; i < this.dynamicProperties.length; i++) + { + const property = this.dynamicProperties[i]; + + property.uploadFunction(children, startIndex, amount, + property.unsignedByte ? this.dynamicDataUint32 : this.dynamicData, + this.dynamicStride, property.offset); + } + + this.dynamicBuffer.upload(); + } + + /** + * Uploads the static properties. + * + * @param {PIXI.DisplayObject[]} children - The children to upload. + * @param {number} startIndex - The index to start at. + * @param {number} amount - The number to upload. + */ + uploadStatic(children, startIndex, amount) + { + for (let i = 0; i < this.staticProperties.length; i++) + { + const property = this.staticProperties[i]; + + property.uploadFunction(children, startIndex, amount, + property.unsignedByte ? this.staticDataUint32 : this.staticData, + this.staticStride, property.offset); + } + + this.staticBuffer.upload(); + } + + /** + * Destroys the ParticleBuffer. + * + */ + destroy() + { + this.dynamicProperties = null; + this.dynamicBuffer.destroy(); + this.dynamicBuffer = null; + this.dynamicData = null; + this.dynamicDataUint32 = null; + + this.staticProperties = null; + this.staticBuffer.destroy(); + this.staticBuffer = null; + this.staticData = null; + this.staticDataUint32 = null; + } +} diff --git a/packages/particles/src/ParticleContainer.js b/packages/particles/src/ParticleContainer.js index c8ade96..9d25c5b 100644 --- a/packages/particles/src/ParticleContainer.js +++ b/packages/particles/src/ParticleContainer.js @@ -25,7 +25,7 @@ * * @class * @extends PIXI.Container - * @memberof PIXI.particles + * @memberof PIXI */ export default class ParticleContainer extends Container { diff --git a/packages/particles/src/ParticleRenderer.js b/packages/particles/src/ParticleRenderer.js new file mode 100644 index 0000000..5464211 --- /dev/null +++ b/packages/particles/src/ParticleRenderer.js @@ -0,0 +1,455 @@ +import { ObjectRenderer } from '@pixi/core'; +import { correctBlendMode, premultiplyRgba, premultiplyTint } from '@pixi/utils'; +import { Matrix } from '@pixi/math'; +import ParticleShader from './ParticleShader'; +import ParticleBuffer from './ParticleBuffer'; + +/** + * @author Mat Groves + * + * Big thanks to the very clever Matt DesLauriers https://github.com/mattdesl/ + * for creating the original PixiJS version! + * Also a thanks to https://github.com/bchevalier for tweaking the tint and alpha so that they now + * share 4 bytes on the vertex buffer + * + * Heavily inspired by LibGDX's ParticleRenderer: + * https://github.com/libgdx/libgdx/blob/master/gdx/src/com/badlogic/gdx/graphics/g2d/ParticleRenderer.java + */ + +/** + * + * @class + * @memberof PIXI + */ +export default class ParticleRenderer extends ObjectRenderer +{ + /** + * @param {PIXI.Renderer} renderer - The renderer this sprite batch works for. + */ + constructor(renderer) + { + super(renderer); + + // 65535 is max vertex index in the index buffer (see ParticleRenderer) + // so max number of particles is 65536 / 4 = 16384 + // and max number of element in the index buffer is 16384 * 6 = 98304 + // Creating a full index buffer, overhead is 98304 * 2 = 196Ko + // let numIndices = 98304; + + /** + * The default shader that is used if a sprite doesn't have a more specific one. + * + * @member {PIXI.Shader} + */ + this.shader = null; + + this.indexBuffer = null; + + this.properties = null; + + this.tempMatrix = new Matrix(); + + this.CONTEXT_UID = 0; + } + + /** + * When there is a WebGL context change + * + * @private + */ + onContextChange() + { + const gl = this.renderer.gl; + + this.CONTEXT_UID = this.renderer.CONTEXT_UID; + + // setup default shader + this.shader = new ParticleShader(gl); + + this.properties = [ + // verticesData + { + attribute: this.shader.attributes.aVertexPosition, + size: 2, + uploadFunction: this.uploadVertices, + offset: 0, + }, + // positionData + { + attribute: this.shader.attributes.aPositionCoord, + size: 2, + uploadFunction: this.uploadPosition, + offset: 0, + }, + // rotationData + { + attribute: this.shader.attributes.aRotation, + size: 1, + uploadFunction: this.uploadRotation, + offset: 0, + }, + // uvsData + { + attribute: this.shader.attributes.aTextureCoord, + size: 2, + uploadFunction: this.uploadUvs, + offset: 0, + }, + // tintData + { + attribute: this.shader.attributes.aColor, + size: 1, + unsignedByte: true, + uploadFunction: this.uploadTint, + offset: 0, + }, + ]; + } + + /** + * Starts a new particle batch. + * + */ + start() + { + this.renderer._bindGLShader(this.shader); + } + + /** + * Renders the particle container object. + * + * @param {PIXI.ParticleContainer} container - The container to render using this ParticleRenderer + */ + render(container) + { + const children = container.children; + const maxSize = container._maxSize; + const batchSize = container._batchSize; + const renderer = this.renderer; + let totalChildren = children.length; + + if (totalChildren === 0) + { + return; + } + else if (totalChildren > maxSize) + { + totalChildren = maxSize; + } + + let buffers = container._glBuffers[renderer.CONTEXT_UID]; + + if (!buffers) + { + buffers = container._glBuffers[renderer.CONTEXT_UID] = this.generateBuffers(container); + } + + const baseTexture = children[0]._texture.baseTexture; + + // if the uvs have not updated then no point rendering just yet! + this.renderer.setBlendMode(correctBlendMode(container.blendMode, baseTexture.premultiplyAlpha)); + + const gl = renderer.gl; + + const m = container.worldTransform.copy(this.tempMatrix); + + m.prepend(renderer._activeRenderTarget.projectionMatrix); + + this.shader.uniforms.projectionMatrix = m.toArray(true); + + this.shader.uniforms.uColor = premultiplyRgba(container.tintRgb, + container.worldAlpha, this.shader.uniforms.uColor, baseTexture.premultiplyAlpha); + + // make sure the texture is bound.. + this.shader.uniforms.uSampler = renderer.bindTexture(baseTexture); + + // now lets upload and render the buffers.. + for (let i = 0, j = 0; i < totalChildren; i += batchSize, j += 1) + { + let amount = (totalChildren - i); + + if (amount > batchSize) + { + amount = batchSize; + } + + if (j >= buffers.length) + { + if (!container.autoResize) + { + break; + } + buffers.push(this._generateOneMoreBuffer(container)); + } + + const buffer = buffers[j]; + + // we always upload the dynamic + buffer.uploadDynamic(children, i, amount); + + // we only upload the static content when we have to! + if (container._bufferToUpdate === j) + { + buffer.uploadStatic(children, i, amount); + container._bufferToUpdate = j + 1; + } + + // bind the buffer + renderer.bindVao(buffer.vao); + buffer.vao.draw(gl.TRIANGLES, amount * 6); + } + } + + /** + * Creates one particle buffer for each child in the container we want to render and updates internal properties + * + * @param {PIXI.ParticleContainer} container - The container to render using this ParticleRenderer + * @return {PIXI.ParticleBuffer[]} The buffers + */ + generateBuffers(container) + { + const gl = this.renderer.gl; + const buffers = []; + const size = container._maxSize; + const batchSize = container._batchSize; + const dynamicPropertyFlags = container._properties; + + for (let i = 0; i < size; i += batchSize) + { + buffers.push(new ParticleBuffer(gl, this.properties, dynamicPropertyFlags, batchSize)); + } + + return buffers; + } + + /** + * Creates one more particle buffer, because container has autoResize feature + * + * @param {PIXI.ParticleContainer} container - The container to render using this ParticleRenderer + * @return {PIXI.ParticleBuffer} generated buffer + * @private + */ + _generateOneMoreBuffer(container) + { + const gl = this.renderer.gl; + const batchSize = container._batchSize; + const dynamicPropertyFlags = container._properties; + + return new ParticleBuffer(gl, this.properties, dynamicPropertyFlags, batchSize); + } + + /** + * Uploads the verticies. + * + * @param {PIXI.DisplayObject[]} children - the array of display objects to render + * @param {number} startIndex - the index to start from in the children array + * @param {number} amount - the amount of children that will have their vertices uploaded + * @param {number[]} array - The vertices to upload. + * @param {number} stride - Stride to use for iteration. + * @param {number} offset - Offset to start at. + */ + uploadVertices(children, startIndex, amount, array, stride, offset) + { + let w0 = 0; + let w1 = 0; + let h0 = 0; + let h1 = 0; + + for (let i = 0; i < amount; ++i) + { + const sprite = children[startIndex + i]; + const texture = sprite._texture; + const sx = sprite.scale.x; + const sy = sprite.scale.y; + const trim = texture.trim; + const orig = texture.orig; + + if (trim) + { + // if the sprite is trimmed and is not a tilingsprite then we need to add the + // extra space before transforming the sprite coords.. + w1 = trim.x - (sprite.anchor.x * orig.width); + w0 = w1 + trim.width; + + h1 = trim.y - (sprite.anchor.y * orig.height); + h0 = h1 + trim.height; + } + else + { + w0 = (orig.width) * (1 - sprite.anchor.x); + w1 = (orig.width) * -sprite.anchor.x; + + h0 = orig.height * (1 - sprite.anchor.y); + h1 = orig.height * -sprite.anchor.y; + } + + array[offset] = w1 * sx; + array[offset + 1] = h1 * sy; + + array[offset + stride] = w0 * sx; + array[offset + stride + 1] = h1 * sy; + + array[offset + (stride * 2)] = w0 * sx; + array[offset + (stride * 2) + 1] = h0 * sy; + + array[offset + (stride * 3)] = w1 * sx; + array[offset + (stride * 3) + 1] = h0 * sy; + + offset += stride * 4; + } + } + + /** + * + * @param {PIXI.DisplayObject[]} children - the array of display objects to render + * @param {number} startIndex - the index to start from in the children array + * @param {number} amount - the amount of children that will have their positions uploaded + * @param {number[]} array - The vertices to upload. + * @param {number} stride - Stride to use for iteration. + * @param {number} offset - Offset to start at. + */ + uploadPosition(children, startIndex, amount, array, stride, offset) + { + for (let i = 0; i < amount; i++) + { + const spritePosition = children[startIndex + i].position; + + array[offset] = spritePosition.x; + array[offset + 1] = spritePosition.y; + + array[offset + stride] = spritePosition.x; + array[offset + stride + 1] = spritePosition.y; + + array[offset + (stride * 2)] = spritePosition.x; + array[offset + (stride * 2) + 1] = spritePosition.y; + + array[offset + (stride * 3)] = spritePosition.x; + array[offset + (stride * 3) + 1] = spritePosition.y; + + offset += stride * 4; + } + } + + /** + * + * @param {PIXI.DisplayObject[]} children - the array of display objects to render + * @param {number} startIndex - the index to start from in the children array + * @param {number} amount - the amount of children that will have their rotation uploaded + * @param {number[]} array - The vertices to upload. + * @param {number} stride - Stride to use for iteration. + * @param {number} offset - Offset to start at. + */ + uploadRotation(children, startIndex, amount, array, stride, offset) + { + for (let i = 0; i < amount; i++) + { + const spriteRotation = children[startIndex + i].rotation; + + array[offset] = spriteRotation; + array[offset + stride] = spriteRotation; + array[offset + (stride * 2)] = spriteRotation; + array[offset + (stride * 3)] = spriteRotation; + + offset += stride * 4; + } + } + + /** + * + * @param {PIXI.DisplayObject[]} children - the array of display objects to render + * @param {number} startIndex - the index to start from in the children array + * @param {number} amount - the amount of children that will have their rotation uploaded + * @param {number[]} array - The vertices to upload. + * @param {number} stride - Stride to use for iteration. + * @param {number} offset - Offset to start at. + */ + uploadUvs(children, startIndex, amount, array, stride, offset) + { + for (let i = 0; i < amount; ++i) + { + const textureUvs = children[startIndex + i]._texture._uvs; + + if (textureUvs) + { + array[offset] = textureUvs.x0; + array[offset + 1] = textureUvs.y0; + + array[offset + stride] = textureUvs.x1; + array[offset + stride + 1] = textureUvs.y1; + + array[offset + (stride * 2)] = textureUvs.x2; + array[offset + (stride * 2) + 1] = textureUvs.y2; + + array[offset + (stride * 3)] = textureUvs.x3; + array[offset + (stride * 3) + 1] = textureUvs.y3; + + offset += stride * 4; + } + else + { + // TODO you know this can be easier! + array[offset] = 0; + array[offset + 1] = 0; + + array[offset + stride] = 0; + array[offset + stride + 1] = 0; + + array[offset + (stride * 2)] = 0; + array[offset + (stride * 2) + 1] = 0; + + array[offset + (stride * 3)] = 0; + array[offset + (stride * 3) + 1] = 0; + + offset += stride * 4; + } + } + } + + /** + * + * @param {PIXI.DisplayObject[]} children - the array of display objects to render + * @param {number} startIndex - the index to start from in the children array + * @param {number} amount - the amount of children that will have their rotation uploaded + * @param {number[]} array - The vertices to upload. + * @param {number} stride - Stride to use for iteration. + * @param {number} offset - Offset to start at. + */ + uploadTint(children, startIndex, amount, array, stride, offset) + { + for (let i = 0; i < amount; ++i) + { + const sprite = children[startIndex + i]; + const premultiplied = sprite._texture.baseTexture.premultiplyAlpha; + const alpha = sprite.alpha; + // we dont call extra function if alpha is 1.0, that's faster + const argb = alpha < 1.0 && premultiplied ? premultiplyTint(sprite._tintRGB, alpha) + : sprite._tintRGB + (alpha * 255 << 24); + + array[offset] = argb; + array[offset + stride] = argb; + array[offset + (stride * 2)] = argb; + array[offset + (stride * 3)] = argb; + + offset += stride * 4; + } + } + + /** + * Destroys the ParticleRenderer. + * + */ + destroy() + { + if (this.renderer.gl) + { + this.renderer.gl.deleteBuffer(this.indexBuffer); + } + + super.destroy(); + + this.shader.destroy(); + + this.indices = null; + this.tempMatrix = null; + } +} diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index e59c66a..a390a1a 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -332,6 +332,43 @@ /* * This namespace has been removed and items have been moved to * the top-level `PIXI` object. + * @namespace PIXI.particles + * @deprecated since 5.0.0 + */ + PIXI.particles = {}; + + Object.defineProperties(PIXI.particles, { + /** + * @class PIXI.particles.ParticleContainer + * @deprecated since 5.0.0 + * @see PIXI.ParticleContainer + */ + ParticleContainer: { + get() + { + warn('PIXI.particles.ParticleContainer has moved to PIXI.ParticleContainer'); + + return PIXI.ParticleContainer; + }, + }, + /** + * @class PIXI.particles.ParticleRenderer + * @deprecated since 5.0.0 + * @see PIXI.ParticleRenderer + */ + ParticleRenderer: { + get() + { + warn('PIXI.particles.ParticleRenderer has moved to PIXI.ParticleRenderer'); + + return PIXI.ParticleRenderer; + }, + }, + }); + + /* + * This namespace has been removed and items have been moved to + * the top-level `PIXI` object. * @namespace PIXI.ticker * @deprecated since 5.0.0 */ diff --git a/bundles/pixi.js/src/index.js b/bundles/pixi.js/src/index.js index 52d7cb4..fb839a0 100644 --- a/bundles/pixi.js/src/index.js +++ b/bundles/pixi.js/src/index.js @@ -9,6 +9,7 @@ import * as loaders from '@pixi/loaders'; import * as math from '@pixi/math'; import * as mesh from '@pixi/mesh'; +import * as particles from '@pixi/particles'; import * as prepare from '@pixi/prepare'; import * as sprite from '@pixi/sprite'; import * as spriteAnimated from '@pixi/sprite-animated'; @@ -37,6 +38,7 @@ core.Renderer.registerPlugin('graphics', graphics.GraphicsRenderer); core.Renderer.registerPlugin('interaction', interaction.InteractionManager); core.Renderer.registerPlugin('mesh', mesh.MeshRenderer); +core.Renderer.registerPlugin('particle', particles.ParticleRenderer); core.Renderer.registerPlugin('prepare', prepare.Prepare); core.Renderer.registerPlugin('sprite', sprite.SpriteRenderer); core.Renderer.registerPlugin('tilingSprite', spriteTiling.TilingSpriteRenderer); @@ -118,6 +120,7 @@ loaders, math, mesh, + particles, sprite, spriteAnimated, spritesheet, @@ -141,6 +144,7 @@ export * from '@pixi/loaders'; export * from '@pixi/math'; export * from '@pixi/mesh'; +export * from '@pixi/particles'; export * from '@pixi/sprite'; export * from '@pixi/spritesheet'; export * from '@pixi/sprite-animated'; diff --git a/packages/particles/README.md b/packages/particles/README.md index bca3a96..dca806d 100644 --- a/packages/particles/README.md +++ b/packages/particles/README.md @@ -9,5 +9,8 @@ ## Usage ```js -import '@pixi/particles'; +import {ParticleRenderer} from '@pixi/particles'; +import {Renderer} from '@pixi/core'; + +Renderer.registerPlugin('particle', ParticleRenderer); ``` \ No newline at end of file diff --git a/packages/particles/src/ParticleBuffer.js b/packages/particles/src/ParticleBuffer.js new file mode 100644 index 0000000..6306bda --- /dev/null +++ b/packages/particles/src/ParticleBuffer.js @@ -0,0 +1,268 @@ +// import { VertexArrayObject } from 'pixi-gl-core'; +import { createIndicesForQuads } from '@pixi/utils'; +import { GLBuffer } from 'pixi-gl-core'; + +/** + * @author Mat Groves + * + * Big thanks to the very clever Matt DesLauriers https://github.com/mattdesl/ + * for creating the original PixiJS version! + * Also a thanks to https://github.com/bchevalier for tweaking the tint and alpha so that + * they now share 4 bytes on the vertex buffer + * + * Heavily inspired by LibGDX's ParticleBuffer: + * https://github.com/libgdx/libgdx/blob/master/gdx/src/com/badlogic/gdx/graphics/g2d/ParticleBuffer.java + */ + +/** + * The particle buffer manages the static and dynamic buffers for a particle container. + * + * @class + * @private + * @memberof PIXI + */ +export default class ParticleBuffer +{ + /** + * @param {WebGLRenderingContext} gl - The rendering context. + * @param {object} properties - The properties to upload. + * @param {boolean[]} dynamicPropertyFlags - Flags for which properties are dynamic. + * @param {number} size - The size of the batch. + */ + constructor(gl, properties, dynamicPropertyFlags, size) + { + /** + * The current WebGL drawing context. + * + * @member {WebGLRenderingContext} + */ + this.gl = gl; + + /** + * The number of particles the buffer can hold + * + * @member {number} + */ + this.size = size; + + /** + * A list of the properties that are dynamic. + * + * @member {object[]} + */ + this.dynamicProperties = []; + + /** + * A list of the properties that are static. + * + * @member {object[]} + */ + this.staticProperties = []; + + for (let i = 0; i < properties.length; ++i) + { + let property = properties[i]; + + // Make copy of properties object so that when we edit the offset it doesn't + // change all other instances of the object literal + property = { + attribute: property.attribute, + size: property.size, + uploadFunction: property.uploadFunction, + unsignedByte: property.unsignedByte, + offset: property.offset, + }; + + if (dynamicPropertyFlags[i]) + { + this.dynamicProperties.push(property); + } + else + { + this.staticProperties.push(property); + } + } + + this.staticStride = 0; + this.staticBuffer = null; + this.staticData = null; + this.staticDataUint32 = null; + + this.dynamicStride = 0; + this.dynamicBuffer = null; + this.dynamicData = null; + this.dynamicDataUint32 = null; + + this.initBuffers(); + } + + /** + * Sets up the renderer context and necessary buffers. + * + * @private + */ + initBuffers() + { + const gl = this.gl; + let dynamicOffset = 0; + + /** + * Holds the indices of the geometry (quads) to draw + * + * @member {Uint16Array} + */ + this.indices = createIndicesForQuads(this.size); + this.indexBuffer = GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); + + this.dynamicStride = 0; + + for (let i = 0; i < this.dynamicProperties.length; ++i) + { + const property = this.dynamicProperties[i]; + + property.offset = dynamicOffset; + dynamicOffset += property.size; + this.dynamicStride += property.size; + } + + this.dynamicData = new Float32Array(this.size * this.dynamicStride * 4); + this.dynamicBuffer = GLBuffer.createVertexBuffer(gl, this.dynamicData, gl.STREAM_DRAW); + + // static // + let staticOffset = 0; + + this.staticStride = 0; + + for (let i = 0; i < this.staticProperties.length; ++i) + { + const property = this.staticProperties[i]; + + property.offset = staticOffset; + staticOffset += property.size; + this.staticStride += property.size; + } + + this.staticData = new Float32Array(this.size * this.staticStride * 4); + this.staticBuffer = GLBuffer.createVertexBuffer(gl, this.staticData, gl.STATIC_DRAW); + + // this.vao = new VertexArrayObject(gl) + // .addIndex(this.indexBuffer); + + for (let i = 0; i < this.dynamicProperties.length; ++i) + { + const property = this.dynamicProperties[i]; + + if (property.unsignedByte) + { + this.vao.addAttribute( + this.dynamicBuffer, + property.attribute, + gl.UNSIGNED_BYTE, + true, + this.dynamicStride * 4, + property.offset * 4 + ); + } + else + { + this.vao.addAttribute( + this.dynamicBuffer, + property.attribute, + gl.FLOAT, + false, + this.dynamicStride * 4, + property.offset * 4 + ); + } + } + + for (let i = 0; i < this.staticProperties.length; ++i) + { + const property = this.staticProperties[i]; + + if (property.unsignedByte) + { + this.vao.addAttribute( + this.staticBuffer, + property.attribute, + gl.UNSIGNED_BYTE, + true, + this.staticStride * 4, + property.offset * 4 + ); + } + else + { + this.vao.addAttribute( + this.staticBuffer, + property.attribute, + gl.FLOAT, + false, + this.staticStride * 4, + property.offset * 4 + ); + } + } + } + + /** + * Uploads the dynamic properties. + * + * @param {PIXI.DisplayObject[]} children - The children to upload. + * @param {number} startIndex - The index to start at. + * @param {number} amount - The number to upload. + */ + uploadDynamic(children, startIndex, amount) + { + for (let i = 0; i < this.dynamicProperties.length; i++) + { + const property = this.dynamicProperties[i]; + + property.uploadFunction(children, startIndex, amount, + property.unsignedByte ? this.dynamicDataUint32 : this.dynamicData, + this.dynamicStride, property.offset); + } + + this.dynamicBuffer.upload(); + } + + /** + * Uploads the static properties. + * + * @param {PIXI.DisplayObject[]} children - The children to upload. + * @param {number} startIndex - The index to start at. + * @param {number} amount - The number to upload. + */ + uploadStatic(children, startIndex, amount) + { + for (let i = 0; i < this.staticProperties.length; i++) + { + const property = this.staticProperties[i]; + + property.uploadFunction(children, startIndex, amount, + property.unsignedByte ? this.staticDataUint32 : this.staticData, + this.staticStride, property.offset); + } + + this.staticBuffer.upload(); + } + + /** + * Destroys the ParticleBuffer. + * + */ + destroy() + { + this.dynamicProperties = null; + this.dynamicBuffer.destroy(); + this.dynamicBuffer = null; + this.dynamicData = null; + this.dynamicDataUint32 = null; + + this.staticProperties = null; + this.staticBuffer.destroy(); + this.staticBuffer = null; + this.staticData = null; + this.staticDataUint32 = null; + } +} diff --git a/packages/particles/src/ParticleContainer.js b/packages/particles/src/ParticleContainer.js index c8ade96..9d25c5b 100644 --- a/packages/particles/src/ParticleContainer.js +++ b/packages/particles/src/ParticleContainer.js @@ -25,7 +25,7 @@ * * @class * @extends PIXI.Container - * @memberof PIXI.particles + * @memberof PIXI */ export default class ParticleContainer extends Container { diff --git a/packages/particles/src/ParticleRenderer.js b/packages/particles/src/ParticleRenderer.js new file mode 100644 index 0000000..5464211 --- /dev/null +++ b/packages/particles/src/ParticleRenderer.js @@ -0,0 +1,455 @@ +import { ObjectRenderer } from '@pixi/core'; +import { correctBlendMode, premultiplyRgba, premultiplyTint } from '@pixi/utils'; +import { Matrix } from '@pixi/math'; +import ParticleShader from './ParticleShader'; +import ParticleBuffer from './ParticleBuffer'; + +/** + * @author Mat Groves + * + * Big thanks to the very clever Matt DesLauriers https://github.com/mattdesl/ + * for creating the original PixiJS version! + * Also a thanks to https://github.com/bchevalier for tweaking the tint and alpha so that they now + * share 4 bytes on the vertex buffer + * + * Heavily inspired by LibGDX's ParticleRenderer: + * https://github.com/libgdx/libgdx/blob/master/gdx/src/com/badlogic/gdx/graphics/g2d/ParticleRenderer.java + */ + +/** + * + * @class + * @memberof PIXI + */ +export default class ParticleRenderer extends ObjectRenderer +{ + /** + * @param {PIXI.Renderer} renderer - The renderer this sprite batch works for. + */ + constructor(renderer) + { + super(renderer); + + // 65535 is max vertex index in the index buffer (see ParticleRenderer) + // so max number of particles is 65536 / 4 = 16384 + // and max number of element in the index buffer is 16384 * 6 = 98304 + // Creating a full index buffer, overhead is 98304 * 2 = 196Ko + // let numIndices = 98304; + + /** + * The default shader that is used if a sprite doesn't have a more specific one. + * + * @member {PIXI.Shader} + */ + this.shader = null; + + this.indexBuffer = null; + + this.properties = null; + + this.tempMatrix = new Matrix(); + + this.CONTEXT_UID = 0; + } + + /** + * When there is a WebGL context change + * + * @private + */ + onContextChange() + { + const gl = this.renderer.gl; + + this.CONTEXT_UID = this.renderer.CONTEXT_UID; + + // setup default shader + this.shader = new ParticleShader(gl); + + this.properties = [ + // verticesData + { + attribute: this.shader.attributes.aVertexPosition, + size: 2, + uploadFunction: this.uploadVertices, + offset: 0, + }, + // positionData + { + attribute: this.shader.attributes.aPositionCoord, + size: 2, + uploadFunction: this.uploadPosition, + offset: 0, + }, + // rotationData + { + attribute: this.shader.attributes.aRotation, + size: 1, + uploadFunction: this.uploadRotation, + offset: 0, + }, + // uvsData + { + attribute: this.shader.attributes.aTextureCoord, + size: 2, + uploadFunction: this.uploadUvs, + offset: 0, + }, + // tintData + { + attribute: this.shader.attributes.aColor, + size: 1, + unsignedByte: true, + uploadFunction: this.uploadTint, + offset: 0, + }, + ]; + } + + /** + * Starts a new particle batch. + * + */ + start() + { + this.renderer._bindGLShader(this.shader); + } + + /** + * Renders the particle container object. + * + * @param {PIXI.ParticleContainer} container - The container to render using this ParticleRenderer + */ + render(container) + { + const children = container.children; + const maxSize = container._maxSize; + const batchSize = container._batchSize; + const renderer = this.renderer; + let totalChildren = children.length; + + if (totalChildren === 0) + { + return; + } + else if (totalChildren > maxSize) + { + totalChildren = maxSize; + } + + let buffers = container._glBuffers[renderer.CONTEXT_UID]; + + if (!buffers) + { + buffers = container._glBuffers[renderer.CONTEXT_UID] = this.generateBuffers(container); + } + + const baseTexture = children[0]._texture.baseTexture; + + // if the uvs have not updated then no point rendering just yet! + this.renderer.setBlendMode(correctBlendMode(container.blendMode, baseTexture.premultiplyAlpha)); + + const gl = renderer.gl; + + const m = container.worldTransform.copy(this.tempMatrix); + + m.prepend(renderer._activeRenderTarget.projectionMatrix); + + this.shader.uniforms.projectionMatrix = m.toArray(true); + + this.shader.uniforms.uColor = premultiplyRgba(container.tintRgb, + container.worldAlpha, this.shader.uniforms.uColor, baseTexture.premultiplyAlpha); + + // make sure the texture is bound.. + this.shader.uniforms.uSampler = renderer.bindTexture(baseTexture); + + // now lets upload and render the buffers.. + for (let i = 0, j = 0; i < totalChildren; i += batchSize, j += 1) + { + let amount = (totalChildren - i); + + if (amount > batchSize) + { + amount = batchSize; + } + + if (j >= buffers.length) + { + if (!container.autoResize) + { + break; + } + buffers.push(this._generateOneMoreBuffer(container)); + } + + const buffer = buffers[j]; + + // we always upload the dynamic + buffer.uploadDynamic(children, i, amount); + + // we only upload the static content when we have to! + if (container._bufferToUpdate === j) + { + buffer.uploadStatic(children, i, amount); + container._bufferToUpdate = j + 1; + } + + // bind the buffer + renderer.bindVao(buffer.vao); + buffer.vao.draw(gl.TRIANGLES, amount * 6); + } + } + + /** + * Creates one particle buffer for each child in the container we want to render and updates internal properties + * + * @param {PIXI.ParticleContainer} container - The container to render using this ParticleRenderer + * @return {PIXI.ParticleBuffer[]} The buffers + */ + generateBuffers(container) + { + const gl = this.renderer.gl; + const buffers = []; + const size = container._maxSize; + const batchSize = container._batchSize; + const dynamicPropertyFlags = container._properties; + + for (let i = 0; i < size; i += batchSize) + { + buffers.push(new ParticleBuffer(gl, this.properties, dynamicPropertyFlags, batchSize)); + } + + return buffers; + } + + /** + * Creates one more particle buffer, because container has autoResize feature + * + * @param {PIXI.ParticleContainer} container - The container to render using this ParticleRenderer + * @return {PIXI.ParticleBuffer} generated buffer + * @private + */ + _generateOneMoreBuffer(container) + { + const gl = this.renderer.gl; + const batchSize = container._batchSize; + const dynamicPropertyFlags = container._properties; + + return new ParticleBuffer(gl, this.properties, dynamicPropertyFlags, batchSize); + } + + /** + * Uploads the verticies. + * + * @param {PIXI.DisplayObject[]} children - the array of display objects to render + * @param {number} startIndex - the index to start from in the children array + * @param {number} amount - the amount of children that will have their vertices uploaded + * @param {number[]} array - The vertices to upload. + * @param {number} stride - Stride to use for iteration. + * @param {number} offset - Offset to start at. + */ + uploadVertices(children, startIndex, amount, array, stride, offset) + { + let w0 = 0; + let w1 = 0; + let h0 = 0; + let h1 = 0; + + for (let i = 0; i < amount; ++i) + { + const sprite = children[startIndex + i]; + const texture = sprite._texture; + const sx = sprite.scale.x; + const sy = sprite.scale.y; + const trim = texture.trim; + const orig = texture.orig; + + if (trim) + { + // if the sprite is trimmed and is not a tilingsprite then we need to add the + // extra space before transforming the sprite coords.. + w1 = trim.x - (sprite.anchor.x * orig.width); + w0 = w1 + trim.width; + + h1 = trim.y - (sprite.anchor.y * orig.height); + h0 = h1 + trim.height; + } + else + { + w0 = (orig.width) * (1 - sprite.anchor.x); + w1 = (orig.width) * -sprite.anchor.x; + + h0 = orig.height * (1 - sprite.anchor.y); + h1 = orig.height * -sprite.anchor.y; + } + + array[offset] = w1 * sx; + array[offset + 1] = h1 * sy; + + array[offset + stride] = w0 * sx; + array[offset + stride + 1] = h1 * sy; + + array[offset + (stride * 2)] = w0 * sx; + array[offset + (stride * 2) + 1] = h0 * sy; + + array[offset + (stride * 3)] = w1 * sx; + array[offset + (stride * 3) + 1] = h0 * sy; + + offset += stride * 4; + } + } + + /** + * + * @param {PIXI.DisplayObject[]} children - the array of display objects to render + * @param {number} startIndex - the index to start from in the children array + * @param {number} amount - the amount of children that will have their positions uploaded + * @param {number[]} array - The vertices to upload. + * @param {number} stride - Stride to use for iteration. + * @param {number} offset - Offset to start at. + */ + uploadPosition(children, startIndex, amount, array, stride, offset) + { + for (let i = 0; i < amount; i++) + { + const spritePosition = children[startIndex + i].position; + + array[offset] = spritePosition.x; + array[offset + 1] = spritePosition.y; + + array[offset + stride] = spritePosition.x; + array[offset + stride + 1] = spritePosition.y; + + array[offset + (stride * 2)] = spritePosition.x; + array[offset + (stride * 2) + 1] = spritePosition.y; + + array[offset + (stride * 3)] = spritePosition.x; + array[offset + (stride * 3) + 1] = spritePosition.y; + + offset += stride * 4; + } + } + + /** + * + * @param {PIXI.DisplayObject[]} children - the array of display objects to render + * @param {number} startIndex - the index to start from in the children array + * @param {number} amount - the amount of children that will have their rotation uploaded + * @param {number[]} array - The vertices to upload. + * @param {number} stride - Stride to use for iteration. + * @param {number} offset - Offset to start at. + */ + uploadRotation(children, startIndex, amount, array, stride, offset) + { + for (let i = 0; i < amount; i++) + { + const spriteRotation = children[startIndex + i].rotation; + + array[offset] = spriteRotation; + array[offset + stride] = spriteRotation; + array[offset + (stride * 2)] = spriteRotation; + array[offset + (stride * 3)] = spriteRotation; + + offset += stride * 4; + } + } + + /** + * + * @param {PIXI.DisplayObject[]} children - the array of display objects to render + * @param {number} startIndex - the index to start from in the children array + * @param {number} amount - the amount of children that will have their rotation uploaded + * @param {number[]} array - The vertices to upload. + * @param {number} stride - Stride to use for iteration. + * @param {number} offset - Offset to start at. + */ + uploadUvs(children, startIndex, amount, array, stride, offset) + { + for (let i = 0; i < amount; ++i) + { + const textureUvs = children[startIndex + i]._texture._uvs; + + if (textureUvs) + { + array[offset] = textureUvs.x0; + array[offset + 1] = textureUvs.y0; + + array[offset + stride] = textureUvs.x1; + array[offset + stride + 1] = textureUvs.y1; + + array[offset + (stride * 2)] = textureUvs.x2; + array[offset + (stride * 2) + 1] = textureUvs.y2; + + array[offset + (stride * 3)] = textureUvs.x3; + array[offset + (stride * 3) + 1] = textureUvs.y3; + + offset += stride * 4; + } + else + { + // TODO you know this can be easier! + array[offset] = 0; + array[offset + 1] = 0; + + array[offset + stride] = 0; + array[offset + stride + 1] = 0; + + array[offset + (stride * 2)] = 0; + array[offset + (stride * 2) + 1] = 0; + + array[offset + (stride * 3)] = 0; + array[offset + (stride * 3) + 1] = 0; + + offset += stride * 4; + } + } + } + + /** + * + * @param {PIXI.DisplayObject[]} children - the array of display objects to render + * @param {number} startIndex - the index to start from in the children array + * @param {number} amount - the amount of children that will have their rotation uploaded + * @param {number[]} array - The vertices to upload. + * @param {number} stride - Stride to use for iteration. + * @param {number} offset - Offset to start at. + */ + uploadTint(children, startIndex, amount, array, stride, offset) + { + for (let i = 0; i < amount; ++i) + { + const sprite = children[startIndex + i]; + const premultiplied = sprite._texture.baseTexture.premultiplyAlpha; + const alpha = sprite.alpha; + // we dont call extra function if alpha is 1.0, that's faster + const argb = alpha < 1.0 && premultiplied ? premultiplyTint(sprite._tintRGB, alpha) + : sprite._tintRGB + (alpha * 255 << 24); + + array[offset] = argb; + array[offset + stride] = argb; + array[offset + (stride * 2)] = argb; + array[offset + (stride * 3)] = argb; + + offset += stride * 4; + } + } + + /** + * Destroys the ParticleRenderer. + * + */ + destroy() + { + if (this.renderer.gl) + { + this.renderer.gl.deleteBuffer(this.indexBuffer); + } + + super.destroy(); + + this.shader.destroy(); + + this.indices = null; + this.tempMatrix = null; + } +} diff --git a/packages/particles/src/ParticleShader.js b/packages/particles/src/ParticleShader.js new file mode 100644 index 0000000..069afe6 --- /dev/null +++ b/packages/particles/src/ParticleShader.js @@ -0,0 +1,21 @@ +import { GLShader } from 'pixi-gl-core'; +import { PRECISION } from '@pixi/constants'; +import vertex from './particles.vert'; +import fragment from './particles.frag'; + +/** + * @class + * @extends PIXI.Shader + * @memberof PIXI + * @private + */ +export default class ParticleShader extends GLShader +{ + /** + * @param {PIXI.Shader} gl - The webgl shader manager this shader works for. + */ + constructor(gl) + { + super(gl, vertex, fragment, PRECISION.DEFAULT); + } +} diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index e59c66a..a390a1a 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -332,6 +332,43 @@ /* * This namespace has been removed and items have been moved to * the top-level `PIXI` object. + * @namespace PIXI.particles + * @deprecated since 5.0.0 + */ + PIXI.particles = {}; + + Object.defineProperties(PIXI.particles, { + /** + * @class PIXI.particles.ParticleContainer + * @deprecated since 5.0.0 + * @see PIXI.ParticleContainer + */ + ParticleContainer: { + get() + { + warn('PIXI.particles.ParticleContainer has moved to PIXI.ParticleContainer'); + + return PIXI.ParticleContainer; + }, + }, + /** + * @class PIXI.particles.ParticleRenderer + * @deprecated since 5.0.0 + * @see PIXI.ParticleRenderer + */ + ParticleRenderer: { + get() + { + warn('PIXI.particles.ParticleRenderer has moved to PIXI.ParticleRenderer'); + + return PIXI.ParticleRenderer; + }, + }, + }); + + /* + * This namespace has been removed and items have been moved to + * the top-level `PIXI` object. * @namespace PIXI.ticker * @deprecated since 5.0.0 */ diff --git a/bundles/pixi.js/src/index.js b/bundles/pixi.js/src/index.js index 52d7cb4..fb839a0 100644 --- a/bundles/pixi.js/src/index.js +++ b/bundles/pixi.js/src/index.js @@ -9,6 +9,7 @@ import * as loaders from '@pixi/loaders'; import * as math from '@pixi/math'; import * as mesh from '@pixi/mesh'; +import * as particles from '@pixi/particles'; import * as prepare from '@pixi/prepare'; import * as sprite from '@pixi/sprite'; import * as spriteAnimated from '@pixi/sprite-animated'; @@ -37,6 +38,7 @@ core.Renderer.registerPlugin('graphics', graphics.GraphicsRenderer); core.Renderer.registerPlugin('interaction', interaction.InteractionManager); core.Renderer.registerPlugin('mesh', mesh.MeshRenderer); +core.Renderer.registerPlugin('particle', particles.ParticleRenderer); core.Renderer.registerPlugin('prepare', prepare.Prepare); core.Renderer.registerPlugin('sprite', sprite.SpriteRenderer); core.Renderer.registerPlugin('tilingSprite', spriteTiling.TilingSpriteRenderer); @@ -118,6 +120,7 @@ loaders, math, mesh, + particles, sprite, spriteAnimated, spritesheet, @@ -141,6 +144,7 @@ export * from '@pixi/loaders'; export * from '@pixi/math'; export * from '@pixi/mesh'; +export * from '@pixi/particles'; export * from '@pixi/sprite'; export * from '@pixi/spritesheet'; export * from '@pixi/sprite-animated'; diff --git a/packages/particles/README.md b/packages/particles/README.md index bca3a96..dca806d 100644 --- a/packages/particles/README.md +++ b/packages/particles/README.md @@ -9,5 +9,8 @@ ## Usage ```js -import '@pixi/particles'; +import {ParticleRenderer} from '@pixi/particles'; +import {Renderer} from '@pixi/core'; + +Renderer.registerPlugin('particle', ParticleRenderer); ``` \ No newline at end of file diff --git a/packages/particles/src/ParticleBuffer.js b/packages/particles/src/ParticleBuffer.js new file mode 100644 index 0000000..6306bda --- /dev/null +++ b/packages/particles/src/ParticleBuffer.js @@ -0,0 +1,268 @@ +// import { VertexArrayObject } from 'pixi-gl-core'; +import { createIndicesForQuads } from '@pixi/utils'; +import { GLBuffer } from 'pixi-gl-core'; + +/** + * @author Mat Groves + * + * Big thanks to the very clever Matt DesLauriers https://github.com/mattdesl/ + * for creating the original PixiJS version! + * Also a thanks to https://github.com/bchevalier for tweaking the tint and alpha so that + * they now share 4 bytes on the vertex buffer + * + * Heavily inspired by LibGDX's ParticleBuffer: + * https://github.com/libgdx/libgdx/blob/master/gdx/src/com/badlogic/gdx/graphics/g2d/ParticleBuffer.java + */ + +/** + * The particle buffer manages the static and dynamic buffers for a particle container. + * + * @class + * @private + * @memberof PIXI + */ +export default class ParticleBuffer +{ + /** + * @param {WebGLRenderingContext} gl - The rendering context. + * @param {object} properties - The properties to upload. + * @param {boolean[]} dynamicPropertyFlags - Flags for which properties are dynamic. + * @param {number} size - The size of the batch. + */ + constructor(gl, properties, dynamicPropertyFlags, size) + { + /** + * The current WebGL drawing context. + * + * @member {WebGLRenderingContext} + */ + this.gl = gl; + + /** + * The number of particles the buffer can hold + * + * @member {number} + */ + this.size = size; + + /** + * A list of the properties that are dynamic. + * + * @member {object[]} + */ + this.dynamicProperties = []; + + /** + * A list of the properties that are static. + * + * @member {object[]} + */ + this.staticProperties = []; + + for (let i = 0; i < properties.length; ++i) + { + let property = properties[i]; + + // Make copy of properties object so that when we edit the offset it doesn't + // change all other instances of the object literal + property = { + attribute: property.attribute, + size: property.size, + uploadFunction: property.uploadFunction, + unsignedByte: property.unsignedByte, + offset: property.offset, + }; + + if (dynamicPropertyFlags[i]) + { + this.dynamicProperties.push(property); + } + else + { + this.staticProperties.push(property); + } + } + + this.staticStride = 0; + this.staticBuffer = null; + this.staticData = null; + this.staticDataUint32 = null; + + this.dynamicStride = 0; + this.dynamicBuffer = null; + this.dynamicData = null; + this.dynamicDataUint32 = null; + + this.initBuffers(); + } + + /** + * Sets up the renderer context and necessary buffers. + * + * @private + */ + initBuffers() + { + const gl = this.gl; + let dynamicOffset = 0; + + /** + * Holds the indices of the geometry (quads) to draw + * + * @member {Uint16Array} + */ + this.indices = createIndicesForQuads(this.size); + this.indexBuffer = GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); + + this.dynamicStride = 0; + + for (let i = 0; i < this.dynamicProperties.length; ++i) + { + const property = this.dynamicProperties[i]; + + property.offset = dynamicOffset; + dynamicOffset += property.size; + this.dynamicStride += property.size; + } + + this.dynamicData = new Float32Array(this.size * this.dynamicStride * 4); + this.dynamicBuffer = GLBuffer.createVertexBuffer(gl, this.dynamicData, gl.STREAM_DRAW); + + // static // + let staticOffset = 0; + + this.staticStride = 0; + + for (let i = 0; i < this.staticProperties.length; ++i) + { + const property = this.staticProperties[i]; + + property.offset = staticOffset; + staticOffset += property.size; + this.staticStride += property.size; + } + + this.staticData = new Float32Array(this.size * this.staticStride * 4); + this.staticBuffer = GLBuffer.createVertexBuffer(gl, this.staticData, gl.STATIC_DRAW); + + // this.vao = new VertexArrayObject(gl) + // .addIndex(this.indexBuffer); + + for (let i = 0; i < this.dynamicProperties.length; ++i) + { + const property = this.dynamicProperties[i]; + + if (property.unsignedByte) + { + this.vao.addAttribute( + this.dynamicBuffer, + property.attribute, + gl.UNSIGNED_BYTE, + true, + this.dynamicStride * 4, + property.offset * 4 + ); + } + else + { + this.vao.addAttribute( + this.dynamicBuffer, + property.attribute, + gl.FLOAT, + false, + this.dynamicStride * 4, + property.offset * 4 + ); + } + } + + for (let i = 0; i < this.staticProperties.length; ++i) + { + const property = this.staticProperties[i]; + + if (property.unsignedByte) + { + this.vao.addAttribute( + this.staticBuffer, + property.attribute, + gl.UNSIGNED_BYTE, + true, + this.staticStride * 4, + property.offset * 4 + ); + } + else + { + this.vao.addAttribute( + this.staticBuffer, + property.attribute, + gl.FLOAT, + false, + this.staticStride * 4, + property.offset * 4 + ); + } + } + } + + /** + * Uploads the dynamic properties. + * + * @param {PIXI.DisplayObject[]} children - The children to upload. + * @param {number} startIndex - The index to start at. + * @param {number} amount - The number to upload. + */ + uploadDynamic(children, startIndex, amount) + { + for (let i = 0; i < this.dynamicProperties.length; i++) + { + const property = this.dynamicProperties[i]; + + property.uploadFunction(children, startIndex, amount, + property.unsignedByte ? this.dynamicDataUint32 : this.dynamicData, + this.dynamicStride, property.offset); + } + + this.dynamicBuffer.upload(); + } + + /** + * Uploads the static properties. + * + * @param {PIXI.DisplayObject[]} children - The children to upload. + * @param {number} startIndex - The index to start at. + * @param {number} amount - The number to upload. + */ + uploadStatic(children, startIndex, amount) + { + for (let i = 0; i < this.staticProperties.length; i++) + { + const property = this.staticProperties[i]; + + property.uploadFunction(children, startIndex, amount, + property.unsignedByte ? this.staticDataUint32 : this.staticData, + this.staticStride, property.offset); + } + + this.staticBuffer.upload(); + } + + /** + * Destroys the ParticleBuffer. + * + */ + destroy() + { + this.dynamicProperties = null; + this.dynamicBuffer.destroy(); + this.dynamicBuffer = null; + this.dynamicData = null; + this.dynamicDataUint32 = null; + + this.staticProperties = null; + this.staticBuffer.destroy(); + this.staticBuffer = null; + this.staticData = null; + this.staticDataUint32 = null; + } +} diff --git a/packages/particles/src/ParticleContainer.js b/packages/particles/src/ParticleContainer.js index c8ade96..9d25c5b 100644 --- a/packages/particles/src/ParticleContainer.js +++ b/packages/particles/src/ParticleContainer.js @@ -25,7 +25,7 @@ * * @class * @extends PIXI.Container - * @memberof PIXI.particles + * @memberof PIXI */ export default class ParticleContainer extends Container { diff --git a/packages/particles/src/ParticleRenderer.js b/packages/particles/src/ParticleRenderer.js new file mode 100644 index 0000000..5464211 --- /dev/null +++ b/packages/particles/src/ParticleRenderer.js @@ -0,0 +1,455 @@ +import { ObjectRenderer } from '@pixi/core'; +import { correctBlendMode, premultiplyRgba, premultiplyTint } from '@pixi/utils'; +import { Matrix } from '@pixi/math'; +import ParticleShader from './ParticleShader'; +import ParticleBuffer from './ParticleBuffer'; + +/** + * @author Mat Groves + * + * Big thanks to the very clever Matt DesLauriers https://github.com/mattdesl/ + * for creating the original PixiJS version! + * Also a thanks to https://github.com/bchevalier for tweaking the tint and alpha so that they now + * share 4 bytes on the vertex buffer + * + * Heavily inspired by LibGDX's ParticleRenderer: + * https://github.com/libgdx/libgdx/blob/master/gdx/src/com/badlogic/gdx/graphics/g2d/ParticleRenderer.java + */ + +/** + * + * @class + * @memberof PIXI + */ +export default class ParticleRenderer extends ObjectRenderer +{ + /** + * @param {PIXI.Renderer} renderer - The renderer this sprite batch works for. + */ + constructor(renderer) + { + super(renderer); + + // 65535 is max vertex index in the index buffer (see ParticleRenderer) + // so max number of particles is 65536 / 4 = 16384 + // and max number of element in the index buffer is 16384 * 6 = 98304 + // Creating a full index buffer, overhead is 98304 * 2 = 196Ko + // let numIndices = 98304; + + /** + * The default shader that is used if a sprite doesn't have a more specific one. + * + * @member {PIXI.Shader} + */ + this.shader = null; + + this.indexBuffer = null; + + this.properties = null; + + this.tempMatrix = new Matrix(); + + this.CONTEXT_UID = 0; + } + + /** + * When there is a WebGL context change + * + * @private + */ + onContextChange() + { + const gl = this.renderer.gl; + + this.CONTEXT_UID = this.renderer.CONTEXT_UID; + + // setup default shader + this.shader = new ParticleShader(gl); + + this.properties = [ + // verticesData + { + attribute: this.shader.attributes.aVertexPosition, + size: 2, + uploadFunction: this.uploadVertices, + offset: 0, + }, + // positionData + { + attribute: this.shader.attributes.aPositionCoord, + size: 2, + uploadFunction: this.uploadPosition, + offset: 0, + }, + // rotationData + { + attribute: this.shader.attributes.aRotation, + size: 1, + uploadFunction: this.uploadRotation, + offset: 0, + }, + // uvsData + { + attribute: this.shader.attributes.aTextureCoord, + size: 2, + uploadFunction: this.uploadUvs, + offset: 0, + }, + // tintData + { + attribute: this.shader.attributes.aColor, + size: 1, + unsignedByte: true, + uploadFunction: this.uploadTint, + offset: 0, + }, + ]; + } + + /** + * Starts a new particle batch. + * + */ + start() + { + this.renderer._bindGLShader(this.shader); + } + + /** + * Renders the particle container object. + * + * @param {PIXI.ParticleContainer} container - The container to render using this ParticleRenderer + */ + render(container) + { + const children = container.children; + const maxSize = container._maxSize; + const batchSize = container._batchSize; + const renderer = this.renderer; + let totalChildren = children.length; + + if (totalChildren === 0) + { + return; + } + else if (totalChildren > maxSize) + { + totalChildren = maxSize; + } + + let buffers = container._glBuffers[renderer.CONTEXT_UID]; + + if (!buffers) + { + buffers = container._glBuffers[renderer.CONTEXT_UID] = this.generateBuffers(container); + } + + const baseTexture = children[0]._texture.baseTexture; + + // if the uvs have not updated then no point rendering just yet! + this.renderer.setBlendMode(correctBlendMode(container.blendMode, baseTexture.premultiplyAlpha)); + + const gl = renderer.gl; + + const m = container.worldTransform.copy(this.tempMatrix); + + m.prepend(renderer._activeRenderTarget.projectionMatrix); + + this.shader.uniforms.projectionMatrix = m.toArray(true); + + this.shader.uniforms.uColor = premultiplyRgba(container.tintRgb, + container.worldAlpha, this.shader.uniforms.uColor, baseTexture.premultiplyAlpha); + + // make sure the texture is bound.. + this.shader.uniforms.uSampler = renderer.bindTexture(baseTexture); + + // now lets upload and render the buffers.. + for (let i = 0, j = 0; i < totalChildren; i += batchSize, j += 1) + { + let amount = (totalChildren - i); + + if (amount > batchSize) + { + amount = batchSize; + } + + if (j >= buffers.length) + { + if (!container.autoResize) + { + break; + } + buffers.push(this._generateOneMoreBuffer(container)); + } + + const buffer = buffers[j]; + + // we always upload the dynamic + buffer.uploadDynamic(children, i, amount); + + // we only upload the static content when we have to! + if (container._bufferToUpdate === j) + { + buffer.uploadStatic(children, i, amount); + container._bufferToUpdate = j + 1; + } + + // bind the buffer + renderer.bindVao(buffer.vao); + buffer.vao.draw(gl.TRIANGLES, amount * 6); + } + } + + /** + * Creates one particle buffer for each child in the container we want to render and updates internal properties + * + * @param {PIXI.ParticleContainer} container - The container to render using this ParticleRenderer + * @return {PIXI.ParticleBuffer[]} The buffers + */ + generateBuffers(container) + { + const gl = this.renderer.gl; + const buffers = []; + const size = container._maxSize; + const batchSize = container._batchSize; + const dynamicPropertyFlags = container._properties; + + for (let i = 0; i < size; i += batchSize) + { + buffers.push(new ParticleBuffer(gl, this.properties, dynamicPropertyFlags, batchSize)); + } + + return buffers; + } + + /** + * Creates one more particle buffer, because container has autoResize feature + * + * @param {PIXI.ParticleContainer} container - The container to render using this ParticleRenderer + * @return {PIXI.ParticleBuffer} generated buffer + * @private + */ + _generateOneMoreBuffer(container) + { + const gl = this.renderer.gl; + const batchSize = container._batchSize; + const dynamicPropertyFlags = container._properties; + + return new ParticleBuffer(gl, this.properties, dynamicPropertyFlags, batchSize); + } + + /** + * Uploads the verticies. + * + * @param {PIXI.DisplayObject[]} children - the array of display objects to render + * @param {number} startIndex - the index to start from in the children array + * @param {number} amount - the amount of children that will have their vertices uploaded + * @param {number[]} array - The vertices to upload. + * @param {number} stride - Stride to use for iteration. + * @param {number} offset - Offset to start at. + */ + uploadVertices(children, startIndex, amount, array, stride, offset) + { + let w0 = 0; + let w1 = 0; + let h0 = 0; + let h1 = 0; + + for (let i = 0; i < amount; ++i) + { + const sprite = children[startIndex + i]; + const texture = sprite._texture; + const sx = sprite.scale.x; + const sy = sprite.scale.y; + const trim = texture.trim; + const orig = texture.orig; + + if (trim) + { + // if the sprite is trimmed and is not a tilingsprite then we need to add the + // extra space before transforming the sprite coords.. + w1 = trim.x - (sprite.anchor.x * orig.width); + w0 = w1 + trim.width; + + h1 = trim.y - (sprite.anchor.y * orig.height); + h0 = h1 + trim.height; + } + else + { + w0 = (orig.width) * (1 - sprite.anchor.x); + w1 = (orig.width) * -sprite.anchor.x; + + h0 = orig.height * (1 - sprite.anchor.y); + h1 = orig.height * -sprite.anchor.y; + } + + array[offset] = w1 * sx; + array[offset + 1] = h1 * sy; + + array[offset + stride] = w0 * sx; + array[offset + stride + 1] = h1 * sy; + + array[offset + (stride * 2)] = w0 * sx; + array[offset + (stride * 2) + 1] = h0 * sy; + + array[offset + (stride * 3)] = w1 * sx; + array[offset + (stride * 3) + 1] = h0 * sy; + + offset += stride * 4; + } + } + + /** + * + * @param {PIXI.DisplayObject[]} children - the array of display objects to render + * @param {number} startIndex - the index to start from in the children array + * @param {number} amount - the amount of children that will have their positions uploaded + * @param {number[]} array - The vertices to upload. + * @param {number} stride - Stride to use for iteration. + * @param {number} offset - Offset to start at. + */ + uploadPosition(children, startIndex, amount, array, stride, offset) + { + for (let i = 0; i < amount; i++) + { + const spritePosition = children[startIndex + i].position; + + array[offset] = spritePosition.x; + array[offset + 1] = spritePosition.y; + + array[offset + stride] = spritePosition.x; + array[offset + stride + 1] = spritePosition.y; + + array[offset + (stride * 2)] = spritePosition.x; + array[offset + (stride * 2) + 1] = spritePosition.y; + + array[offset + (stride * 3)] = spritePosition.x; + array[offset + (stride * 3) + 1] = spritePosition.y; + + offset += stride * 4; + } + } + + /** + * + * @param {PIXI.DisplayObject[]} children - the array of display objects to render + * @param {number} startIndex - the index to start from in the children array + * @param {number} amount - the amount of children that will have their rotation uploaded + * @param {number[]} array - The vertices to upload. + * @param {number} stride - Stride to use for iteration. + * @param {number} offset - Offset to start at. + */ + uploadRotation(children, startIndex, amount, array, stride, offset) + { + for (let i = 0; i < amount; i++) + { + const spriteRotation = children[startIndex + i].rotation; + + array[offset] = spriteRotation; + array[offset + stride] = spriteRotation; + array[offset + (stride * 2)] = spriteRotation; + array[offset + (stride * 3)] = spriteRotation; + + offset += stride * 4; + } + } + + /** + * + * @param {PIXI.DisplayObject[]} children - the array of display objects to render + * @param {number} startIndex - the index to start from in the children array + * @param {number} amount - the amount of children that will have their rotation uploaded + * @param {number[]} array - The vertices to upload. + * @param {number} stride - Stride to use for iteration. + * @param {number} offset - Offset to start at. + */ + uploadUvs(children, startIndex, amount, array, stride, offset) + { + for (let i = 0; i < amount; ++i) + { + const textureUvs = children[startIndex + i]._texture._uvs; + + if (textureUvs) + { + array[offset] = textureUvs.x0; + array[offset + 1] = textureUvs.y0; + + array[offset + stride] = textureUvs.x1; + array[offset + stride + 1] = textureUvs.y1; + + array[offset + (stride * 2)] = textureUvs.x2; + array[offset + (stride * 2) + 1] = textureUvs.y2; + + array[offset + (stride * 3)] = textureUvs.x3; + array[offset + (stride * 3) + 1] = textureUvs.y3; + + offset += stride * 4; + } + else + { + // TODO you know this can be easier! + array[offset] = 0; + array[offset + 1] = 0; + + array[offset + stride] = 0; + array[offset + stride + 1] = 0; + + array[offset + (stride * 2)] = 0; + array[offset + (stride * 2) + 1] = 0; + + array[offset + (stride * 3)] = 0; + array[offset + (stride * 3) + 1] = 0; + + offset += stride * 4; + } + } + } + + /** + * + * @param {PIXI.DisplayObject[]} children - the array of display objects to render + * @param {number} startIndex - the index to start from in the children array + * @param {number} amount - the amount of children that will have their rotation uploaded + * @param {number[]} array - The vertices to upload. + * @param {number} stride - Stride to use for iteration. + * @param {number} offset - Offset to start at. + */ + uploadTint(children, startIndex, amount, array, stride, offset) + { + for (let i = 0; i < amount; ++i) + { + const sprite = children[startIndex + i]; + const premultiplied = sprite._texture.baseTexture.premultiplyAlpha; + const alpha = sprite.alpha; + // we dont call extra function if alpha is 1.0, that's faster + const argb = alpha < 1.0 && premultiplied ? premultiplyTint(sprite._tintRGB, alpha) + : sprite._tintRGB + (alpha * 255 << 24); + + array[offset] = argb; + array[offset + stride] = argb; + array[offset + (stride * 2)] = argb; + array[offset + (stride * 3)] = argb; + + offset += stride * 4; + } + } + + /** + * Destroys the ParticleRenderer. + * + */ + destroy() + { + if (this.renderer.gl) + { + this.renderer.gl.deleteBuffer(this.indexBuffer); + } + + super.destroy(); + + this.shader.destroy(); + + this.indices = null; + this.tempMatrix = null; + } +} diff --git a/packages/particles/src/ParticleShader.js b/packages/particles/src/ParticleShader.js new file mode 100644 index 0000000..069afe6 --- /dev/null +++ b/packages/particles/src/ParticleShader.js @@ -0,0 +1,21 @@ +import { GLShader } from 'pixi-gl-core'; +import { PRECISION } from '@pixi/constants'; +import vertex from './particles.vert'; +import fragment from './particles.frag'; + +/** + * @class + * @extends PIXI.Shader + * @memberof PIXI + * @private + */ +export default class ParticleShader extends GLShader +{ + /** + * @param {PIXI.Shader} gl - The webgl shader manager this shader works for. + */ + constructor(gl) + { + super(gl, vertex, fragment, PRECISION.DEFAULT); + } +} diff --git a/packages/particles/src/index.js b/packages/particles/src/index.js index 629d599..d315119 100644 --- a/packages/particles/src/index.js +++ b/packages/particles/src/index.js @@ -1,5 +1,2 @@ -/** - * @namespace PIXI.particles - */ export { default as ParticleContainer } from './ParticleContainer'; -export { default as ParticleRenderer } from './webgl/ParticleRenderer'; +export { default as ParticleRenderer } from './ParticleRenderer'; diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index e59c66a..a390a1a 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -332,6 +332,43 @@ /* * This namespace has been removed and items have been moved to * the top-level `PIXI` object. + * @namespace PIXI.particles + * @deprecated since 5.0.0 + */ + PIXI.particles = {}; + + Object.defineProperties(PIXI.particles, { + /** + * @class PIXI.particles.ParticleContainer + * @deprecated since 5.0.0 + * @see PIXI.ParticleContainer + */ + ParticleContainer: { + get() + { + warn('PIXI.particles.ParticleContainer has moved to PIXI.ParticleContainer'); + + return PIXI.ParticleContainer; + }, + }, + /** + * @class PIXI.particles.ParticleRenderer + * @deprecated since 5.0.0 + * @see PIXI.ParticleRenderer + */ + ParticleRenderer: { + get() + { + warn('PIXI.particles.ParticleRenderer has moved to PIXI.ParticleRenderer'); + + return PIXI.ParticleRenderer; + }, + }, + }); + + /* + * This namespace has been removed and items have been moved to + * the top-level `PIXI` object. * @namespace PIXI.ticker * @deprecated since 5.0.0 */ diff --git a/bundles/pixi.js/src/index.js b/bundles/pixi.js/src/index.js index 52d7cb4..fb839a0 100644 --- a/bundles/pixi.js/src/index.js +++ b/bundles/pixi.js/src/index.js @@ -9,6 +9,7 @@ import * as loaders from '@pixi/loaders'; import * as math from '@pixi/math'; import * as mesh from '@pixi/mesh'; +import * as particles from '@pixi/particles'; import * as prepare from '@pixi/prepare'; import * as sprite from '@pixi/sprite'; import * as spriteAnimated from '@pixi/sprite-animated'; @@ -37,6 +38,7 @@ core.Renderer.registerPlugin('graphics', graphics.GraphicsRenderer); core.Renderer.registerPlugin('interaction', interaction.InteractionManager); core.Renderer.registerPlugin('mesh', mesh.MeshRenderer); +core.Renderer.registerPlugin('particle', particles.ParticleRenderer); core.Renderer.registerPlugin('prepare', prepare.Prepare); core.Renderer.registerPlugin('sprite', sprite.SpriteRenderer); core.Renderer.registerPlugin('tilingSprite', spriteTiling.TilingSpriteRenderer); @@ -118,6 +120,7 @@ loaders, math, mesh, + particles, sprite, spriteAnimated, spritesheet, @@ -141,6 +144,7 @@ export * from '@pixi/loaders'; export * from '@pixi/math'; export * from '@pixi/mesh'; +export * from '@pixi/particles'; export * from '@pixi/sprite'; export * from '@pixi/spritesheet'; export * from '@pixi/sprite-animated'; diff --git a/packages/particles/README.md b/packages/particles/README.md index bca3a96..dca806d 100644 --- a/packages/particles/README.md +++ b/packages/particles/README.md @@ -9,5 +9,8 @@ ## Usage ```js -import '@pixi/particles'; +import {ParticleRenderer} from '@pixi/particles'; +import {Renderer} from '@pixi/core'; + +Renderer.registerPlugin('particle', ParticleRenderer); ``` \ No newline at end of file diff --git a/packages/particles/src/ParticleBuffer.js b/packages/particles/src/ParticleBuffer.js new file mode 100644 index 0000000..6306bda --- /dev/null +++ b/packages/particles/src/ParticleBuffer.js @@ -0,0 +1,268 @@ +// import { VertexArrayObject } from 'pixi-gl-core'; +import { createIndicesForQuads } from '@pixi/utils'; +import { GLBuffer } from 'pixi-gl-core'; + +/** + * @author Mat Groves + * + * Big thanks to the very clever Matt DesLauriers https://github.com/mattdesl/ + * for creating the original PixiJS version! + * Also a thanks to https://github.com/bchevalier for tweaking the tint and alpha so that + * they now share 4 bytes on the vertex buffer + * + * Heavily inspired by LibGDX's ParticleBuffer: + * https://github.com/libgdx/libgdx/blob/master/gdx/src/com/badlogic/gdx/graphics/g2d/ParticleBuffer.java + */ + +/** + * The particle buffer manages the static and dynamic buffers for a particle container. + * + * @class + * @private + * @memberof PIXI + */ +export default class ParticleBuffer +{ + /** + * @param {WebGLRenderingContext} gl - The rendering context. + * @param {object} properties - The properties to upload. + * @param {boolean[]} dynamicPropertyFlags - Flags for which properties are dynamic. + * @param {number} size - The size of the batch. + */ + constructor(gl, properties, dynamicPropertyFlags, size) + { + /** + * The current WebGL drawing context. + * + * @member {WebGLRenderingContext} + */ + this.gl = gl; + + /** + * The number of particles the buffer can hold + * + * @member {number} + */ + this.size = size; + + /** + * A list of the properties that are dynamic. + * + * @member {object[]} + */ + this.dynamicProperties = []; + + /** + * A list of the properties that are static. + * + * @member {object[]} + */ + this.staticProperties = []; + + for (let i = 0; i < properties.length; ++i) + { + let property = properties[i]; + + // Make copy of properties object so that when we edit the offset it doesn't + // change all other instances of the object literal + property = { + attribute: property.attribute, + size: property.size, + uploadFunction: property.uploadFunction, + unsignedByte: property.unsignedByte, + offset: property.offset, + }; + + if (dynamicPropertyFlags[i]) + { + this.dynamicProperties.push(property); + } + else + { + this.staticProperties.push(property); + } + } + + this.staticStride = 0; + this.staticBuffer = null; + this.staticData = null; + this.staticDataUint32 = null; + + this.dynamicStride = 0; + this.dynamicBuffer = null; + this.dynamicData = null; + this.dynamicDataUint32 = null; + + this.initBuffers(); + } + + /** + * Sets up the renderer context and necessary buffers. + * + * @private + */ + initBuffers() + { + const gl = this.gl; + let dynamicOffset = 0; + + /** + * Holds the indices of the geometry (quads) to draw + * + * @member {Uint16Array} + */ + this.indices = createIndicesForQuads(this.size); + this.indexBuffer = GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); + + this.dynamicStride = 0; + + for (let i = 0; i < this.dynamicProperties.length; ++i) + { + const property = this.dynamicProperties[i]; + + property.offset = dynamicOffset; + dynamicOffset += property.size; + this.dynamicStride += property.size; + } + + this.dynamicData = new Float32Array(this.size * this.dynamicStride * 4); + this.dynamicBuffer = GLBuffer.createVertexBuffer(gl, this.dynamicData, gl.STREAM_DRAW); + + // static // + let staticOffset = 0; + + this.staticStride = 0; + + for (let i = 0; i < this.staticProperties.length; ++i) + { + const property = this.staticProperties[i]; + + property.offset = staticOffset; + staticOffset += property.size; + this.staticStride += property.size; + } + + this.staticData = new Float32Array(this.size * this.staticStride * 4); + this.staticBuffer = GLBuffer.createVertexBuffer(gl, this.staticData, gl.STATIC_DRAW); + + // this.vao = new VertexArrayObject(gl) + // .addIndex(this.indexBuffer); + + for (let i = 0; i < this.dynamicProperties.length; ++i) + { + const property = this.dynamicProperties[i]; + + if (property.unsignedByte) + { + this.vao.addAttribute( + this.dynamicBuffer, + property.attribute, + gl.UNSIGNED_BYTE, + true, + this.dynamicStride * 4, + property.offset * 4 + ); + } + else + { + this.vao.addAttribute( + this.dynamicBuffer, + property.attribute, + gl.FLOAT, + false, + this.dynamicStride * 4, + property.offset * 4 + ); + } + } + + for (let i = 0; i < this.staticProperties.length; ++i) + { + const property = this.staticProperties[i]; + + if (property.unsignedByte) + { + this.vao.addAttribute( + this.staticBuffer, + property.attribute, + gl.UNSIGNED_BYTE, + true, + this.staticStride * 4, + property.offset * 4 + ); + } + else + { + this.vao.addAttribute( + this.staticBuffer, + property.attribute, + gl.FLOAT, + false, + this.staticStride * 4, + property.offset * 4 + ); + } + } + } + + /** + * Uploads the dynamic properties. + * + * @param {PIXI.DisplayObject[]} children - The children to upload. + * @param {number} startIndex - The index to start at. + * @param {number} amount - The number to upload. + */ + uploadDynamic(children, startIndex, amount) + { + for (let i = 0; i < this.dynamicProperties.length; i++) + { + const property = this.dynamicProperties[i]; + + property.uploadFunction(children, startIndex, amount, + property.unsignedByte ? this.dynamicDataUint32 : this.dynamicData, + this.dynamicStride, property.offset); + } + + this.dynamicBuffer.upload(); + } + + /** + * Uploads the static properties. + * + * @param {PIXI.DisplayObject[]} children - The children to upload. + * @param {number} startIndex - The index to start at. + * @param {number} amount - The number to upload. + */ + uploadStatic(children, startIndex, amount) + { + for (let i = 0; i < this.staticProperties.length; i++) + { + const property = this.staticProperties[i]; + + property.uploadFunction(children, startIndex, amount, + property.unsignedByte ? this.staticDataUint32 : this.staticData, + this.staticStride, property.offset); + } + + this.staticBuffer.upload(); + } + + /** + * Destroys the ParticleBuffer. + * + */ + destroy() + { + this.dynamicProperties = null; + this.dynamicBuffer.destroy(); + this.dynamicBuffer = null; + this.dynamicData = null; + this.dynamicDataUint32 = null; + + this.staticProperties = null; + this.staticBuffer.destroy(); + this.staticBuffer = null; + this.staticData = null; + this.staticDataUint32 = null; + } +} diff --git a/packages/particles/src/ParticleContainer.js b/packages/particles/src/ParticleContainer.js index c8ade96..9d25c5b 100644 --- a/packages/particles/src/ParticleContainer.js +++ b/packages/particles/src/ParticleContainer.js @@ -25,7 +25,7 @@ * * @class * @extends PIXI.Container - * @memberof PIXI.particles + * @memberof PIXI */ export default class ParticleContainer extends Container { diff --git a/packages/particles/src/ParticleRenderer.js b/packages/particles/src/ParticleRenderer.js new file mode 100644 index 0000000..5464211 --- /dev/null +++ b/packages/particles/src/ParticleRenderer.js @@ -0,0 +1,455 @@ +import { ObjectRenderer } from '@pixi/core'; +import { correctBlendMode, premultiplyRgba, premultiplyTint } from '@pixi/utils'; +import { Matrix } from '@pixi/math'; +import ParticleShader from './ParticleShader'; +import ParticleBuffer from './ParticleBuffer'; + +/** + * @author Mat Groves + * + * Big thanks to the very clever Matt DesLauriers https://github.com/mattdesl/ + * for creating the original PixiJS version! + * Also a thanks to https://github.com/bchevalier for tweaking the tint and alpha so that they now + * share 4 bytes on the vertex buffer + * + * Heavily inspired by LibGDX's ParticleRenderer: + * https://github.com/libgdx/libgdx/blob/master/gdx/src/com/badlogic/gdx/graphics/g2d/ParticleRenderer.java + */ + +/** + * + * @class + * @memberof PIXI + */ +export default class ParticleRenderer extends ObjectRenderer +{ + /** + * @param {PIXI.Renderer} renderer - The renderer this sprite batch works for. + */ + constructor(renderer) + { + super(renderer); + + // 65535 is max vertex index in the index buffer (see ParticleRenderer) + // so max number of particles is 65536 / 4 = 16384 + // and max number of element in the index buffer is 16384 * 6 = 98304 + // Creating a full index buffer, overhead is 98304 * 2 = 196Ko + // let numIndices = 98304; + + /** + * The default shader that is used if a sprite doesn't have a more specific one. + * + * @member {PIXI.Shader} + */ + this.shader = null; + + this.indexBuffer = null; + + this.properties = null; + + this.tempMatrix = new Matrix(); + + this.CONTEXT_UID = 0; + } + + /** + * When there is a WebGL context change + * + * @private + */ + onContextChange() + { + const gl = this.renderer.gl; + + this.CONTEXT_UID = this.renderer.CONTEXT_UID; + + // setup default shader + this.shader = new ParticleShader(gl); + + this.properties = [ + // verticesData + { + attribute: this.shader.attributes.aVertexPosition, + size: 2, + uploadFunction: this.uploadVertices, + offset: 0, + }, + // positionData + { + attribute: this.shader.attributes.aPositionCoord, + size: 2, + uploadFunction: this.uploadPosition, + offset: 0, + }, + // rotationData + { + attribute: this.shader.attributes.aRotation, + size: 1, + uploadFunction: this.uploadRotation, + offset: 0, + }, + // uvsData + { + attribute: this.shader.attributes.aTextureCoord, + size: 2, + uploadFunction: this.uploadUvs, + offset: 0, + }, + // tintData + { + attribute: this.shader.attributes.aColor, + size: 1, + unsignedByte: true, + uploadFunction: this.uploadTint, + offset: 0, + }, + ]; + } + + /** + * Starts a new particle batch. + * + */ + start() + { + this.renderer._bindGLShader(this.shader); + } + + /** + * Renders the particle container object. + * + * @param {PIXI.ParticleContainer} container - The container to render using this ParticleRenderer + */ + render(container) + { + const children = container.children; + const maxSize = container._maxSize; + const batchSize = container._batchSize; + const renderer = this.renderer; + let totalChildren = children.length; + + if (totalChildren === 0) + { + return; + } + else if (totalChildren > maxSize) + { + totalChildren = maxSize; + } + + let buffers = container._glBuffers[renderer.CONTEXT_UID]; + + if (!buffers) + { + buffers = container._glBuffers[renderer.CONTEXT_UID] = this.generateBuffers(container); + } + + const baseTexture = children[0]._texture.baseTexture; + + // if the uvs have not updated then no point rendering just yet! + this.renderer.setBlendMode(correctBlendMode(container.blendMode, baseTexture.premultiplyAlpha)); + + const gl = renderer.gl; + + const m = container.worldTransform.copy(this.tempMatrix); + + m.prepend(renderer._activeRenderTarget.projectionMatrix); + + this.shader.uniforms.projectionMatrix = m.toArray(true); + + this.shader.uniforms.uColor = premultiplyRgba(container.tintRgb, + container.worldAlpha, this.shader.uniforms.uColor, baseTexture.premultiplyAlpha); + + // make sure the texture is bound.. + this.shader.uniforms.uSampler = renderer.bindTexture(baseTexture); + + // now lets upload and render the buffers.. + for (let i = 0, j = 0; i < totalChildren; i += batchSize, j += 1) + { + let amount = (totalChildren - i); + + if (amount > batchSize) + { + amount = batchSize; + } + + if (j >= buffers.length) + { + if (!container.autoResize) + { + break; + } + buffers.push(this._generateOneMoreBuffer(container)); + } + + const buffer = buffers[j]; + + // we always upload the dynamic + buffer.uploadDynamic(children, i, amount); + + // we only upload the static content when we have to! + if (container._bufferToUpdate === j) + { + buffer.uploadStatic(children, i, amount); + container._bufferToUpdate = j + 1; + } + + // bind the buffer + renderer.bindVao(buffer.vao); + buffer.vao.draw(gl.TRIANGLES, amount * 6); + } + } + + /** + * Creates one particle buffer for each child in the container we want to render and updates internal properties + * + * @param {PIXI.ParticleContainer} container - The container to render using this ParticleRenderer + * @return {PIXI.ParticleBuffer[]} The buffers + */ + generateBuffers(container) + { + const gl = this.renderer.gl; + const buffers = []; + const size = container._maxSize; + const batchSize = container._batchSize; + const dynamicPropertyFlags = container._properties; + + for (let i = 0; i < size; i += batchSize) + { + buffers.push(new ParticleBuffer(gl, this.properties, dynamicPropertyFlags, batchSize)); + } + + return buffers; + } + + /** + * Creates one more particle buffer, because container has autoResize feature + * + * @param {PIXI.ParticleContainer} container - The container to render using this ParticleRenderer + * @return {PIXI.ParticleBuffer} generated buffer + * @private + */ + _generateOneMoreBuffer(container) + { + const gl = this.renderer.gl; + const batchSize = container._batchSize; + const dynamicPropertyFlags = container._properties; + + return new ParticleBuffer(gl, this.properties, dynamicPropertyFlags, batchSize); + } + + /** + * Uploads the verticies. + * + * @param {PIXI.DisplayObject[]} children - the array of display objects to render + * @param {number} startIndex - the index to start from in the children array + * @param {number} amount - the amount of children that will have their vertices uploaded + * @param {number[]} array - The vertices to upload. + * @param {number} stride - Stride to use for iteration. + * @param {number} offset - Offset to start at. + */ + uploadVertices(children, startIndex, amount, array, stride, offset) + { + let w0 = 0; + let w1 = 0; + let h0 = 0; + let h1 = 0; + + for (let i = 0; i < amount; ++i) + { + const sprite = children[startIndex + i]; + const texture = sprite._texture; + const sx = sprite.scale.x; + const sy = sprite.scale.y; + const trim = texture.trim; + const orig = texture.orig; + + if (trim) + { + // if the sprite is trimmed and is not a tilingsprite then we need to add the + // extra space before transforming the sprite coords.. + w1 = trim.x - (sprite.anchor.x * orig.width); + w0 = w1 + trim.width; + + h1 = trim.y - (sprite.anchor.y * orig.height); + h0 = h1 + trim.height; + } + else + { + w0 = (orig.width) * (1 - sprite.anchor.x); + w1 = (orig.width) * -sprite.anchor.x; + + h0 = orig.height * (1 - sprite.anchor.y); + h1 = orig.height * -sprite.anchor.y; + } + + array[offset] = w1 * sx; + array[offset + 1] = h1 * sy; + + array[offset + stride] = w0 * sx; + array[offset + stride + 1] = h1 * sy; + + array[offset + (stride * 2)] = w0 * sx; + array[offset + (stride * 2) + 1] = h0 * sy; + + array[offset + (stride * 3)] = w1 * sx; + array[offset + (stride * 3) + 1] = h0 * sy; + + offset += stride * 4; + } + } + + /** + * + * @param {PIXI.DisplayObject[]} children - the array of display objects to render + * @param {number} startIndex - the index to start from in the children array + * @param {number} amount - the amount of children that will have their positions uploaded + * @param {number[]} array - The vertices to upload. + * @param {number} stride - Stride to use for iteration. + * @param {number} offset - Offset to start at. + */ + uploadPosition(children, startIndex, amount, array, stride, offset) + { + for (let i = 0; i < amount; i++) + { + const spritePosition = children[startIndex + i].position; + + array[offset] = spritePosition.x; + array[offset + 1] = spritePosition.y; + + array[offset + stride] = spritePosition.x; + array[offset + stride + 1] = spritePosition.y; + + array[offset + (stride * 2)] = spritePosition.x; + array[offset + (stride * 2) + 1] = spritePosition.y; + + array[offset + (stride * 3)] = spritePosition.x; + array[offset + (stride * 3) + 1] = spritePosition.y; + + offset += stride * 4; + } + } + + /** + * + * @param {PIXI.DisplayObject[]} children - the array of display objects to render + * @param {number} startIndex - the index to start from in the children array + * @param {number} amount - the amount of children that will have their rotation uploaded + * @param {number[]} array - The vertices to upload. + * @param {number} stride - Stride to use for iteration. + * @param {number} offset - Offset to start at. + */ + uploadRotation(children, startIndex, amount, array, stride, offset) + { + for (let i = 0; i < amount; i++) + { + const spriteRotation = children[startIndex + i].rotation; + + array[offset] = spriteRotation; + array[offset + stride] = spriteRotation; + array[offset + (stride * 2)] = spriteRotation; + array[offset + (stride * 3)] = spriteRotation; + + offset += stride * 4; + } + } + + /** + * + * @param {PIXI.DisplayObject[]} children - the array of display objects to render + * @param {number} startIndex - the index to start from in the children array + * @param {number} amount - the amount of children that will have their rotation uploaded + * @param {number[]} array - The vertices to upload. + * @param {number} stride - Stride to use for iteration. + * @param {number} offset - Offset to start at. + */ + uploadUvs(children, startIndex, amount, array, stride, offset) + { + for (let i = 0; i < amount; ++i) + { + const textureUvs = children[startIndex + i]._texture._uvs; + + if (textureUvs) + { + array[offset] = textureUvs.x0; + array[offset + 1] = textureUvs.y0; + + array[offset + stride] = textureUvs.x1; + array[offset + stride + 1] = textureUvs.y1; + + array[offset + (stride * 2)] = textureUvs.x2; + array[offset + (stride * 2) + 1] = textureUvs.y2; + + array[offset + (stride * 3)] = textureUvs.x3; + array[offset + (stride * 3) + 1] = textureUvs.y3; + + offset += stride * 4; + } + else + { + // TODO you know this can be easier! + array[offset] = 0; + array[offset + 1] = 0; + + array[offset + stride] = 0; + array[offset + stride + 1] = 0; + + array[offset + (stride * 2)] = 0; + array[offset + (stride * 2) + 1] = 0; + + array[offset + (stride * 3)] = 0; + array[offset + (stride * 3) + 1] = 0; + + offset += stride * 4; + } + } + } + + /** + * + * @param {PIXI.DisplayObject[]} children - the array of display objects to render + * @param {number} startIndex - the index to start from in the children array + * @param {number} amount - the amount of children that will have their rotation uploaded + * @param {number[]} array - The vertices to upload. + * @param {number} stride - Stride to use for iteration. + * @param {number} offset - Offset to start at. + */ + uploadTint(children, startIndex, amount, array, stride, offset) + { + for (let i = 0; i < amount; ++i) + { + const sprite = children[startIndex + i]; + const premultiplied = sprite._texture.baseTexture.premultiplyAlpha; + const alpha = sprite.alpha; + // we dont call extra function if alpha is 1.0, that's faster + const argb = alpha < 1.0 && premultiplied ? premultiplyTint(sprite._tintRGB, alpha) + : sprite._tintRGB + (alpha * 255 << 24); + + array[offset] = argb; + array[offset + stride] = argb; + array[offset + (stride * 2)] = argb; + array[offset + (stride * 3)] = argb; + + offset += stride * 4; + } + } + + /** + * Destroys the ParticleRenderer. + * + */ + destroy() + { + if (this.renderer.gl) + { + this.renderer.gl.deleteBuffer(this.indexBuffer); + } + + super.destroy(); + + this.shader.destroy(); + + this.indices = null; + this.tempMatrix = null; + } +} diff --git a/packages/particles/src/ParticleShader.js b/packages/particles/src/ParticleShader.js new file mode 100644 index 0000000..069afe6 --- /dev/null +++ b/packages/particles/src/ParticleShader.js @@ -0,0 +1,21 @@ +import { GLShader } from 'pixi-gl-core'; +import { PRECISION } from '@pixi/constants'; +import vertex from './particles.vert'; +import fragment from './particles.frag'; + +/** + * @class + * @extends PIXI.Shader + * @memberof PIXI + * @private + */ +export default class ParticleShader extends GLShader +{ + /** + * @param {PIXI.Shader} gl - The webgl shader manager this shader works for. + */ + constructor(gl) + { + super(gl, vertex, fragment, PRECISION.DEFAULT); + } +} diff --git a/packages/particles/src/index.js b/packages/particles/src/index.js index 629d599..d315119 100644 --- a/packages/particles/src/index.js +++ b/packages/particles/src/index.js @@ -1,5 +1,2 @@ -/** - * @namespace PIXI.particles - */ export { default as ParticleContainer } from './ParticleContainer'; -export { default as ParticleRenderer } from './webgl/ParticleRenderer'; +export { default as ParticleRenderer } from './ParticleRenderer'; diff --git a/packages/particles/src/particles.frag b/packages/particles/src/particles.frag new file mode 100644 index 0000000..a725046 --- /dev/null +++ b/packages/particles/src/particles.frag @@ -0,0 +1,9 @@ +varying vec2 vTextureCoord; +varying vec4 vColor; + +uniform sampler2D uSampler; + +void main(void){ + vec4 color = texture2D(uSampler, vTextureCoord) * vColor; + gl_FragColor = color; +} \ No newline at end of file diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index e59c66a..a390a1a 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -332,6 +332,43 @@ /* * This namespace has been removed and items have been moved to * the top-level `PIXI` object. + * @namespace PIXI.particles + * @deprecated since 5.0.0 + */ + PIXI.particles = {}; + + Object.defineProperties(PIXI.particles, { + /** + * @class PIXI.particles.ParticleContainer + * @deprecated since 5.0.0 + * @see PIXI.ParticleContainer + */ + ParticleContainer: { + get() + { + warn('PIXI.particles.ParticleContainer has moved to PIXI.ParticleContainer'); + + return PIXI.ParticleContainer; + }, + }, + /** + * @class PIXI.particles.ParticleRenderer + * @deprecated since 5.0.0 + * @see PIXI.ParticleRenderer + */ + ParticleRenderer: { + get() + { + warn('PIXI.particles.ParticleRenderer has moved to PIXI.ParticleRenderer'); + + return PIXI.ParticleRenderer; + }, + }, + }); + + /* + * This namespace has been removed and items have been moved to + * the top-level `PIXI` object. * @namespace PIXI.ticker * @deprecated since 5.0.0 */ diff --git a/bundles/pixi.js/src/index.js b/bundles/pixi.js/src/index.js index 52d7cb4..fb839a0 100644 --- a/bundles/pixi.js/src/index.js +++ b/bundles/pixi.js/src/index.js @@ -9,6 +9,7 @@ import * as loaders from '@pixi/loaders'; import * as math from '@pixi/math'; import * as mesh from '@pixi/mesh'; +import * as particles from '@pixi/particles'; import * as prepare from '@pixi/prepare'; import * as sprite from '@pixi/sprite'; import * as spriteAnimated from '@pixi/sprite-animated'; @@ -37,6 +38,7 @@ core.Renderer.registerPlugin('graphics', graphics.GraphicsRenderer); core.Renderer.registerPlugin('interaction', interaction.InteractionManager); core.Renderer.registerPlugin('mesh', mesh.MeshRenderer); +core.Renderer.registerPlugin('particle', particles.ParticleRenderer); core.Renderer.registerPlugin('prepare', prepare.Prepare); core.Renderer.registerPlugin('sprite', sprite.SpriteRenderer); core.Renderer.registerPlugin('tilingSprite', spriteTiling.TilingSpriteRenderer); @@ -118,6 +120,7 @@ loaders, math, mesh, + particles, sprite, spriteAnimated, spritesheet, @@ -141,6 +144,7 @@ export * from '@pixi/loaders'; export * from '@pixi/math'; export * from '@pixi/mesh'; +export * from '@pixi/particles'; export * from '@pixi/sprite'; export * from '@pixi/spritesheet'; export * from '@pixi/sprite-animated'; diff --git a/packages/particles/README.md b/packages/particles/README.md index bca3a96..dca806d 100644 --- a/packages/particles/README.md +++ b/packages/particles/README.md @@ -9,5 +9,8 @@ ## Usage ```js -import '@pixi/particles'; +import {ParticleRenderer} from '@pixi/particles'; +import {Renderer} from '@pixi/core'; + +Renderer.registerPlugin('particle', ParticleRenderer); ``` \ No newline at end of file diff --git a/packages/particles/src/ParticleBuffer.js b/packages/particles/src/ParticleBuffer.js new file mode 100644 index 0000000..6306bda --- /dev/null +++ b/packages/particles/src/ParticleBuffer.js @@ -0,0 +1,268 @@ +// import { VertexArrayObject } from 'pixi-gl-core'; +import { createIndicesForQuads } from '@pixi/utils'; +import { GLBuffer } from 'pixi-gl-core'; + +/** + * @author Mat Groves + * + * Big thanks to the very clever Matt DesLauriers https://github.com/mattdesl/ + * for creating the original PixiJS version! + * Also a thanks to https://github.com/bchevalier for tweaking the tint and alpha so that + * they now share 4 bytes on the vertex buffer + * + * Heavily inspired by LibGDX's ParticleBuffer: + * https://github.com/libgdx/libgdx/blob/master/gdx/src/com/badlogic/gdx/graphics/g2d/ParticleBuffer.java + */ + +/** + * The particle buffer manages the static and dynamic buffers for a particle container. + * + * @class + * @private + * @memberof PIXI + */ +export default class ParticleBuffer +{ + /** + * @param {WebGLRenderingContext} gl - The rendering context. + * @param {object} properties - The properties to upload. + * @param {boolean[]} dynamicPropertyFlags - Flags for which properties are dynamic. + * @param {number} size - The size of the batch. + */ + constructor(gl, properties, dynamicPropertyFlags, size) + { + /** + * The current WebGL drawing context. + * + * @member {WebGLRenderingContext} + */ + this.gl = gl; + + /** + * The number of particles the buffer can hold + * + * @member {number} + */ + this.size = size; + + /** + * A list of the properties that are dynamic. + * + * @member {object[]} + */ + this.dynamicProperties = []; + + /** + * A list of the properties that are static. + * + * @member {object[]} + */ + this.staticProperties = []; + + for (let i = 0; i < properties.length; ++i) + { + let property = properties[i]; + + // Make copy of properties object so that when we edit the offset it doesn't + // change all other instances of the object literal + property = { + attribute: property.attribute, + size: property.size, + uploadFunction: property.uploadFunction, + unsignedByte: property.unsignedByte, + offset: property.offset, + }; + + if (dynamicPropertyFlags[i]) + { + this.dynamicProperties.push(property); + } + else + { + this.staticProperties.push(property); + } + } + + this.staticStride = 0; + this.staticBuffer = null; + this.staticData = null; + this.staticDataUint32 = null; + + this.dynamicStride = 0; + this.dynamicBuffer = null; + this.dynamicData = null; + this.dynamicDataUint32 = null; + + this.initBuffers(); + } + + /** + * Sets up the renderer context and necessary buffers. + * + * @private + */ + initBuffers() + { + const gl = this.gl; + let dynamicOffset = 0; + + /** + * Holds the indices of the geometry (quads) to draw + * + * @member {Uint16Array} + */ + this.indices = createIndicesForQuads(this.size); + this.indexBuffer = GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); + + this.dynamicStride = 0; + + for (let i = 0; i < this.dynamicProperties.length; ++i) + { + const property = this.dynamicProperties[i]; + + property.offset = dynamicOffset; + dynamicOffset += property.size; + this.dynamicStride += property.size; + } + + this.dynamicData = new Float32Array(this.size * this.dynamicStride * 4); + this.dynamicBuffer = GLBuffer.createVertexBuffer(gl, this.dynamicData, gl.STREAM_DRAW); + + // static // + let staticOffset = 0; + + this.staticStride = 0; + + for (let i = 0; i < this.staticProperties.length; ++i) + { + const property = this.staticProperties[i]; + + property.offset = staticOffset; + staticOffset += property.size; + this.staticStride += property.size; + } + + this.staticData = new Float32Array(this.size * this.staticStride * 4); + this.staticBuffer = GLBuffer.createVertexBuffer(gl, this.staticData, gl.STATIC_DRAW); + + // this.vao = new VertexArrayObject(gl) + // .addIndex(this.indexBuffer); + + for (let i = 0; i < this.dynamicProperties.length; ++i) + { + const property = this.dynamicProperties[i]; + + if (property.unsignedByte) + { + this.vao.addAttribute( + this.dynamicBuffer, + property.attribute, + gl.UNSIGNED_BYTE, + true, + this.dynamicStride * 4, + property.offset * 4 + ); + } + else + { + this.vao.addAttribute( + this.dynamicBuffer, + property.attribute, + gl.FLOAT, + false, + this.dynamicStride * 4, + property.offset * 4 + ); + } + } + + for (let i = 0; i < this.staticProperties.length; ++i) + { + const property = this.staticProperties[i]; + + if (property.unsignedByte) + { + this.vao.addAttribute( + this.staticBuffer, + property.attribute, + gl.UNSIGNED_BYTE, + true, + this.staticStride * 4, + property.offset * 4 + ); + } + else + { + this.vao.addAttribute( + this.staticBuffer, + property.attribute, + gl.FLOAT, + false, + this.staticStride * 4, + property.offset * 4 + ); + } + } + } + + /** + * Uploads the dynamic properties. + * + * @param {PIXI.DisplayObject[]} children - The children to upload. + * @param {number} startIndex - The index to start at. + * @param {number} amount - The number to upload. + */ + uploadDynamic(children, startIndex, amount) + { + for (let i = 0; i < this.dynamicProperties.length; i++) + { + const property = this.dynamicProperties[i]; + + property.uploadFunction(children, startIndex, amount, + property.unsignedByte ? this.dynamicDataUint32 : this.dynamicData, + this.dynamicStride, property.offset); + } + + this.dynamicBuffer.upload(); + } + + /** + * Uploads the static properties. + * + * @param {PIXI.DisplayObject[]} children - The children to upload. + * @param {number} startIndex - The index to start at. + * @param {number} amount - The number to upload. + */ + uploadStatic(children, startIndex, amount) + { + for (let i = 0; i < this.staticProperties.length; i++) + { + const property = this.staticProperties[i]; + + property.uploadFunction(children, startIndex, amount, + property.unsignedByte ? this.staticDataUint32 : this.staticData, + this.staticStride, property.offset); + } + + this.staticBuffer.upload(); + } + + /** + * Destroys the ParticleBuffer. + * + */ + destroy() + { + this.dynamicProperties = null; + this.dynamicBuffer.destroy(); + this.dynamicBuffer = null; + this.dynamicData = null; + this.dynamicDataUint32 = null; + + this.staticProperties = null; + this.staticBuffer.destroy(); + this.staticBuffer = null; + this.staticData = null; + this.staticDataUint32 = null; + } +} diff --git a/packages/particles/src/ParticleContainer.js b/packages/particles/src/ParticleContainer.js index c8ade96..9d25c5b 100644 --- a/packages/particles/src/ParticleContainer.js +++ b/packages/particles/src/ParticleContainer.js @@ -25,7 +25,7 @@ * * @class * @extends PIXI.Container - * @memberof PIXI.particles + * @memberof PIXI */ export default class ParticleContainer extends Container { diff --git a/packages/particles/src/ParticleRenderer.js b/packages/particles/src/ParticleRenderer.js new file mode 100644 index 0000000..5464211 --- /dev/null +++ b/packages/particles/src/ParticleRenderer.js @@ -0,0 +1,455 @@ +import { ObjectRenderer } from '@pixi/core'; +import { correctBlendMode, premultiplyRgba, premultiplyTint } from '@pixi/utils'; +import { Matrix } from '@pixi/math'; +import ParticleShader from './ParticleShader'; +import ParticleBuffer from './ParticleBuffer'; + +/** + * @author Mat Groves + * + * Big thanks to the very clever Matt DesLauriers https://github.com/mattdesl/ + * for creating the original PixiJS version! + * Also a thanks to https://github.com/bchevalier for tweaking the tint and alpha so that they now + * share 4 bytes on the vertex buffer + * + * Heavily inspired by LibGDX's ParticleRenderer: + * https://github.com/libgdx/libgdx/blob/master/gdx/src/com/badlogic/gdx/graphics/g2d/ParticleRenderer.java + */ + +/** + * + * @class + * @memberof PIXI + */ +export default class ParticleRenderer extends ObjectRenderer +{ + /** + * @param {PIXI.Renderer} renderer - The renderer this sprite batch works for. + */ + constructor(renderer) + { + super(renderer); + + // 65535 is max vertex index in the index buffer (see ParticleRenderer) + // so max number of particles is 65536 / 4 = 16384 + // and max number of element in the index buffer is 16384 * 6 = 98304 + // Creating a full index buffer, overhead is 98304 * 2 = 196Ko + // let numIndices = 98304; + + /** + * The default shader that is used if a sprite doesn't have a more specific one. + * + * @member {PIXI.Shader} + */ + this.shader = null; + + this.indexBuffer = null; + + this.properties = null; + + this.tempMatrix = new Matrix(); + + this.CONTEXT_UID = 0; + } + + /** + * When there is a WebGL context change + * + * @private + */ + onContextChange() + { + const gl = this.renderer.gl; + + this.CONTEXT_UID = this.renderer.CONTEXT_UID; + + // setup default shader + this.shader = new ParticleShader(gl); + + this.properties = [ + // verticesData + { + attribute: this.shader.attributes.aVertexPosition, + size: 2, + uploadFunction: this.uploadVertices, + offset: 0, + }, + // positionData + { + attribute: this.shader.attributes.aPositionCoord, + size: 2, + uploadFunction: this.uploadPosition, + offset: 0, + }, + // rotationData + { + attribute: this.shader.attributes.aRotation, + size: 1, + uploadFunction: this.uploadRotation, + offset: 0, + }, + // uvsData + { + attribute: this.shader.attributes.aTextureCoord, + size: 2, + uploadFunction: this.uploadUvs, + offset: 0, + }, + // tintData + { + attribute: this.shader.attributes.aColor, + size: 1, + unsignedByte: true, + uploadFunction: this.uploadTint, + offset: 0, + }, + ]; + } + + /** + * Starts a new particle batch. + * + */ + start() + { + this.renderer._bindGLShader(this.shader); + } + + /** + * Renders the particle container object. + * + * @param {PIXI.ParticleContainer} container - The container to render using this ParticleRenderer + */ + render(container) + { + const children = container.children; + const maxSize = container._maxSize; + const batchSize = container._batchSize; + const renderer = this.renderer; + let totalChildren = children.length; + + if (totalChildren === 0) + { + return; + } + else if (totalChildren > maxSize) + { + totalChildren = maxSize; + } + + let buffers = container._glBuffers[renderer.CONTEXT_UID]; + + if (!buffers) + { + buffers = container._glBuffers[renderer.CONTEXT_UID] = this.generateBuffers(container); + } + + const baseTexture = children[0]._texture.baseTexture; + + // if the uvs have not updated then no point rendering just yet! + this.renderer.setBlendMode(correctBlendMode(container.blendMode, baseTexture.premultiplyAlpha)); + + const gl = renderer.gl; + + const m = container.worldTransform.copy(this.tempMatrix); + + m.prepend(renderer._activeRenderTarget.projectionMatrix); + + this.shader.uniforms.projectionMatrix = m.toArray(true); + + this.shader.uniforms.uColor = premultiplyRgba(container.tintRgb, + container.worldAlpha, this.shader.uniforms.uColor, baseTexture.premultiplyAlpha); + + // make sure the texture is bound.. + this.shader.uniforms.uSampler = renderer.bindTexture(baseTexture); + + // now lets upload and render the buffers.. + for (let i = 0, j = 0; i < totalChildren; i += batchSize, j += 1) + { + let amount = (totalChildren - i); + + if (amount > batchSize) + { + amount = batchSize; + } + + if (j >= buffers.length) + { + if (!container.autoResize) + { + break; + } + buffers.push(this._generateOneMoreBuffer(container)); + } + + const buffer = buffers[j]; + + // we always upload the dynamic + buffer.uploadDynamic(children, i, amount); + + // we only upload the static content when we have to! + if (container._bufferToUpdate === j) + { + buffer.uploadStatic(children, i, amount); + container._bufferToUpdate = j + 1; + } + + // bind the buffer + renderer.bindVao(buffer.vao); + buffer.vao.draw(gl.TRIANGLES, amount * 6); + } + } + + /** + * Creates one particle buffer for each child in the container we want to render and updates internal properties + * + * @param {PIXI.ParticleContainer} container - The container to render using this ParticleRenderer + * @return {PIXI.ParticleBuffer[]} The buffers + */ + generateBuffers(container) + { + const gl = this.renderer.gl; + const buffers = []; + const size = container._maxSize; + const batchSize = container._batchSize; + const dynamicPropertyFlags = container._properties; + + for (let i = 0; i < size; i += batchSize) + { + buffers.push(new ParticleBuffer(gl, this.properties, dynamicPropertyFlags, batchSize)); + } + + return buffers; + } + + /** + * Creates one more particle buffer, because container has autoResize feature + * + * @param {PIXI.ParticleContainer} container - The container to render using this ParticleRenderer + * @return {PIXI.ParticleBuffer} generated buffer + * @private + */ + _generateOneMoreBuffer(container) + { + const gl = this.renderer.gl; + const batchSize = container._batchSize; + const dynamicPropertyFlags = container._properties; + + return new ParticleBuffer(gl, this.properties, dynamicPropertyFlags, batchSize); + } + + /** + * Uploads the verticies. + * + * @param {PIXI.DisplayObject[]} children - the array of display objects to render + * @param {number} startIndex - the index to start from in the children array + * @param {number} amount - the amount of children that will have their vertices uploaded + * @param {number[]} array - The vertices to upload. + * @param {number} stride - Stride to use for iteration. + * @param {number} offset - Offset to start at. + */ + uploadVertices(children, startIndex, amount, array, stride, offset) + { + let w0 = 0; + let w1 = 0; + let h0 = 0; + let h1 = 0; + + for (let i = 0; i < amount; ++i) + { + const sprite = children[startIndex + i]; + const texture = sprite._texture; + const sx = sprite.scale.x; + const sy = sprite.scale.y; + const trim = texture.trim; + const orig = texture.orig; + + if (trim) + { + // if the sprite is trimmed and is not a tilingsprite then we need to add the + // extra space before transforming the sprite coords.. + w1 = trim.x - (sprite.anchor.x * orig.width); + w0 = w1 + trim.width; + + h1 = trim.y - (sprite.anchor.y * orig.height); + h0 = h1 + trim.height; + } + else + { + w0 = (orig.width) * (1 - sprite.anchor.x); + w1 = (orig.width) * -sprite.anchor.x; + + h0 = orig.height * (1 - sprite.anchor.y); + h1 = orig.height * -sprite.anchor.y; + } + + array[offset] = w1 * sx; + array[offset + 1] = h1 * sy; + + array[offset + stride] = w0 * sx; + array[offset + stride + 1] = h1 * sy; + + array[offset + (stride * 2)] = w0 * sx; + array[offset + (stride * 2) + 1] = h0 * sy; + + array[offset + (stride * 3)] = w1 * sx; + array[offset + (stride * 3) + 1] = h0 * sy; + + offset += stride * 4; + } + } + + /** + * + * @param {PIXI.DisplayObject[]} children - the array of display objects to render + * @param {number} startIndex - the index to start from in the children array + * @param {number} amount - the amount of children that will have their positions uploaded + * @param {number[]} array - The vertices to upload. + * @param {number} stride - Stride to use for iteration. + * @param {number} offset - Offset to start at. + */ + uploadPosition(children, startIndex, amount, array, stride, offset) + { + for (let i = 0; i < amount; i++) + { + const spritePosition = children[startIndex + i].position; + + array[offset] = spritePosition.x; + array[offset + 1] = spritePosition.y; + + array[offset + stride] = spritePosition.x; + array[offset + stride + 1] = spritePosition.y; + + array[offset + (stride * 2)] = spritePosition.x; + array[offset + (stride * 2) + 1] = spritePosition.y; + + array[offset + (stride * 3)] = spritePosition.x; + array[offset + (stride * 3) + 1] = spritePosition.y; + + offset += stride * 4; + } + } + + /** + * + * @param {PIXI.DisplayObject[]} children - the array of display objects to render + * @param {number} startIndex - the index to start from in the children array + * @param {number} amount - the amount of children that will have their rotation uploaded + * @param {number[]} array - The vertices to upload. + * @param {number} stride - Stride to use for iteration. + * @param {number} offset - Offset to start at. + */ + uploadRotation(children, startIndex, amount, array, stride, offset) + { + for (let i = 0; i < amount; i++) + { + const spriteRotation = children[startIndex + i].rotation; + + array[offset] = spriteRotation; + array[offset + stride] = spriteRotation; + array[offset + (stride * 2)] = spriteRotation; + array[offset + (stride * 3)] = spriteRotation; + + offset += stride * 4; + } + } + + /** + * + * @param {PIXI.DisplayObject[]} children - the array of display objects to render + * @param {number} startIndex - the index to start from in the children array + * @param {number} amount - the amount of children that will have their rotation uploaded + * @param {number[]} array - The vertices to upload. + * @param {number} stride - Stride to use for iteration. + * @param {number} offset - Offset to start at. + */ + uploadUvs(children, startIndex, amount, array, stride, offset) + { + for (let i = 0; i < amount; ++i) + { + const textureUvs = children[startIndex + i]._texture._uvs; + + if (textureUvs) + { + array[offset] = textureUvs.x0; + array[offset + 1] = textureUvs.y0; + + array[offset + stride] = textureUvs.x1; + array[offset + stride + 1] = textureUvs.y1; + + array[offset + (stride * 2)] = textureUvs.x2; + array[offset + (stride * 2) + 1] = textureUvs.y2; + + array[offset + (stride * 3)] = textureUvs.x3; + array[offset + (stride * 3) + 1] = textureUvs.y3; + + offset += stride * 4; + } + else + { + // TODO you know this can be easier! + array[offset] = 0; + array[offset + 1] = 0; + + array[offset + stride] = 0; + array[offset + stride + 1] = 0; + + array[offset + (stride * 2)] = 0; + array[offset + (stride * 2) + 1] = 0; + + array[offset + (stride * 3)] = 0; + array[offset + (stride * 3) + 1] = 0; + + offset += stride * 4; + } + } + } + + /** + * + * @param {PIXI.DisplayObject[]} children - the array of display objects to render + * @param {number} startIndex - the index to start from in the children array + * @param {number} amount - the amount of children that will have their rotation uploaded + * @param {number[]} array - The vertices to upload. + * @param {number} stride - Stride to use for iteration. + * @param {number} offset - Offset to start at. + */ + uploadTint(children, startIndex, amount, array, stride, offset) + { + for (let i = 0; i < amount; ++i) + { + const sprite = children[startIndex + i]; + const premultiplied = sprite._texture.baseTexture.premultiplyAlpha; + const alpha = sprite.alpha; + // we dont call extra function if alpha is 1.0, that's faster + const argb = alpha < 1.0 && premultiplied ? premultiplyTint(sprite._tintRGB, alpha) + : sprite._tintRGB + (alpha * 255 << 24); + + array[offset] = argb; + array[offset + stride] = argb; + array[offset + (stride * 2)] = argb; + array[offset + (stride * 3)] = argb; + + offset += stride * 4; + } + } + + /** + * Destroys the ParticleRenderer. + * + */ + destroy() + { + if (this.renderer.gl) + { + this.renderer.gl.deleteBuffer(this.indexBuffer); + } + + super.destroy(); + + this.shader.destroy(); + + this.indices = null; + this.tempMatrix = null; + } +} diff --git a/packages/particles/src/ParticleShader.js b/packages/particles/src/ParticleShader.js new file mode 100644 index 0000000..069afe6 --- /dev/null +++ b/packages/particles/src/ParticleShader.js @@ -0,0 +1,21 @@ +import { GLShader } from 'pixi-gl-core'; +import { PRECISION } from '@pixi/constants'; +import vertex from './particles.vert'; +import fragment from './particles.frag'; + +/** + * @class + * @extends PIXI.Shader + * @memberof PIXI + * @private + */ +export default class ParticleShader extends GLShader +{ + /** + * @param {PIXI.Shader} gl - The webgl shader manager this shader works for. + */ + constructor(gl) + { + super(gl, vertex, fragment, PRECISION.DEFAULT); + } +} diff --git a/packages/particles/src/index.js b/packages/particles/src/index.js index 629d599..d315119 100644 --- a/packages/particles/src/index.js +++ b/packages/particles/src/index.js @@ -1,5 +1,2 @@ -/** - * @namespace PIXI.particles - */ export { default as ParticleContainer } from './ParticleContainer'; -export { default as ParticleRenderer } from './webgl/ParticleRenderer'; +export { default as ParticleRenderer } from './ParticleRenderer'; diff --git a/packages/particles/src/particles.frag b/packages/particles/src/particles.frag new file mode 100644 index 0000000..a725046 --- /dev/null +++ b/packages/particles/src/particles.frag @@ -0,0 +1,9 @@ +varying vec2 vTextureCoord; +varying vec4 vColor; + +uniform sampler2D uSampler; + +void main(void){ + vec4 color = texture2D(uSampler, vTextureCoord) * vColor; + gl_FragColor = color; +} \ No newline at end of file diff --git a/packages/particles/src/particles.vert b/packages/particles/src/particles.vert new file mode 100644 index 0000000..bd5401c --- /dev/null +++ b/packages/particles/src/particles.vert @@ -0,0 +1,26 @@ +attribute vec2 aVertexPosition; +attribute vec2 aTextureCoord; +attribute vec4 aColor; + +attribute vec2 aPositionCoord; +attribute vec2 aScale; +attribute float aRotation; + +uniform mat3 projectionMatrix; +uniform vec4 uColor; + +varying vec2 vTextureCoord; +varying vec4 vColor; + +void main(void){ + float x = (aVertexPosition.x) * cos(aRotation) - (aVertexPosition.y) * sin(aRotation); + float y = (aVertexPosition.x) * sin(aRotation) + (aVertexPosition.y) * cos(aRotation); + + vec2 v = vec2(x, y); + v = v + aPositionCoord; + + gl_Position = vec4((projectionMatrix * vec3(v, 1.0)).xy, 0.0, 1.0); + + vTextureCoord = aTextureCoord; + vColor = aColor * uColor; +} \ No newline at end of file diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index e59c66a..a390a1a 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -332,6 +332,43 @@ /* * This namespace has been removed and items have been moved to * the top-level `PIXI` object. + * @namespace PIXI.particles + * @deprecated since 5.0.0 + */ + PIXI.particles = {}; + + Object.defineProperties(PIXI.particles, { + /** + * @class PIXI.particles.ParticleContainer + * @deprecated since 5.0.0 + * @see PIXI.ParticleContainer + */ + ParticleContainer: { + get() + { + warn('PIXI.particles.ParticleContainer has moved to PIXI.ParticleContainer'); + + return PIXI.ParticleContainer; + }, + }, + /** + * @class PIXI.particles.ParticleRenderer + * @deprecated since 5.0.0 + * @see PIXI.ParticleRenderer + */ + ParticleRenderer: { + get() + { + warn('PIXI.particles.ParticleRenderer has moved to PIXI.ParticleRenderer'); + + return PIXI.ParticleRenderer; + }, + }, + }); + + /* + * This namespace has been removed and items have been moved to + * the top-level `PIXI` object. * @namespace PIXI.ticker * @deprecated since 5.0.0 */ diff --git a/bundles/pixi.js/src/index.js b/bundles/pixi.js/src/index.js index 52d7cb4..fb839a0 100644 --- a/bundles/pixi.js/src/index.js +++ b/bundles/pixi.js/src/index.js @@ -9,6 +9,7 @@ import * as loaders from '@pixi/loaders'; import * as math from '@pixi/math'; import * as mesh from '@pixi/mesh'; +import * as particles from '@pixi/particles'; import * as prepare from '@pixi/prepare'; import * as sprite from '@pixi/sprite'; import * as spriteAnimated from '@pixi/sprite-animated'; @@ -37,6 +38,7 @@ core.Renderer.registerPlugin('graphics', graphics.GraphicsRenderer); core.Renderer.registerPlugin('interaction', interaction.InteractionManager); core.Renderer.registerPlugin('mesh', mesh.MeshRenderer); +core.Renderer.registerPlugin('particle', particles.ParticleRenderer); core.Renderer.registerPlugin('prepare', prepare.Prepare); core.Renderer.registerPlugin('sprite', sprite.SpriteRenderer); core.Renderer.registerPlugin('tilingSprite', spriteTiling.TilingSpriteRenderer); @@ -118,6 +120,7 @@ loaders, math, mesh, + particles, sprite, spriteAnimated, spritesheet, @@ -141,6 +144,7 @@ export * from '@pixi/loaders'; export * from '@pixi/math'; export * from '@pixi/mesh'; +export * from '@pixi/particles'; export * from '@pixi/sprite'; export * from '@pixi/spritesheet'; export * from '@pixi/sprite-animated'; diff --git a/packages/particles/README.md b/packages/particles/README.md index bca3a96..dca806d 100644 --- a/packages/particles/README.md +++ b/packages/particles/README.md @@ -9,5 +9,8 @@ ## Usage ```js -import '@pixi/particles'; +import {ParticleRenderer} from '@pixi/particles'; +import {Renderer} from '@pixi/core'; + +Renderer.registerPlugin('particle', ParticleRenderer); ``` \ No newline at end of file diff --git a/packages/particles/src/ParticleBuffer.js b/packages/particles/src/ParticleBuffer.js new file mode 100644 index 0000000..6306bda --- /dev/null +++ b/packages/particles/src/ParticleBuffer.js @@ -0,0 +1,268 @@ +// import { VertexArrayObject } from 'pixi-gl-core'; +import { createIndicesForQuads } from '@pixi/utils'; +import { GLBuffer } from 'pixi-gl-core'; + +/** + * @author Mat Groves + * + * Big thanks to the very clever Matt DesLauriers https://github.com/mattdesl/ + * for creating the original PixiJS version! + * Also a thanks to https://github.com/bchevalier for tweaking the tint and alpha so that + * they now share 4 bytes on the vertex buffer + * + * Heavily inspired by LibGDX's ParticleBuffer: + * https://github.com/libgdx/libgdx/blob/master/gdx/src/com/badlogic/gdx/graphics/g2d/ParticleBuffer.java + */ + +/** + * The particle buffer manages the static and dynamic buffers for a particle container. + * + * @class + * @private + * @memberof PIXI + */ +export default class ParticleBuffer +{ + /** + * @param {WebGLRenderingContext} gl - The rendering context. + * @param {object} properties - The properties to upload. + * @param {boolean[]} dynamicPropertyFlags - Flags for which properties are dynamic. + * @param {number} size - The size of the batch. + */ + constructor(gl, properties, dynamicPropertyFlags, size) + { + /** + * The current WebGL drawing context. + * + * @member {WebGLRenderingContext} + */ + this.gl = gl; + + /** + * The number of particles the buffer can hold + * + * @member {number} + */ + this.size = size; + + /** + * A list of the properties that are dynamic. + * + * @member {object[]} + */ + this.dynamicProperties = []; + + /** + * A list of the properties that are static. + * + * @member {object[]} + */ + this.staticProperties = []; + + for (let i = 0; i < properties.length; ++i) + { + let property = properties[i]; + + // Make copy of properties object so that when we edit the offset it doesn't + // change all other instances of the object literal + property = { + attribute: property.attribute, + size: property.size, + uploadFunction: property.uploadFunction, + unsignedByte: property.unsignedByte, + offset: property.offset, + }; + + if (dynamicPropertyFlags[i]) + { + this.dynamicProperties.push(property); + } + else + { + this.staticProperties.push(property); + } + } + + this.staticStride = 0; + this.staticBuffer = null; + this.staticData = null; + this.staticDataUint32 = null; + + this.dynamicStride = 0; + this.dynamicBuffer = null; + this.dynamicData = null; + this.dynamicDataUint32 = null; + + this.initBuffers(); + } + + /** + * Sets up the renderer context and necessary buffers. + * + * @private + */ + initBuffers() + { + const gl = this.gl; + let dynamicOffset = 0; + + /** + * Holds the indices of the geometry (quads) to draw + * + * @member {Uint16Array} + */ + this.indices = createIndicesForQuads(this.size); + this.indexBuffer = GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); + + this.dynamicStride = 0; + + for (let i = 0; i < this.dynamicProperties.length; ++i) + { + const property = this.dynamicProperties[i]; + + property.offset = dynamicOffset; + dynamicOffset += property.size; + this.dynamicStride += property.size; + } + + this.dynamicData = new Float32Array(this.size * this.dynamicStride * 4); + this.dynamicBuffer = GLBuffer.createVertexBuffer(gl, this.dynamicData, gl.STREAM_DRAW); + + // static // + let staticOffset = 0; + + this.staticStride = 0; + + for (let i = 0; i < this.staticProperties.length; ++i) + { + const property = this.staticProperties[i]; + + property.offset = staticOffset; + staticOffset += property.size; + this.staticStride += property.size; + } + + this.staticData = new Float32Array(this.size * this.staticStride * 4); + this.staticBuffer = GLBuffer.createVertexBuffer(gl, this.staticData, gl.STATIC_DRAW); + + // this.vao = new VertexArrayObject(gl) + // .addIndex(this.indexBuffer); + + for (let i = 0; i < this.dynamicProperties.length; ++i) + { + const property = this.dynamicProperties[i]; + + if (property.unsignedByte) + { + this.vao.addAttribute( + this.dynamicBuffer, + property.attribute, + gl.UNSIGNED_BYTE, + true, + this.dynamicStride * 4, + property.offset * 4 + ); + } + else + { + this.vao.addAttribute( + this.dynamicBuffer, + property.attribute, + gl.FLOAT, + false, + this.dynamicStride * 4, + property.offset * 4 + ); + } + } + + for (let i = 0; i < this.staticProperties.length; ++i) + { + const property = this.staticProperties[i]; + + if (property.unsignedByte) + { + this.vao.addAttribute( + this.staticBuffer, + property.attribute, + gl.UNSIGNED_BYTE, + true, + this.staticStride * 4, + property.offset * 4 + ); + } + else + { + this.vao.addAttribute( + this.staticBuffer, + property.attribute, + gl.FLOAT, + false, + this.staticStride * 4, + property.offset * 4 + ); + } + } + } + + /** + * Uploads the dynamic properties. + * + * @param {PIXI.DisplayObject[]} children - The children to upload. + * @param {number} startIndex - The index to start at. + * @param {number} amount - The number to upload. + */ + uploadDynamic(children, startIndex, amount) + { + for (let i = 0; i < this.dynamicProperties.length; i++) + { + const property = this.dynamicProperties[i]; + + property.uploadFunction(children, startIndex, amount, + property.unsignedByte ? this.dynamicDataUint32 : this.dynamicData, + this.dynamicStride, property.offset); + } + + this.dynamicBuffer.upload(); + } + + /** + * Uploads the static properties. + * + * @param {PIXI.DisplayObject[]} children - The children to upload. + * @param {number} startIndex - The index to start at. + * @param {number} amount - The number to upload. + */ + uploadStatic(children, startIndex, amount) + { + for (let i = 0; i < this.staticProperties.length; i++) + { + const property = this.staticProperties[i]; + + property.uploadFunction(children, startIndex, amount, + property.unsignedByte ? this.staticDataUint32 : this.staticData, + this.staticStride, property.offset); + } + + this.staticBuffer.upload(); + } + + /** + * Destroys the ParticleBuffer. + * + */ + destroy() + { + this.dynamicProperties = null; + this.dynamicBuffer.destroy(); + this.dynamicBuffer = null; + this.dynamicData = null; + this.dynamicDataUint32 = null; + + this.staticProperties = null; + this.staticBuffer.destroy(); + this.staticBuffer = null; + this.staticData = null; + this.staticDataUint32 = null; + } +} diff --git a/packages/particles/src/ParticleContainer.js b/packages/particles/src/ParticleContainer.js index c8ade96..9d25c5b 100644 --- a/packages/particles/src/ParticleContainer.js +++ b/packages/particles/src/ParticleContainer.js @@ -25,7 +25,7 @@ * * @class * @extends PIXI.Container - * @memberof PIXI.particles + * @memberof PIXI */ export default class ParticleContainer extends Container { diff --git a/packages/particles/src/ParticleRenderer.js b/packages/particles/src/ParticleRenderer.js new file mode 100644 index 0000000..5464211 --- /dev/null +++ b/packages/particles/src/ParticleRenderer.js @@ -0,0 +1,455 @@ +import { ObjectRenderer } from '@pixi/core'; +import { correctBlendMode, premultiplyRgba, premultiplyTint } from '@pixi/utils'; +import { Matrix } from '@pixi/math'; +import ParticleShader from './ParticleShader'; +import ParticleBuffer from './ParticleBuffer'; + +/** + * @author Mat Groves + * + * Big thanks to the very clever Matt DesLauriers https://github.com/mattdesl/ + * for creating the original PixiJS version! + * Also a thanks to https://github.com/bchevalier for tweaking the tint and alpha so that they now + * share 4 bytes on the vertex buffer + * + * Heavily inspired by LibGDX's ParticleRenderer: + * https://github.com/libgdx/libgdx/blob/master/gdx/src/com/badlogic/gdx/graphics/g2d/ParticleRenderer.java + */ + +/** + * + * @class + * @memberof PIXI + */ +export default class ParticleRenderer extends ObjectRenderer +{ + /** + * @param {PIXI.Renderer} renderer - The renderer this sprite batch works for. + */ + constructor(renderer) + { + super(renderer); + + // 65535 is max vertex index in the index buffer (see ParticleRenderer) + // so max number of particles is 65536 / 4 = 16384 + // and max number of element in the index buffer is 16384 * 6 = 98304 + // Creating a full index buffer, overhead is 98304 * 2 = 196Ko + // let numIndices = 98304; + + /** + * The default shader that is used if a sprite doesn't have a more specific one. + * + * @member {PIXI.Shader} + */ + this.shader = null; + + this.indexBuffer = null; + + this.properties = null; + + this.tempMatrix = new Matrix(); + + this.CONTEXT_UID = 0; + } + + /** + * When there is a WebGL context change + * + * @private + */ + onContextChange() + { + const gl = this.renderer.gl; + + this.CONTEXT_UID = this.renderer.CONTEXT_UID; + + // setup default shader + this.shader = new ParticleShader(gl); + + this.properties = [ + // verticesData + { + attribute: this.shader.attributes.aVertexPosition, + size: 2, + uploadFunction: this.uploadVertices, + offset: 0, + }, + // positionData + { + attribute: this.shader.attributes.aPositionCoord, + size: 2, + uploadFunction: this.uploadPosition, + offset: 0, + }, + // rotationData + { + attribute: this.shader.attributes.aRotation, + size: 1, + uploadFunction: this.uploadRotation, + offset: 0, + }, + // uvsData + { + attribute: this.shader.attributes.aTextureCoord, + size: 2, + uploadFunction: this.uploadUvs, + offset: 0, + }, + // tintData + { + attribute: this.shader.attributes.aColor, + size: 1, + unsignedByte: true, + uploadFunction: this.uploadTint, + offset: 0, + }, + ]; + } + + /** + * Starts a new particle batch. + * + */ + start() + { + this.renderer._bindGLShader(this.shader); + } + + /** + * Renders the particle container object. + * + * @param {PIXI.ParticleContainer} container - The container to render using this ParticleRenderer + */ + render(container) + { + const children = container.children; + const maxSize = container._maxSize; + const batchSize = container._batchSize; + const renderer = this.renderer; + let totalChildren = children.length; + + if (totalChildren === 0) + { + return; + } + else if (totalChildren > maxSize) + { + totalChildren = maxSize; + } + + let buffers = container._glBuffers[renderer.CONTEXT_UID]; + + if (!buffers) + { + buffers = container._glBuffers[renderer.CONTEXT_UID] = this.generateBuffers(container); + } + + const baseTexture = children[0]._texture.baseTexture; + + // if the uvs have not updated then no point rendering just yet! + this.renderer.setBlendMode(correctBlendMode(container.blendMode, baseTexture.premultiplyAlpha)); + + const gl = renderer.gl; + + const m = container.worldTransform.copy(this.tempMatrix); + + m.prepend(renderer._activeRenderTarget.projectionMatrix); + + this.shader.uniforms.projectionMatrix = m.toArray(true); + + this.shader.uniforms.uColor = premultiplyRgba(container.tintRgb, + container.worldAlpha, this.shader.uniforms.uColor, baseTexture.premultiplyAlpha); + + // make sure the texture is bound.. + this.shader.uniforms.uSampler = renderer.bindTexture(baseTexture); + + // now lets upload and render the buffers.. + for (let i = 0, j = 0; i < totalChildren; i += batchSize, j += 1) + { + let amount = (totalChildren - i); + + if (amount > batchSize) + { + amount = batchSize; + } + + if (j >= buffers.length) + { + if (!container.autoResize) + { + break; + } + buffers.push(this._generateOneMoreBuffer(container)); + } + + const buffer = buffers[j]; + + // we always upload the dynamic + buffer.uploadDynamic(children, i, amount); + + // we only upload the static content when we have to! + if (container._bufferToUpdate === j) + { + buffer.uploadStatic(children, i, amount); + container._bufferToUpdate = j + 1; + } + + // bind the buffer + renderer.bindVao(buffer.vao); + buffer.vao.draw(gl.TRIANGLES, amount * 6); + } + } + + /** + * Creates one particle buffer for each child in the container we want to render and updates internal properties + * + * @param {PIXI.ParticleContainer} container - The container to render using this ParticleRenderer + * @return {PIXI.ParticleBuffer[]} The buffers + */ + generateBuffers(container) + { + const gl = this.renderer.gl; + const buffers = []; + const size = container._maxSize; + const batchSize = container._batchSize; + const dynamicPropertyFlags = container._properties; + + for (let i = 0; i < size; i += batchSize) + { + buffers.push(new ParticleBuffer(gl, this.properties, dynamicPropertyFlags, batchSize)); + } + + return buffers; + } + + /** + * Creates one more particle buffer, because container has autoResize feature + * + * @param {PIXI.ParticleContainer} container - The container to render using this ParticleRenderer + * @return {PIXI.ParticleBuffer} generated buffer + * @private + */ + _generateOneMoreBuffer(container) + { + const gl = this.renderer.gl; + const batchSize = container._batchSize; + const dynamicPropertyFlags = container._properties; + + return new ParticleBuffer(gl, this.properties, dynamicPropertyFlags, batchSize); + } + + /** + * Uploads the verticies. + * + * @param {PIXI.DisplayObject[]} children - the array of display objects to render + * @param {number} startIndex - the index to start from in the children array + * @param {number} amount - the amount of children that will have their vertices uploaded + * @param {number[]} array - The vertices to upload. + * @param {number} stride - Stride to use for iteration. + * @param {number} offset - Offset to start at. + */ + uploadVertices(children, startIndex, amount, array, stride, offset) + { + let w0 = 0; + let w1 = 0; + let h0 = 0; + let h1 = 0; + + for (let i = 0; i < amount; ++i) + { + const sprite = children[startIndex + i]; + const texture = sprite._texture; + const sx = sprite.scale.x; + const sy = sprite.scale.y; + const trim = texture.trim; + const orig = texture.orig; + + if (trim) + { + // if the sprite is trimmed and is not a tilingsprite then we need to add the + // extra space before transforming the sprite coords.. + w1 = trim.x - (sprite.anchor.x * orig.width); + w0 = w1 + trim.width; + + h1 = trim.y - (sprite.anchor.y * orig.height); + h0 = h1 + trim.height; + } + else + { + w0 = (orig.width) * (1 - sprite.anchor.x); + w1 = (orig.width) * -sprite.anchor.x; + + h0 = orig.height * (1 - sprite.anchor.y); + h1 = orig.height * -sprite.anchor.y; + } + + array[offset] = w1 * sx; + array[offset + 1] = h1 * sy; + + array[offset + stride] = w0 * sx; + array[offset + stride + 1] = h1 * sy; + + array[offset + (stride * 2)] = w0 * sx; + array[offset + (stride * 2) + 1] = h0 * sy; + + array[offset + (stride * 3)] = w1 * sx; + array[offset + (stride * 3) + 1] = h0 * sy; + + offset += stride * 4; + } + } + + /** + * + * @param {PIXI.DisplayObject[]} children - the array of display objects to render + * @param {number} startIndex - the index to start from in the children array + * @param {number} amount - the amount of children that will have their positions uploaded + * @param {number[]} array - The vertices to upload. + * @param {number} stride - Stride to use for iteration. + * @param {number} offset - Offset to start at. + */ + uploadPosition(children, startIndex, amount, array, stride, offset) + { + for (let i = 0; i < amount; i++) + { + const spritePosition = children[startIndex + i].position; + + array[offset] = spritePosition.x; + array[offset + 1] = spritePosition.y; + + array[offset + stride] = spritePosition.x; + array[offset + stride + 1] = spritePosition.y; + + array[offset + (stride * 2)] = spritePosition.x; + array[offset + (stride * 2) + 1] = spritePosition.y; + + array[offset + (stride * 3)] = spritePosition.x; + array[offset + (stride * 3) + 1] = spritePosition.y; + + offset += stride * 4; + } + } + + /** + * + * @param {PIXI.DisplayObject[]} children - the array of display objects to render + * @param {number} startIndex - the index to start from in the children array + * @param {number} amount - the amount of children that will have their rotation uploaded + * @param {number[]} array - The vertices to upload. + * @param {number} stride - Stride to use for iteration. + * @param {number} offset - Offset to start at. + */ + uploadRotation(children, startIndex, amount, array, stride, offset) + { + for (let i = 0; i < amount; i++) + { + const spriteRotation = children[startIndex + i].rotation; + + array[offset] = spriteRotation; + array[offset + stride] = spriteRotation; + array[offset + (stride * 2)] = spriteRotation; + array[offset + (stride * 3)] = spriteRotation; + + offset += stride * 4; + } + } + + /** + * + * @param {PIXI.DisplayObject[]} children - the array of display objects to render + * @param {number} startIndex - the index to start from in the children array + * @param {number} amount - the amount of children that will have their rotation uploaded + * @param {number[]} array - The vertices to upload. + * @param {number} stride - Stride to use for iteration. + * @param {number} offset - Offset to start at. + */ + uploadUvs(children, startIndex, amount, array, stride, offset) + { + for (let i = 0; i < amount; ++i) + { + const textureUvs = children[startIndex + i]._texture._uvs; + + if (textureUvs) + { + array[offset] = textureUvs.x0; + array[offset + 1] = textureUvs.y0; + + array[offset + stride] = textureUvs.x1; + array[offset + stride + 1] = textureUvs.y1; + + array[offset + (stride * 2)] = textureUvs.x2; + array[offset + (stride * 2) + 1] = textureUvs.y2; + + array[offset + (stride * 3)] = textureUvs.x3; + array[offset + (stride * 3) + 1] = textureUvs.y3; + + offset += stride * 4; + } + else + { + // TODO you know this can be easier! + array[offset] = 0; + array[offset + 1] = 0; + + array[offset + stride] = 0; + array[offset + stride + 1] = 0; + + array[offset + (stride * 2)] = 0; + array[offset + (stride * 2) + 1] = 0; + + array[offset + (stride * 3)] = 0; + array[offset + (stride * 3) + 1] = 0; + + offset += stride * 4; + } + } + } + + /** + * + * @param {PIXI.DisplayObject[]} children - the array of display objects to render + * @param {number} startIndex - the index to start from in the children array + * @param {number} amount - the amount of children that will have their rotation uploaded + * @param {number[]} array - The vertices to upload. + * @param {number} stride - Stride to use for iteration. + * @param {number} offset - Offset to start at. + */ + uploadTint(children, startIndex, amount, array, stride, offset) + { + for (let i = 0; i < amount; ++i) + { + const sprite = children[startIndex + i]; + const premultiplied = sprite._texture.baseTexture.premultiplyAlpha; + const alpha = sprite.alpha; + // we dont call extra function if alpha is 1.0, that's faster + const argb = alpha < 1.0 && premultiplied ? premultiplyTint(sprite._tintRGB, alpha) + : sprite._tintRGB + (alpha * 255 << 24); + + array[offset] = argb; + array[offset + stride] = argb; + array[offset + (stride * 2)] = argb; + array[offset + (stride * 3)] = argb; + + offset += stride * 4; + } + } + + /** + * Destroys the ParticleRenderer. + * + */ + destroy() + { + if (this.renderer.gl) + { + this.renderer.gl.deleteBuffer(this.indexBuffer); + } + + super.destroy(); + + this.shader.destroy(); + + this.indices = null; + this.tempMatrix = null; + } +} diff --git a/packages/particles/src/ParticleShader.js b/packages/particles/src/ParticleShader.js new file mode 100644 index 0000000..069afe6 --- /dev/null +++ b/packages/particles/src/ParticleShader.js @@ -0,0 +1,21 @@ +import { GLShader } from 'pixi-gl-core'; +import { PRECISION } from '@pixi/constants'; +import vertex from './particles.vert'; +import fragment from './particles.frag'; + +/** + * @class + * @extends PIXI.Shader + * @memberof PIXI + * @private + */ +export default class ParticleShader extends GLShader +{ + /** + * @param {PIXI.Shader} gl - The webgl shader manager this shader works for. + */ + constructor(gl) + { + super(gl, vertex, fragment, PRECISION.DEFAULT); + } +} diff --git a/packages/particles/src/index.js b/packages/particles/src/index.js index 629d599..d315119 100644 --- a/packages/particles/src/index.js +++ b/packages/particles/src/index.js @@ -1,5 +1,2 @@ -/** - * @namespace PIXI.particles - */ export { default as ParticleContainer } from './ParticleContainer'; -export { default as ParticleRenderer } from './webgl/ParticleRenderer'; +export { default as ParticleRenderer } from './ParticleRenderer'; diff --git a/packages/particles/src/particles.frag b/packages/particles/src/particles.frag new file mode 100644 index 0000000..a725046 --- /dev/null +++ b/packages/particles/src/particles.frag @@ -0,0 +1,9 @@ +varying vec2 vTextureCoord; +varying vec4 vColor; + +uniform sampler2D uSampler; + +void main(void){ + vec4 color = texture2D(uSampler, vTextureCoord) * vColor; + gl_FragColor = color; +} \ No newline at end of file diff --git a/packages/particles/src/particles.vert b/packages/particles/src/particles.vert new file mode 100644 index 0000000..bd5401c --- /dev/null +++ b/packages/particles/src/particles.vert @@ -0,0 +1,26 @@ +attribute vec2 aVertexPosition; +attribute vec2 aTextureCoord; +attribute vec4 aColor; + +attribute vec2 aPositionCoord; +attribute vec2 aScale; +attribute float aRotation; + +uniform mat3 projectionMatrix; +uniform vec4 uColor; + +varying vec2 vTextureCoord; +varying vec4 vColor; + +void main(void){ + float x = (aVertexPosition.x) * cos(aRotation) - (aVertexPosition.y) * sin(aRotation); + float y = (aVertexPosition.x) * sin(aRotation) + (aVertexPosition.y) * cos(aRotation); + + vec2 v = vec2(x, y); + v = v + aPositionCoord; + + gl_Position = vec4((projectionMatrix * vec3(v, 1.0)).xy, 0.0, 1.0); + + vTextureCoord = aTextureCoord; + vColor = aColor * uColor; +} \ No newline at end of file diff --git a/packages/particles/src/webgl/ParticleBuffer.js b/packages/particles/src/webgl/ParticleBuffer.js deleted file mode 100644 index 6306bda..0000000 --- a/packages/particles/src/webgl/ParticleBuffer.js +++ /dev/null @@ -1,268 +0,0 @@ -// import { VertexArrayObject } from 'pixi-gl-core'; -import { createIndicesForQuads } from '@pixi/utils'; -import { GLBuffer } from 'pixi-gl-core'; - -/** - * @author Mat Groves - * - * Big thanks to the very clever Matt DesLauriers https://github.com/mattdesl/ - * for creating the original PixiJS version! - * Also a thanks to https://github.com/bchevalier for tweaking the tint and alpha so that - * they now share 4 bytes on the vertex buffer - * - * Heavily inspired by LibGDX's ParticleBuffer: - * https://github.com/libgdx/libgdx/blob/master/gdx/src/com/badlogic/gdx/graphics/g2d/ParticleBuffer.java - */ - -/** - * The particle buffer manages the static and dynamic buffers for a particle container. - * - * @class - * @private - * @memberof PIXI - */ -export default class ParticleBuffer -{ - /** - * @param {WebGLRenderingContext} gl - The rendering context. - * @param {object} properties - The properties to upload. - * @param {boolean[]} dynamicPropertyFlags - Flags for which properties are dynamic. - * @param {number} size - The size of the batch. - */ - constructor(gl, properties, dynamicPropertyFlags, size) - { - /** - * The current WebGL drawing context. - * - * @member {WebGLRenderingContext} - */ - this.gl = gl; - - /** - * The number of particles the buffer can hold - * - * @member {number} - */ - this.size = size; - - /** - * A list of the properties that are dynamic. - * - * @member {object[]} - */ - this.dynamicProperties = []; - - /** - * A list of the properties that are static. - * - * @member {object[]} - */ - this.staticProperties = []; - - for (let i = 0; i < properties.length; ++i) - { - let property = properties[i]; - - // Make copy of properties object so that when we edit the offset it doesn't - // change all other instances of the object literal - property = { - attribute: property.attribute, - size: property.size, - uploadFunction: property.uploadFunction, - unsignedByte: property.unsignedByte, - offset: property.offset, - }; - - if (dynamicPropertyFlags[i]) - { - this.dynamicProperties.push(property); - } - else - { - this.staticProperties.push(property); - } - } - - this.staticStride = 0; - this.staticBuffer = null; - this.staticData = null; - this.staticDataUint32 = null; - - this.dynamicStride = 0; - this.dynamicBuffer = null; - this.dynamicData = null; - this.dynamicDataUint32 = null; - - this.initBuffers(); - } - - /** - * Sets up the renderer context and necessary buffers. - * - * @private - */ - initBuffers() - { - const gl = this.gl; - let dynamicOffset = 0; - - /** - * Holds the indices of the geometry (quads) to draw - * - * @member {Uint16Array} - */ - this.indices = createIndicesForQuads(this.size); - this.indexBuffer = GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); - - this.dynamicStride = 0; - - for (let i = 0; i < this.dynamicProperties.length; ++i) - { - const property = this.dynamicProperties[i]; - - property.offset = dynamicOffset; - dynamicOffset += property.size; - this.dynamicStride += property.size; - } - - this.dynamicData = new Float32Array(this.size * this.dynamicStride * 4); - this.dynamicBuffer = GLBuffer.createVertexBuffer(gl, this.dynamicData, gl.STREAM_DRAW); - - // static // - let staticOffset = 0; - - this.staticStride = 0; - - for (let i = 0; i < this.staticProperties.length; ++i) - { - const property = this.staticProperties[i]; - - property.offset = staticOffset; - staticOffset += property.size; - this.staticStride += property.size; - } - - this.staticData = new Float32Array(this.size * this.staticStride * 4); - this.staticBuffer = GLBuffer.createVertexBuffer(gl, this.staticData, gl.STATIC_DRAW); - - // this.vao = new VertexArrayObject(gl) - // .addIndex(this.indexBuffer); - - for (let i = 0; i < this.dynamicProperties.length; ++i) - { - const property = this.dynamicProperties[i]; - - if (property.unsignedByte) - { - this.vao.addAttribute( - this.dynamicBuffer, - property.attribute, - gl.UNSIGNED_BYTE, - true, - this.dynamicStride * 4, - property.offset * 4 - ); - } - else - { - this.vao.addAttribute( - this.dynamicBuffer, - property.attribute, - gl.FLOAT, - false, - this.dynamicStride * 4, - property.offset * 4 - ); - } - } - - for (let i = 0; i < this.staticProperties.length; ++i) - { - const property = this.staticProperties[i]; - - if (property.unsignedByte) - { - this.vao.addAttribute( - this.staticBuffer, - property.attribute, - gl.UNSIGNED_BYTE, - true, - this.staticStride * 4, - property.offset * 4 - ); - } - else - { - this.vao.addAttribute( - this.staticBuffer, - property.attribute, - gl.FLOAT, - false, - this.staticStride * 4, - property.offset * 4 - ); - } - } - } - - /** - * Uploads the dynamic properties. - * - * @param {PIXI.DisplayObject[]} children - The children to upload. - * @param {number} startIndex - The index to start at. - * @param {number} amount - The number to upload. - */ - uploadDynamic(children, startIndex, amount) - { - for (let i = 0; i < this.dynamicProperties.length; i++) - { - const property = this.dynamicProperties[i]; - - property.uploadFunction(children, startIndex, amount, - property.unsignedByte ? this.dynamicDataUint32 : this.dynamicData, - this.dynamicStride, property.offset); - } - - this.dynamicBuffer.upload(); - } - - /** - * Uploads the static properties. - * - * @param {PIXI.DisplayObject[]} children - The children to upload. - * @param {number} startIndex - The index to start at. - * @param {number} amount - The number to upload. - */ - uploadStatic(children, startIndex, amount) - { - for (let i = 0; i < this.staticProperties.length; i++) - { - const property = this.staticProperties[i]; - - property.uploadFunction(children, startIndex, amount, - property.unsignedByte ? this.staticDataUint32 : this.staticData, - this.staticStride, property.offset); - } - - this.staticBuffer.upload(); - } - - /** - * Destroys the ParticleBuffer. - * - */ - destroy() - { - this.dynamicProperties = null; - this.dynamicBuffer.destroy(); - this.dynamicBuffer = null; - this.dynamicData = null; - this.dynamicDataUint32 = null; - - this.staticProperties = null; - this.staticBuffer.destroy(); - this.staticBuffer = null; - this.staticData = null; - this.staticDataUint32 = null; - } -} diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index e59c66a..a390a1a 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -332,6 +332,43 @@ /* * This namespace has been removed and items have been moved to * the top-level `PIXI` object. + * @namespace PIXI.particles + * @deprecated since 5.0.0 + */ + PIXI.particles = {}; + + Object.defineProperties(PIXI.particles, { + /** + * @class PIXI.particles.ParticleContainer + * @deprecated since 5.0.0 + * @see PIXI.ParticleContainer + */ + ParticleContainer: { + get() + { + warn('PIXI.particles.ParticleContainer has moved to PIXI.ParticleContainer'); + + return PIXI.ParticleContainer; + }, + }, + /** + * @class PIXI.particles.ParticleRenderer + * @deprecated since 5.0.0 + * @see PIXI.ParticleRenderer + */ + ParticleRenderer: { + get() + { + warn('PIXI.particles.ParticleRenderer has moved to PIXI.ParticleRenderer'); + + return PIXI.ParticleRenderer; + }, + }, + }); + + /* + * This namespace has been removed and items have been moved to + * the top-level `PIXI` object. * @namespace PIXI.ticker * @deprecated since 5.0.0 */ diff --git a/bundles/pixi.js/src/index.js b/bundles/pixi.js/src/index.js index 52d7cb4..fb839a0 100644 --- a/bundles/pixi.js/src/index.js +++ b/bundles/pixi.js/src/index.js @@ -9,6 +9,7 @@ import * as loaders from '@pixi/loaders'; import * as math from '@pixi/math'; import * as mesh from '@pixi/mesh'; +import * as particles from '@pixi/particles'; import * as prepare from '@pixi/prepare'; import * as sprite from '@pixi/sprite'; import * as spriteAnimated from '@pixi/sprite-animated'; @@ -37,6 +38,7 @@ core.Renderer.registerPlugin('graphics', graphics.GraphicsRenderer); core.Renderer.registerPlugin('interaction', interaction.InteractionManager); core.Renderer.registerPlugin('mesh', mesh.MeshRenderer); +core.Renderer.registerPlugin('particle', particles.ParticleRenderer); core.Renderer.registerPlugin('prepare', prepare.Prepare); core.Renderer.registerPlugin('sprite', sprite.SpriteRenderer); core.Renderer.registerPlugin('tilingSprite', spriteTiling.TilingSpriteRenderer); @@ -118,6 +120,7 @@ loaders, math, mesh, + particles, sprite, spriteAnimated, spritesheet, @@ -141,6 +144,7 @@ export * from '@pixi/loaders'; export * from '@pixi/math'; export * from '@pixi/mesh'; +export * from '@pixi/particles'; export * from '@pixi/sprite'; export * from '@pixi/spritesheet'; export * from '@pixi/sprite-animated'; diff --git a/packages/particles/README.md b/packages/particles/README.md index bca3a96..dca806d 100644 --- a/packages/particles/README.md +++ b/packages/particles/README.md @@ -9,5 +9,8 @@ ## Usage ```js -import '@pixi/particles'; +import {ParticleRenderer} from '@pixi/particles'; +import {Renderer} from '@pixi/core'; + +Renderer.registerPlugin('particle', ParticleRenderer); ``` \ No newline at end of file diff --git a/packages/particles/src/ParticleBuffer.js b/packages/particles/src/ParticleBuffer.js new file mode 100644 index 0000000..6306bda --- /dev/null +++ b/packages/particles/src/ParticleBuffer.js @@ -0,0 +1,268 @@ +// import { VertexArrayObject } from 'pixi-gl-core'; +import { createIndicesForQuads } from '@pixi/utils'; +import { GLBuffer } from 'pixi-gl-core'; + +/** + * @author Mat Groves + * + * Big thanks to the very clever Matt DesLauriers https://github.com/mattdesl/ + * for creating the original PixiJS version! + * Also a thanks to https://github.com/bchevalier for tweaking the tint and alpha so that + * they now share 4 bytes on the vertex buffer + * + * Heavily inspired by LibGDX's ParticleBuffer: + * https://github.com/libgdx/libgdx/blob/master/gdx/src/com/badlogic/gdx/graphics/g2d/ParticleBuffer.java + */ + +/** + * The particle buffer manages the static and dynamic buffers for a particle container. + * + * @class + * @private + * @memberof PIXI + */ +export default class ParticleBuffer +{ + /** + * @param {WebGLRenderingContext} gl - The rendering context. + * @param {object} properties - The properties to upload. + * @param {boolean[]} dynamicPropertyFlags - Flags for which properties are dynamic. + * @param {number} size - The size of the batch. + */ + constructor(gl, properties, dynamicPropertyFlags, size) + { + /** + * The current WebGL drawing context. + * + * @member {WebGLRenderingContext} + */ + this.gl = gl; + + /** + * The number of particles the buffer can hold + * + * @member {number} + */ + this.size = size; + + /** + * A list of the properties that are dynamic. + * + * @member {object[]} + */ + this.dynamicProperties = []; + + /** + * A list of the properties that are static. + * + * @member {object[]} + */ + this.staticProperties = []; + + for (let i = 0; i < properties.length; ++i) + { + let property = properties[i]; + + // Make copy of properties object so that when we edit the offset it doesn't + // change all other instances of the object literal + property = { + attribute: property.attribute, + size: property.size, + uploadFunction: property.uploadFunction, + unsignedByte: property.unsignedByte, + offset: property.offset, + }; + + if (dynamicPropertyFlags[i]) + { + this.dynamicProperties.push(property); + } + else + { + this.staticProperties.push(property); + } + } + + this.staticStride = 0; + this.staticBuffer = null; + this.staticData = null; + this.staticDataUint32 = null; + + this.dynamicStride = 0; + this.dynamicBuffer = null; + this.dynamicData = null; + this.dynamicDataUint32 = null; + + this.initBuffers(); + } + + /** + * Sets up the renderer context and necessary buffers. + * + * @private + */ + initBuffers() + { + const gl = this.gl; + let dynamicOffset = 0; + + /** + * Holds the indices of the geometry (quads) to draw + * + * @member {Uint16Array} + */ + this.indices = createIndicesForQuads(this.size); + this.indexBuffer = GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); + + this.dynamicStride = 0; + + for (let i = 0; i < this.dynamicProperties.length; ++i) + { + const property = this.dynamicProperties[i]; + + property.offset = dynamicOffset; + dynamicOffset += property.size; + this.dynamicStride += property.size; + } + + this.dynamicData = new Float32Array(this.size * this.dynamicStride * 4); + this.dynamicBuffer = GLBuffer.createVertexBuffer(gl, this.dynamicData, gl.STREAM_DRAW); + + // static // + let staticOffset = 0; + + this.staticStride = 0; + + for (let i = 0; i < this.staticProperties.length; ++i) + { + const property = this.staticProperties[i]; + + property.offset = staticOffset; + staticOffset += property.size; + this.staticStride += property.size; + } + + this.staticData = new Float32Array(this.size * this.staticStride * 4); + this.staticBuffer = GLBuffer.createVertexBuffer(gl, this.staticData, gl.STATIC_DRAW); + + // this.vao = new VertexArrayObject(gl) + // .addIndex(this.indexBuffer); + + for (let i = 0; i < this.dynamicProperties.length; ++i) + { + const property = this.dynamicProperties[i]; + + if (property.unsignedByte) + { + this.vao.addAttribute( + this.dynamicBuffer, + property.attribute, + gl.UNSIGNED_BYTE, + true, + this.dynamicStride * 4, + property.offset * 4 + ); + } + else + { + this.vao.addAttribute( + this.dynamicBuffer, + property.attribute, + gl.FLOAT, + false, + this.dynamicStride * 4, + property.offset * 4 + ); + } + } + + for (let i = 0; i < this.staticProperties.length; ++i) + { + const property = this.staticProperties[i]; + + if (property.unsignedByte) + { + this.vao.addAttribute( + this.staticBuffer, + property.attribute, + gl.UNSIGNED_BYTE, + true, + this.staticStride * 4, + property.offset * 4 + ); + } + else + { + this.vao.addAttribute( + this.staticBuffer, + property.attribute, + gl.FLOAT, + false, + this.staticStride * 4, + property.offset * 4 + ); + } + } + } + + /** + * Uploads the dynamic properties. + * + * @param {PIXI.DisplayObject[]} children - The children to upload. + * @param {number} startIndex - The index to start at. + * @param {number} amount - The number to upload. + */ + uploadDynamic(children, startIndex, amount) + { + for (let i = 0; i < this.dynamicProperties.length; i++) + { + const property = this.dynamicProperties[i]; + + property.uploadFunction(children, startIndex, amount, + property.unsignedByte ? this.dynamicDataUint32 : this.dynamicData, + this.dynamicStride, property.offset); + } + + this.dynamicBuffer.upload(); + } + + /** + * Uploads the static properties. + * + * @param {PIXI.DisplayObject[]} children - The children to upload. + * @param {number} startIndex - The index to start at. + * @param {number} amount - The number to upload. + */ + uploadStatic(children, startIndex, amount) + { + for (let i = 0; i < this.staticProperties.length; i++) + { + const property = this.staticProperties[i]; + + property.uploadFunction(children, startIndex, amount, + property.unsignedByte ? this.staticDataUint32 : this.staticData, + this.staticStride, property.offset); + } + + this.staticBuffer.upload(); + } + + /** + * Destroys the ParticleBuffer. + * + */ + destroy() + { + this.dynamicProperties = null; + this.dynamicBuffer.destroy(); + this.dynamicBuffer = null; + this.dynamicData = null; + this.dynamicDataUint32 = null; + + this.staticProperties = null; + this.staticBuffer.destroy(); + this.staticBuffer = null; + this.staticData = null; + this.staticDataUint32 = null; + } +} diff --git a/packages/particles/src/ParticleContainer.js b/packages/particles/src/ParticleContainer.js index c8ade96..9d25c5b 100644 --- a/packages/particles/src/ParticleContainer.js +++ b/packages/particles/src/ParticleContainer.js @@ -25,7 +25,7 @@ * * @class * @extends PIXI.Container - * @memberof PIXI.particles + * @memberof PIXI */ export default class ParticleContainer extends Container { diff --git a/packages/particles/src/ParticleRenderer.js b/packages/particles/src/ParticleRenderer.js new file mode 100644 index 0000000..5464211 --- /dev/null +++ b/packages/particles/src/ParticleRenderer.js @@ -0,0 +1,455 @@ +import { ObjectRenderer } from '@pixi/core'; +import { correctBlendMode, premultiplyRgba, premultiplyTint } from '@pixi/utils'; +import { Matrix } from '@pixi/math'; +import ParticleShader from './ParticleShader'; +import ParticleBuffer from './ParticleBuffer'; + +/** + * @author Mat Groves + * + * Big thanks to the very clever Matt DesLauriers https://github.com/mattdesl/ + * for creating the original PixiJS version! + * Also a thanks to https://github.com/bchevalier for tweaking the tint and alpha so that they now + * share 4 bytes on the vertex buffer + * + * Heavily inspired by LibGDX's ParticleRenderer: + * https://github.com/libgdx/libgdx/blob/master/gdx/src/com/badlogic/gdx/graphics/g2d/ParticleRenderer.java + */ + +/** + * + * @class + * @memberof PIXI + */ +export default class ParticleRenderer extends ObjectRenderer +{ + /** + * @param {PIXI.Renderer} renderer - The renderer this sprite batch works for. + */ + constructor(renderer) + { + super(renderer); + + // 65535 is max vertex index in the index buffer (see ParticleRenderer) + // so max number of particles is 65536 / 4 = 16384 + // and max number of element in the index buffer is 16384 * 6 = 98304 + // Creating a full index buffer, overhead is 98304 * 2 = 196Ko + // let numIndices = 98304; + + /** + * The default shader that is used if a sprite doesn't have a more specific one. + * + * @member {PIXI.Shader} + */ + this.shader = null; + + this.indexBuffer = null; + + this.properties = null; + + this.tempMatrix = new Matrix(); + + this.CONTEXT_UID = 0; + } + + /** + * When there is a WebGL context change + * + * @private + */ + onContextChange() + { + const gl = this.renderer.gl; + + this.CONTEXT_UID = this.renderer.CONTEXT_UID; + + // setup default shader + this.shader = new ParticleShader(gl); + + this.properties = [ + // verticesData + { + attribute: this.shader.attributes.aVertexPosition, + size: 2, + uploadFunction: this.uploadVertices, + offset: 0, + }, + // positionData + { + attribute: this.shader.attributes.aPositionCoord, + size: 2, + uploadFunction: this.uploadPosition, + offset: 0, + }, + // rotationData + { + attribute: this.shader.attributes.aRotation, + size: 1, + uploadFunction: this.uploadRotation, + offset: 0, + }, + // uvsData + { + attribute: this.shader.attributes.aTextureCoord, + size: 2, + uploadFunction: this.uploadUvs, + offset: 0, + }, + // tintData + { + attribute: this.shader.attributes.aColor, + size: 1, + unsignedByte: true, + uploadFunction: this.uploadTint, + offset: 0, + }, + ]; + } + + /** + * Starts a new particle batch. + * + */ + start() + { + this.renderer._bindGLShader(this.shader); + } + + /** + * Renders the particle container object. + * + * @param {PIXI.ParticleContainer} container - The container to render using this ParticleRenderer + */ + render(container) + { + const children = container.children; + const maxSize = container._maxSize; + const batchSize = container._batchSize; + const renderer = this.renderer; + let totalChildren = children.length; + + if (totalChildren === 0) + { + return; + } + else if (totalChildren > maxSize) + { + totalChildren = maxSize; + } + + let buffers = container._glBuffers[renderer.CONTEXT_UID]; + + if (!buffers) + { + buffers = container._glBuffers[renderer.CONTEXT_UID] = this.generateBuffers(container); + } + + const baseTexture = children[0]._texture.baseTexture; + + // if the uvs have not updated then no point rendering just yet! + this.renderer.setBlendMode(correctBlendMode(container.blendMode, baseTexture.premultiplyAlpha)); + + const gl = renderer.gl; + + const m = container.worldTransform.copy(this.tempMatrix); + + m.prepend(renderer._activeRenderTarget.projectionMatrix); + + this.shader.uniforms.projectionMatrix = m.toArray(true); + + this.shader.uniforms.uColor = premultiplyRgba(container.tintRgb, + container.worldAlpha, this.shader.uniforms.uColor, baseTexture.premultiplyAlpha); + + // make sure the texture is bound.. + this.shader.uniforms.uSampler = renderer.bindTexture(baseTexture); + + // now lets upload and render the buffers.. + for (let i = 0, j = 0; i < totalChildren; i += batchSize, j += 1) + { + let amount = (totalChildren - i); + + if (amount > batchSize) + { + amount = batchSize; + } + + if (j >= buffers.length) + { + if (!container.autoResize) + { + break; + } + buffers.push(this._generateOneMoreBuffer(container)); + } + + const buffer = buffers[j]; + + // we always upload the dynamic + buffer.uploadDynamic(children, i, amount); + + // we only upload the static content when we have to! + if (container._bufferToUpdate === j) + { + buffer.uploadStatic(children, i, amount); + container._bufferToUpdate = j + 1; + } + + // bind the buffer + renderer.bindVao(buffer.vao); + buffer.vao.draw(gl.TRIANGLES, amount * 6); + } + } + + /** + * Creates one particle buffer for each child in the container we want to render and updates internal properties + * + * @param {PIXI.ParticleContainer} container - The container to render using this ParticleRenderer + * @return {PIXI.ParticleBuffer[]} The buffers + */ + generateBuffers(container) + { + const gl = this.renderer.gl; + const buffers = []; + const size = container._maxSize; + const batchSize = container._batchSize; + const dynamicPropertyFlags = container._properties; + + for (let i = 0; i < size; i += batchSize) + { + buffers.push(new ParticleBuffer(gl, this.properties, dynamicPropertyFlags, batchSize)); + } + + return buffers; + } + + /** + * Creates one more particle buffer, because container has autoResize feature + * + * @param {PIXI.ParticleContainer} container - The container to render using this ParticleRenderer + * @return {PIXI.ParticleBuffer} generated buffer + * @private + */ + _generateOneMoreBuffer(container) + { + const gl = this.renderer.gl; + const batchSize = container._batchSize; + const dynamicPropertyFlags = container._properties; + + return new ParticleBuffer(gl, this.properties, dynamicPropertyFlags, batchSize); + } + + /** + * Uploads the verticies. + * + * @param {PIXI.DisplayObject[]} children - the array of display objects to render + * @param {number} startIndex - the index to start from in the children array + * @param {number} amount - the amount of children that will have their vertices uploaded + * @param {number[]} array - The vertices to upload. + * @param {number} stride - Stride to use for iteration. + * @param {number} offset - Offset to start at. + */ + uploadVertices(children, startIndex, amount, array, stride, offset) + { + let w0 = 0; + let w1 = 0; + let h0 = 0; + let h1 = 0; + + for (let i = 0; i < amount; ++i) + { + const sprite = children[startIndex + i]; + const texture = sprite._texture; + const sx = sprite.scale.x; + const sy = sprite.scale.y; + const trim = texture.trim; + const orig = texture.orig; + + if (trim) + { + // if the sprite is trimmed and is not a tilingsprite then we need to add the + // extra space before transforming the sprite coords.. + w1 = trim.x - (sprite.anchor.x * orig.width); + w0 = w1 + trim.width; + + h1 = trim.y - (sprite.anchor.y * orig.height); + h0 = h1 + trim.height; + } + else + { + w0 = (orig.width) * (1 - sprite.anchor.x); + w1 = (orig.width) * -sprite.anchor.x; + + h0 = orig.height * (1 - sprite.anchor.y); + h1 = orig.height * -sprite.anchor.y; + } + + array[offset] = w1 * sx; + array[offset + 1] = h1 * sy; + + array[offset + stride] = w0 * sx; + array[offset + stride + 1] = h1 * sy; + + array[offset + (stride * 2)] = w0 * sx; + array[offset + (stride * 2) + 1] = h0 * sy; + + array[offset + (stride * 3)] = w1 * sx; + array[offset + (stride * 3) + 1] = h0 * sy; + + offset += stride * 4; + } + } + + /** + * + * @param {PIXI.DisplayObject[]} children - the array of display objects to render + * @param {number} startIndex - the index to start from in the children array + * @param {number} amount - the amount of children that will have their positions uploaded + * @param {number[]} array - The vertices to upload. + * @param {number} stride - Stride to use for iteration. + * @param {number} offset - Offset to start at. + */ + uploadPosition(children, startIndex, amount, array, stride, offset) + { + for (let i = 0; i < amount; i++) + { + const spritePosition = children[startIndex + i].position; + + array[offset] = spritePosition.x; + array[offset + 1] = spritePosition.y; + + array[offset + stride] = spritePosition.x; + array[offset + stride + 1] = spritePosition.y; + + array[offset + (stride * 2)] = spritePosition.x; + array[offset + (stride * 2) + 1] = spritePosition.y; + + array[offset + (stride * 3)] = spritePosition.x; + array[offset + (stride * 3) + 1] = spritePosition.y; + + offset += stride * 4; + } + } + + /** + * + * @param {PIXI.DisplayObject[]} children - the array of display objects to render + * @param {number} startIndex - the index to start from in the children array + * @param {number} amount - the amount of children that will have their rotation uploaded + * @param {number[]} array - The vertices to upload. + * @param {number} stride - Stride to use for iteration. + * @param {number} offset - Offset to start at. + */ + uploadRotation(children, startIndex, amount, array, stride, offset) + { + for (let i = 0; i < amount; i++) + { + const spriteRotation = children[startIndex + i].rotation; + + array[offset] = spriteRotation; + array[offset + stride] = spriteRotation; + array[offset + (stride * 2)] = spriteRotation; + array[offset + (stride * 3)] = spriteRotation; + + offset += stride * 4; + } + } + + /** + * + * @param {PIXI.DisplayObject[]} children - the array of display objects to render + * @param {number} startIndex - the index to start from in the children array + * @param {number} amount - the amount of children that will have their rotation uploaded + * @param {number[]} array - The vertices to upload. + * @param {number} stride - Stride to use for iteration. + * @param {number} offset - Offset to start at. + */ + uploadUvs(children, startIndex, amount, array, stride, offset) + { + for (let i = 0; i < amount; ++i) + { + const textureUvs = children[startIndex + i]._texture._uvs; + + if (textureUvs) + { + array[offset] = textureUvs.x0; + array[offset + 1] = textureUvs.y0; + + array[offset + stride] = textureUvs.x1; + array[offset + stride + 1] = textureUvs.y1; + + array[offset + (stride * 2)] = textureUvs.x2; + array[offset + (stride * 2) + 1] = textureUvs.y2; + + array[offset + (stride * 3)] = textureUvs.x3; + array[offset + (stride * 3) + 1] = textureUvs.y3; + + offset += stride * 4; + } + else + { + // TODO you know this can be easier! + array[offset] = 0; + array[offset + 1] = 0; + + array[offset + stride] = 0; + array[offset + stride + 1] = 0; + + array[offset + (stride * 2)] = 0; + array[offset + (stride * 2) + 1] = 0; + + array[offset + (stride * 3)] = 0; + array[offset + (stride * 3) + 1] = 0; + + offset += stride * 4; + } + } + } + + /** + * + * @param {PIXI.DisplayObject[]} children - the array of display objects to render + * @param {number} startIndex - the index to start from in the children array + * @param {number} amount - the amount of children that will have their rotation uploaded + * @param {number[]} array - The vertices to upload. + * @param {number} stride - Stride to use for iteration. + * @param {number} offset - Offset to start at. + */ + uploadTint(children, startIndex, amount, array, stride, offset) + { + for (let i = 0; i < amount; ++i) + { + const sprite = children[startIndex + i]; + const premultiplied = sprite._texture.baseTexture.premultiplyAlpha; + const alpha = sprite.alpha; + // we dont call extra function if alpha is 1.0, that's faster + const argb = alpha < 1.0 && premultiplied ? premultiplyTint(sprite._tintRGB, alpha) + : sprite._tintRGB + (alpha * 255 << 24); + + array[offset] = argb; + array[offset + stride] = argb; + array[offset + (stride * 2)] = argb; + array[offset + (stride * 3)] = argb; + + offset += stride * 4; + } + } + + /** + * Destroys the ParticleRenderer. + * + */ + destroy() + { + if (this.renderer.gl) + { + this.renderer.gl.deleteBuffer(this.indexBuffer); + } + + super.destroy(); + + this.shader.destroy(); + + this.indices = null; + this.tempMatrix = null; + } +} diff --git a/packages/particles/src/ParticleShader.js b/packages/particles/src/ParticleShader.js new file mode 100644 index 0000000..069afe6 --- /dev/null +++ b/packages/particles/src/ParticleShader.js @@ -0,0 +1,21 @@ +import { GLShader } from 'pixi-gl-core'; +import { PRECISION } from '@pixi/constants'; +import vertex from './particles.vert'; +import fragment from './particles.frag'; + +/** + * @class + * @extends PIXI.Shader + * @memberof PIXI + * @private + */ +export default class ParticleShader extends GLShader +{ + /** + * @param {PIXI.Shader} gl - The webgl shader manager this shader works for. + */ + constructor(gl) + { + super(gl, vertex, fragment, PRECISION.DEFAULT); + } +} diff --git a/packages/particles/src/index.js b/packages/particles/src/index.js index 629d599..d315119 100644 --- a/packages/particles/src/index.js +++ b/packages/particles/src/index.js @@ -1,5 +1,2 @@ -/** - * @namespace PIXI.particles - */ export { default as ParticleContainer } from './ParticleContainer'; -export { default as ParticleRenderer } from './webgl/ParticleRenderer'; +export { default as ParticleRenderer } from './ParticleRenderer'; diff --git a/packages/particles/src/particles.frag b/packages/particles/src/particles.frag new file mode 100644 index 0000000..a725046 --- /dev/null +++ b/packages/particles/src/particles.frag @@ -0,0 +1,9 @@ +varying vec2 vTextureCoord; +varying vec4 vColor; + +uniform sampler2D uSampler; + +void main(void){ + vec4 color = texture2D(uSampler, vTextureCoord) * vColor; + gl_FragColor = color; +} \ No newline at end of file diff --git a/packages/particles/src/particles.vert b/packages/particles/src/particles.vert new file mode 100644 index 0000000..bd5401c --- /dev/null +++ b/packages/particles/src/particles.vert @@ -0,0 +1,26 @@ +attribute vec2 aVertexPosition; +attribute vec2 aTextureCoord; +attribute vec4 aColor; + +attribute vec2 aPositionCoord; +attribute vec2 aScale; +attribute float aRotation; + +uniform mat3 projectionMatrix; +uniform vec4 uColor; + +varying vec2 vTextureCoord; +varying vec4 vColor; + +void main(void){ + float x = (aVertexPosition.x) * cos(aRotation) - (aVertexPosition.y) * sin(aRotation); + float y = (aVertexPosition.x) * sin(aRotation) + (aVertexPosition.y) * cos(aRotation); + + vec2 v = vec2(x, y); + v = v + aPositionCoord; + + gl_Position = vec4((projectionMatrix * vec3(v, 1.0)).xy, 0.0, 1.0); + + vTextureCoord = aTextureCoord; + vColor = aColor * uColor; +} \ No newline at end of file diff --git a/packages/particles/src/webgl/ParticleBuffer.js b/packages/particles/src/webgl/ParticleBuffer.js deleted file mode 100644 index 6306bda..0000000 --- a/packages/particles/src/webgl/ParticleBuffer.js +++ /dev/null @@ -1,268 +0,0 @@ -// import { VertexArrayObject } from 'pixi-gl-core'; -import { createIndicesForQuads } from '@pixi/utils'; -import { GLBuffer } from 'pixi-gl-core'; - -/** - * @author Mat Groves - * - * Big thanks to the very clever Matt DesLauriers https://github.com/mattdesl/ - * for creating the original PixiJS version! - * Also a thanks to https://github.com/bchevalier for tweaking the tint and alpha so that - * they now share 4 bytes on the vertex buffer - * - * Heavily inspired by LibGDX's ParticleBuffer: - * https://github.com/libgdx/libgdx/blob/master/gdx/src/com/badlogic/gdx/graphics/g2d/ParticleBuffer.java - */ - -/** - * The particle buffer manages the static and dynamic buffers for a particle container. - * - * @class - * @private - * @memberof PIXI - */ -export default class ParticleBuffer -{ - /** - * @param {WebGLRenderingContext} gl - The rendering context. - * @param {object} properties - The properties to upload. - * @param {boolean[]} dynamicPropertyFlags - Flags for which properties are dynamic. - * @param {number} size - The size of the batch. - */ - constructor(gl, properties, dynamicPropertyFlags, size) - { - /** - * The current WebGL drawing context. - * - * @member {WebGLRenderingContext} - */ - this.gl = gl; - - /** - * The number of particles the buffer can hold - * - * @member {number} - */ - this.size = size; - - /** - * A list of the properties that are dynamic. - * - * @member {object[]} - */ - this.dynamicProperties = []; - - /** - * A list of the properties that are static. - * - * @member {object[]} - */ - this.staticProperties = []; - - for (let i = 0; i < properties.length; ++i) - { - let property = properties[i]; - - // Make copy of properties object so that when we edit the offset it doesn't - // change all other instances of the object literal - property = { - attribute: property.attribute, - size: property.size, - uploadFunction: property.uploadFunction, - unsignedByte: property.unsignedByte, - offset: property.offset, - }; - - if (dynamicPropertyFlags[i]) - { - this.dynamicProperties.push(property); - } - else - { - this.staticProperties.push(property); - } - } - - this.staticStride = 0; - this.staticBuffer = null; - this.staticData = null; - this.staticDataUint32 = null; - - this.dynamicStride = 0; - this.dynamicBuffer = null; - this.dynamicData = null; - this.dynamicDataUint32 = null; - - this.initBuffers(); - } - - /** - * Sets up the renderer context and necessary buffers. - * - * @private - */ - initBuffers() - { - const gl = this.gl; - let dynamicOffset = 0; - - /** - * Holds the indices of the geometry (quads) to draw - * - * @member {Uint16Array} - */ - this.indices = createIndicesForQuads(this.size); - this.indexBuffer = GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); - - this.dynamicStride = 0; - - for (let i = 0; i < this.dynamicProperties.length; ++i) - { - const property = this.dynamicProperties[i]; - - property.offset = dynamicOffset; - dynamicOffset += property.size; - this.dynamicStride += property.size; - } - - this.dynamicData = new Float32Array(this.size * this.dynamicStride * 4); - this.dynamicBuffer = GLBuffer.createVertexBuffer(gl, this.dynamicData, gl.STREAM_DRAW); - - // static // - let staticOffset = 0; - - this.staticStride = 0; - - for (let i = 0; i < this.staticProperties.length; ++i) - { - const property = this.staticProperties[i]; - - property.offset = staticOffset; - staticOffset += property.size; - this.staticStride += property.size; - } - - this.staticData = new Float32Array(this.size * this.staticStride * 4); - this.staticBuffer = GLBuffer.createVertexBuffer(gl, this.staticData, gl.STATIC_DRAW); - - // this.vao = new VertexArrayObject(gl) - // .addIndex(this.indexBuffer); - - for (let i = 0; i < this.dynamicProperties.length; ++i) - { - const property = this.dynamicProperties[i]; - - if (property.unsignedByte) - { - this.vao.addAttribute( - this.dynamicBuffer, - property.attribute, - gl.UNSIGNED_BYTE, - true, - this.dynamicStride * 4, - property.offset * 4 - ); - } - else - { - this.vao.addAttribute( - this.dynamicBuffer, - property.attribute, - gl.FLOAT, - false, - this.dynamicStride * 4, - property.offset * 4 - ); - } - } - - for (let i = 0; i < this.staticProperties.length; ++i) - { - const property = this.staticProperties[i]; - - if (property.unsignedByte) - { - this.vao.addAttribute( - this.staticBuffer, - property.attribute, - gl.UNSIGNED_BYTE, - true, - this.staticStride * 4, - property.offset * 4 - ); - } - else - { - this.vao.addAttribute( - this.staticBuffer, - property.attribute, - gl.FLOAT, - false, - this.staticStride * 4, - property.offset * 4 - ); - } - } - } - - /** - * Uploads the dynamic properties. - * - * @param {PIXI.DisplayObject[]} children - The children to upload. - * @param {number} startIndex - The index to start at. - * @param {number} amount - The number to upload. - */ - uploadDynamic(children, startIndex, amount) - { - for (let i = 0; i < this.dynamicProperties.length; i++) - { - const property = this.dynamicProperties[i]; - - property.uploadFunction(children, startIndex, amount, - property.unsignedByte ? this.dynamicDataUint32 : this.dynamicData, - this.dynamicStride, property.offset); - } - - this.dynamicBuffer.upload(); - } - - /** - * Uploads the static properties. - * - * @param {PIXI.DisplayObject[]} children - The children to upload. - * @param {number} startIndex - The index to start at. - * @param {number} amount - The number to upload. - */ - uploadStatic(children, startIndex, amount) - { - for (let i = 0; i < this.staticProperties.length; i++) - { - const property = this.staticProperties[i]; - - property.uploadFunction(children, startIndex, amount, - property.unsignedByte ? this.staticDataUint32 : this.staticData, - this.staticStride, property.offset); - } - - this.staticBuffer.upload(); - } - - /** - * Destroys the ParticleBuffer. - * - */ - destroy() - { - this.dynamicProperties = null; - this.dynamicBuffer.destroy(); - this.dynamicBuffer = null; - this.dynamicData = null; - this.dynamicDataUint32 = null; - - this.staticProperties = null; - this.staticBuffer.destroy(); - this.staticBuffer = null; - this.staticData = null; - this.staticDataUint32 = null; - } -} diff --git a/packages/particles/src/webgl/ParticleRenderer.js b/packages/particles/src/webgl/ParticleRenderer.js deleted file mode 100644 index 8e50994..0000000 --- a/packages/particles/src/webgl/ParticleRenderer.js +++ /dev/null @@ -1,458 +0,0 @@ -import { ObjectRenderer, Renderer } from '@pixi/core'; -import { correctBlendMode, premultiplyRgba, premultiplyTint } from '@pixi/utils'; -import { Matrix } from '@pixi/math'; -import ParticleShader from './ParticleShader'; -import ParticleBuffer from './ParticleBuffer'; - -/** - * @author Mat Groves - * - * Big thanks to the very clever Matt DesLauriers https://github.com/mattdesl/ - * for creating the original PixiJS version! - * Also a thanks to https://github.com/bchevalier for tweaking the tint and alpha so that they now - * share 4 bytes on the vertex buffer - * - * Heavily inspired by LibGDX's ParticleRenderer: - * https://github.com/libgdx/libgdx/blob/master/gdx/src/com/badlogic/gdx/graphics/g2d/ParticleRenderer.java - */ - -/** - * - * @class - * @private - * @memberof PIXI - */ -export default class ParticleRenderer extends ObjectRenderer -{ - /** - * @param {PIXI.Renderer} renderer - The renderer this sprite batch works for. - */ - constructor(renderer) - { - super(renderer); - - // 65535 is max vertex index in the index buffer (see ParticleRenderer) - // so max number of particles is 65536 / 4 = 16384 - // and max number of element in the index buffer is 16384 * 6 = 98304 - // Creating a full index buffer, overhead is 98304 * 2 = 196Ko - // let numIndices = 98304; - - /** - * The default shader that is used if a sprite doesn't have a more specific one. - * - * @member {PIXI.Shader} - */ - this.shader = null; - - this.indexBuffer = null; - - this.properties = null; - - this.tempMatrix = new Matrix(); - - this.CONTEXT_UID = 0; - } - - /** - * When there is a WebGL context change - * - * @private - */ - onContextChange() - { - const gl = this.renderer.gl; - - this.CONTEXT_UID = this.renderer.CONTEXT_UID; - - // setup default shader - this.shader = new ParticleShader(gl); - - this.properties = [ - // verticesData - { - attribute: this.shader.attributes.aVertexPosition, - size: 2, - uploadFunction: this.uploadVertices, - offset: 0, - }, - // positionData - { - attribute: this.shader.attributes.aPositionCoord, - size: 2, - uploadFunction: this.uploadPosition, - offset: 0, - }, - // rotationData - { - attribute: this.shader.attributes.aRotation, - size: 1, - uploadFunction: this.uploadRotation, - offset: 0, - }, - // uvsData - { - attribute: this.shader.attributes.aTextureCoord, - size: 2, - uploadFunction: this.uploadUvs, - offset: 0, - }, - // tintData - { - attribute: this.shader.attributes.aColor, - size: 1, - unsignedByte: true, - uploadFunction: this.uploadTint, - offset: 0, - }, - ]; - } - - /** - * Starts a new particle batch. - * - */ - start() - { - this.renderer._bindGLShader(this.shader); - } - - /** - * Renders the particle container object. - * - * @param {PIXI.ParticleContainer} container - The container to render using this ParticleRenderer - */ - render(container) - { - const children = container.children; - const maxSize = container._maxSize; - const batchSize = container._batchSize; - const renderer = this.renderer; - let totalChildren = children.length; - - if (totalChildren === 0) - { - return; - } - else if (totalChildren > maxSize) - { - totalChildren = maxSize; - } - - let buffers = container._glBuffers[renderer.CONTEXT_UID]; - - if (!buffers) - { - buffers = container._glBuffers[renderer.CONTEXT_UID] = this.generateBuffers(container); - } - - const baseTexture = children[0]._texture.baseTexture; - - // if the uvs have not updated then no point rendering just yet! - this.renderer.setBlendMode(correctBlendMode(container.blendMode, baseTexture.premultiplyAlpha)); - - const gl = renderer.gl; - - const m = container.worldTransform.copy(this.tempMatrix); - - m.prepend(renderer._activeRenderTarget.projectionMatrix); - - this.shader.uniforms.projectionMatrix = m.toArray(true); - - this.shader.uniforms.uColor = premultiplyRgba(container.tintRgb, - container.worldAlpha, this.shader.uniforms.uColor, baseTexture.premultiplyAlpha); - - // make sure the texture is bound.. - this.shader.uniforms.uSampler = renderer.bindTexture(baseTexture); - - // now lets upload and render the buffers.. - for (let i = 0, j = 0; i < totalChildren; i += batchSize, j += 1) - { - let amount = (totalChildren - i); - - if (amount > batchSize) - { - amount = batchSize; - } - - if (j >= buffers.length) - { - if (!container.autoResize) - { - break; - } - buffers.push(this._generateOneMoreBuffer(container)); - } - - const buffer = buffers[j]; - - // we always upload the dynamic - buffer.uploadDynamic(children, i, amount); - - // we only upload the static content when we have to! - if (container._bufferToUpdate === j) - { - buffer.uploadStatic(children, i, amount); - container._bufferToUpdate = j + 1; - } - - // bind the buffer - renderer.bindVao(buffer.vao); - buffer.vao.draw(gl.TRIANGLES, amount * 6); - } - } - - /** - * Creates one particle buffer for each child in the container we want to render and updates internal properties - * - * @param {PIXI.ParticleContainer} container - The container to render using this ParticleRenderer - * @return {PIXI.ParticleBuffer[]} The buffers - */ - generateBuffers(container) - { - const gl = this.renderer.gl; - const buffers = []; - const size = container._maxSize; - const batchSize = container._batchSize; - const dynamicPropertyFlags = container._properties; - - for (let i = 0; i < size; i += batchSize) - { - buffers.push(new ParticleBuffer(gl, this.properties, dynamicPropertyFlags, batchSize)); - } - - return buffers; - } - - /** - * Creates one more particle buffer, because container has autoResize feature - * - * @param {PIXI.ParticleContainer} container - The container to render using this ParticleRenderer - * @return {PIXI.ParticleBuffer} generated buffer - * @private - */ - _generateOneMoreBuffer(container) - { - const gl = this.renderer.gl; - const batchSize = container._batchSize; - const dynamicPropertyFlags = container._properties; - - return new ParticleBuffer(gl, this.properties, dynamicPropertyFlags, batchSize); - } - - /** - * Uploads the verticies. - * - * @param {PIXI.DisplayObject[]} children - the array of display objects to render - * @param {number} startIndex - the index to start from in the children array - * @param {number} amount - the amount of children that will have their vertices uploaded - * @param {number[]} array - The vertices to upload. - * @param {number} stride - Stride to use for iteration. - * @param {number} offset - Offset to start at. - */ - uploadVertices(children, startIndex, amount, array, stride, offset) - { - let w0 = 0; - let w1 = 0; - let h0 = 0; - let h1 = 0; - - for (let i = 0; i < amount; ++i) - { - const sprite = children[startIndex + i]; - const texture = sprite._texture; - const sx = sprite.scale.x; - const sy = sprite.scale.y; - const trim = texture.trim; - const orig = texture.orig; - - if (trim) - { - // if the sprite is trimmed and is not a tilingsprite then we need to add the - // extra space before transforming the sprite coords.. - w1 = trim.x - (sprite.anchor.x * orig.width); - w0 = w1 + trim.width; - - h1 = trim.y - (sprite.anchor.y * orig.height); - h0 = h1 + trim.height; - } - else - { - w0 = (orig.width) * (1 - sprite.anchor.x); - w1 = (orig.width) * -sprite.anchor.x; - - h0 = orig.height * (1 - sprite.anchor.y); - h1 = orig.height * -sprite.anchor.y; - } - - array[offset] = w1 * sx; - array[offset + 1] = h1 * sy; - - array[offset + stride] = w0 * sx; - array[offset + stride + 1] = h1 * sy; - - array[offset + (stride * 2)] = w0 * sx; - array[offset + (stride * 2) + 1] = h0 * sy; - - array[offset + (stride * 3)] = w1 * sx; - array[offset + (stride * 3) + 1] = h0 * sy; - - offset += stride * 4; - } - } - - /** - * - * @param {PIXI.DisplayObject[]} children - the array of display objects to render - * @param {number} startIndex - the index to start from in the children array - * @param {number} amount - the amount of children that will have their positions uploaded - * @param {number[]} array - The vertices to upload. - * @param {number} stride - Stride to use for iteration. - * @param {number} offset - Offset to start at. - */ - uploadPosition(children, startIndex, amount, array, stride, offset) - { - for (let i = 0; i < amount; i++) - { - const spritePosition = children[startIndex + i].position; - - array[offset] = spritePosition.x; - array[offset + 1] = spritePosition.y; - - array[offset + stride] = spritePosition.x; - array[offset + stride + 1] = spritePosition.y; - - array[offset + (stride * 2)] = spritePosition.x; - array[offset + (stride * 2) + 1] = spritePosition.y; - - array[offset + (stride * 3)] = spritePosition.x; - array[offset + (stride * 3) + 1] = spritePosition.y; - - offset += stride * 4; - } - } - - /** - * - * @param {PIXI.DisplayObject[]} children - the array of display objects to render - * @param {number} startIndex - the index to start from in the children array - * @param {number} amount - the amount of children that will have their rotation uploaded - * @param {number[]} array - The vertices to upload. - * @param {number} stride - Stride to use for iteration. - * @param {number} offset - Offset to start at. - */ - uploadRotation(children, startIndex, amount, array, stride, offset) - { - for (let i = 0; i < amount; i++) - { - const spriteRotation = children[startIndex + i].rotation; - - array[offset] = spriteRotation; - array[offset + stride] = spriteRotation; - array[offset + (stride * 2)] = spriteRotation; - array[offset + (stride * 3)] = spriteRotation; - - offset += stride * 4; - } - } - - /** - * - * @param {PIXI.DisplayObject[]} children - the array of display objects to render - * @param {number} startIndex - the index to start from in the children array - * @param {number} amount - the amount of children that will have their rotation uploaded - * @param {number[]} array - The vertices to upload. - * @param {number} stride - Stride to use for iteration. - * @param {number} offset - Offset to start at. - */ - uploadUvs(children, startIndex, amount, array, stride, offset) - { - for (let i = 0; i < amount; ++i) - { - const textureUvs = children[startIndex + i]._texture._uvs; - - if (textureUvs) - { - array[offset] = textureUvs.x0; - array[offset + 1] = textureUvs.y0; - - array[offset + stride] = textureUvs.x1; - array[offset + stride + 1] = textureUvs.y1; - - array[offset + (stride * 2)] = textureUvs.x2; - array[offset + (stride * 2) + 1] = textureUvs.y2; - - array[offset + (stride * 3)] = textureUvs.x3; - array[offset + (stride * 3) + 1] = textureUvs.y3; - - offset += stride * 4; - } - else - { - // TODO you know this can be easier! - array[offset] = 0; - array[offset + 1] = 0; - - array[offset + stride] = 0; - array[offset + stride + 1] = 0; - - array[offset + (stride * 2)] = 0; - array[offset + (stride * 2) + 1] = 0; - - array[offset + (stride * 3)] = 0; - array[offset + (stride * 3) + 1] = 0; - - offset += stride * 4; - } - } - } - - /** - * - * @param {PIXI.DisplayObject[]} children - the array of display objects to render - * @param {number} startIndex - the index to start from in the children array - * @param {number} amount - the amount of children that will have their rotation uploaded - * @param {number[]} array - The vertices to upload. - * @param {number} stride - Stride to use for iteration. - * @param {number} offset - Offset to start at. - */ - uploadTint(children, startIndex, amount, array, stride, offset) - { - for (let i = 0; i < amount; ++i) - { - const sprite = children[startIndex + i]; - const premultiplied = sprite._texture.baseTexture.premultiplyAlpha; - const alpha = sprite.alpha; - // we dont call extra function if alpha is 1.0, that's faster - const argb = alpha < 1.0 && premultiplied ? premultiplyTint(sprite._tintRGB, alpha) - : sprite._tintRGB + (alpha * 255 << 24); - - array[offset] = argb; - array[offset + stride] = argb; - array[offset + (stride * 2)] = argb; - array[offset + (stride * 3)] = argb; - - offset += stride * 4; - } - } - - /** - * Destroys the ParticleRenderer. - * - */ - destroy() - { - if (this.renderer.gl) - { - this.renderer.gl.deleteBuffer(this.indexBuffer); - } - - super.destroy(); - - this.shader.destroy(); - - this.indices = null; - this.tempMatrix = null; - } -} - -Renderer.registerPlugin('particle', ParticleRenderer); diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index e59c66a..a390a1a 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -332,6 +332,43 @@ /* * This namespace has been removed and items have been moved to * the top-level `PIXI` object. + * @namespace PIXI.particles + * @deprecated since 5.0.0 + */ + PIXI.particles = {}; + + Object.defineProperties(PIXI.particles, { + /** + * @class PIXI.particles.ParticleContainer + * @deprecated since 5.0.0 + * @see PIXI.ParticleContainer + */ + ParticleContainer: { + get() + { + warn('PIXI.particles.ParticleContainer has moved to PIXI.ParticleContainer'); + + return PIXI.ParticleContainer; + }, + }, + /** + * @class PIXI.particles.ParticleRenderer + * @deprecated since 5.0.0 + * @see PIXI.ParticleRenderer + */ + ParticleRenderer: { + get() + { + warn('PIXI.particles.ParticleRenderer has moved to PIXI.ParticleRenderer'); + + return PIXI.ParticleRenderer; + }, + }, + }); + + /* + * This namespace has been removed and items have been moved to + * the top-level `PIXI` object. * @namespace PIXI.ticker * @deprecated since 5.0.0 */ diff --git a/bundles/pixi.js/src/index.js b/bundles/pixi.js/src/index.js index 52d7cb4..fb839a0 100644 --- a/bundles/pixi.js/src/index.js +++ b/bundles/pixi.js/src/index.js @@ -9,6 +9,7 @@ import * as loaders from '@pixi/loaders'; import * as math from '@pixi/math'; import * as mesh from '@pixi/mesh'; +import * as particles from '@pixi/particles'; import * as prepare from '@pixi/prepare'; import * as sprite from '@pixi/sprite'; import * as spriteAnimated from '@pixi/sprite-animated'; @@ -37,6 +38,7 @@ core.Renderer.registerPlugin('graphics', graphics.GraphicsRenderer); core.Renderer.registerPlugin('interaction', interaction.InteractionManager); core.Renderer.registerPlugin('mesh', mesh.MeshRenderer); +core.Renderer.registerPlugin('particle', particles.ParticleRenderer); core.Renderer.registerPlugin('prepare', prepare.Prepare); core.Renderer.registerPlugin('sprite', sprite.SpriteRenderer); core.Renderer.registerPlugin('tilingSprite', spriteTiling.TilingSpriteRenderer); @@ -118,6 +120,7 @@ loaders, math, mesh, + particles, sprite, spriteAnimated, spritesheet, @@ -141,6 +144,7 @@ export * from '@pixi/loaders'; export * from '@pixi/math'; export * from '@pixi/mesh'; +export * from '@pixi/particles'; export * from '@pixi/sprite'; export * from '@pixi/spritesheet'; export * from '@pixi/sprite-animated'; diff --git a/packages/particles/README.md b/packages/particles/README.md index bca3a96..dca806d 100644 --- a/packages/particles/README.md +++ b/packages/particles/README.md @@ -9,5 +9,8 @@ ## Usage ```js -import '@pixi/particles'; +import {ParticleRenderer} from '@pixi/particles'; +import {Renderer} from '@pixi/core'; + +Renderer.registerPlugin('particle', ParticleRenderer); ``` \ No newline at end of file diff --git a/packages/particles/src/ParticleBuffer.js b/packages/particles/src/ParticleBuffer.js new file mode 100644 index 0000000..6306bda --- /dev/null +++ b/packages/particles/src/ParticleBuffer.js @@ -0,0 +1,268 @@ +// import { VertexArrayObject } from 'pixi-gl-core'; +import { createIndicesForQuads } from '@pixi/utils'; +import { GLBuffer } from 'pixi-gl-core'; + +/** + * @author Mat Groves + * + * Big thanks to the very clever Matt DesLauriers https://github.com/mattdesl/ + * for creating the original PixiJS version! + * Also a thanks to https://github.com/bchevalier for tweaking the tint and alpha so that + * they now share 4 bytes on the vertex buffer + * + * Heavily inspired by LibGDX's ParticleBuffer: + * https://github.com/libgdx/libgdx/blob/master/gdx/src/com/badlogic/gdx/graphics/g2d/ParticleBuffer.java + */ + +/** + * The particle buffer manages the static and dynamic buffers for a particle container. + * + * @class + * @private + * @memberof PIXI + */ +export default class ParticleBuffer +{ + /** + * @param {WebGLRenderingContext} gl - The rendering context. + * @param {object} properties - The properties to upload. + * @param {boolean[]} dynamicPropertyFlags - Flags for which properties are dynamic. + * @param {number} size - The size of the batch. + */ + constructor(gl, properties, dynamicPropertyFlags, size) + { + /** + * The current WebGL drawing context. + * + * @member {WebGLRenderingContext} + */ + this.gl = gl; + + /** + * The number of particles the buffer can hold + * + * @member {number} + */ + this.size = size; + + /** + * A list of the properties that are dynamic. + * + * @member {object[]} + */ + this.dynamicProperties = []; + + /** + * A list of the properties that are static. + * + * @member {object[]} + */ + this.staticProperties = []; + + for (let i = 0; i < properties.length; ++i) + { + let property = properties[i]; + + // Make copy of properties object so that when we edit the offset it doesn't + // change all other instances of the object literal + property = { + attribute: property.attribute, + size: property.size, + uploadFunction: property.uploadFunction, + unsignedByte: property.unsignedByte, + offset: property.offset, + }; + + if (dynamicPropertyFlags[i]) + { + this.dynamicProperties.push(property); + } + else + { + this.staticProperties.push(property); + } + } + + this.staticStride = 0; + this.staticBuffer = null; + this.staticData = null; + this.staticDataUint32 = null; + + this.dynamicStride = 0; + this.dynamicBuffer = null; + this.dynamicData = null; + this.dynamicDataUint32 = null; + + this.initBuffers(); + } + + /** + * Sets up the renderer context and necessary buffers. + * + * @private + */ + initBuffers() + { + const gl = this.gl; + let dynamicOffset = 0; + + /** + * Holds the indices of the geometry (quads) to draw + * + * @member {Uint16Array} + */ + this.indices = createIndicesForQuads(this.size); + this.indexBuffer = GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); + + this.dynamicStride = 0; + + for (let i = 0; i < this.dynamicProperties.length; ++i) + { + const property = this.dynamicProperties[i]; + + property.offset = dynamicOffset; + dynamicOffset += property.size; + this.dynamicStride += property.size; + } + + this.dynamicData = new Float32Array(this.size * this.dynamicStride * 4); + this.dynamicBuffer = GLBuffer.createVertexBuffer(gl, this.dynamicData, gl.STREAM_DRAW); + + // static // + let staticOffset = 0; + + this.staticStride = 0; + + for (let i = 0; i < this.staticProperties.length; ++i) + { + const property = this.staticProperties[i]; + + property.offset = staticOffset; + staticOffset += property.size; + this.staticStride += property.size; + } + + this.staticData = new Float32Array(this.size * this.staticStride * 4); + this.staticBuffer = GLBuffer.createVertexBuffer(gl, this.staticData, gl.STATIC_DRAW); + + // this.vao = new VertexArrayObject(gl) + // .addIndex(this.indexBuffer); + + for (let i = 0; i < this.dynamicProperties.length; ++i) + { + const property = this.dynamicProperties[i]; + + if (property.unsignedByte) + { + this.vao.addAttribute( + this.dynamicBuffer, + property.attribute, + gl.UNSIGNED_BYTE, + true, + this.dynamicStride * 4, + property.offset * 4 + ); + } + else + { + this.vao.addAttribute( + this.dynamicBuffer, + property.attribute, + gl.FLOAT, + false, + this.dynamicStride * 4, + property.offset * 4 + ); + } + } + + for (let i = 0; i < this.staticProperties.length; ++i) + { + const property = this.staticProperties[i]; + + if (property.unsignedByte) + { + this.vao.addAttribute( + this.staticBuffer, + property.attribute, + gl.UNSIGNED_BYTE, + true, + this.staticStride * 4, + property.offset * 4 + ); + } + else + { + this.vao.addAttribute( + this.staticBuffer, + property.attribute, + gl.FLOAT, + false, + this.staticStride * 4, + property.offset * 4 + ); + } + } + } + + /** + * Uploads the dynamic properties. + * + * @param {PIXI.DisplayObject[]} children - The children to upload. + * @param {number} startIndex - The index to start at. + * @param {number} amount - The number to upload. + */ + uploadDynamic(children, startIndex, amount) + { + for (let i = 0; i < this.dynamicProperties.length; i++) + { + const property = this.dynamicProperties[i]; + + property.uploadFunction(children, startIndex, amount, + property.unsignedByte ? this.dynamicDataUint32 : this.dynamicData, + this.dynamicStride, property.offset); + } + + this.dynamicBuffer.upload(); + } + + /** + * Uploads the static properties. + * + * @param {PIXI.DisplayObject[]} children - The children to upload. + * @param {number} startIndex - The index to start at. + * @param {number} amount - The number to upload. + */ + uploadStatic(children, startIndex, amount) + { + for (let i = 0; i < this.staticProperties.length; i++) + { + const property = this.staticProperties[i]; + + property.uploadFunction(children, startIndex, amount, + property.unsignedByte ? this.staticDataUint32 : this.staticData, + this.staticStride, property.offset); + } + + this.staticBuffer.upload(); + } + + /** + * Destroys the ParticleBuffer. + * + */ + destroy() + { + this.dynamicProperties = null; + this.dynamicBuffer.destroy(); + this.dynamicBuffer = null; + this.dynamicData = null; + this.dynamicDataUint32 = null; + + this.staticProperties = null; + this.staticBuffer.destroy(); + this.staticBuffer = null; + this.staticData = null; + this.staticDataUint32 = null; + } +} diff --git a/packages/particles/src/ParticleContainer.js b/packages/particles/src/ParticleContainer.js index c8ade96..9d25c5b 100644 --- a/packages/particles/src/ParticleContainer.js +++ b/packages/particles/src/ParticleContainer.js @@ -25,7 +25,7 @@ * * @class * @extends PIXI.Container - * @memberof PIXI.particles + * @memberof PIXI */ export default class ParticleContainer extends Container { diff --git a/packages/particles/src/ParticleRenderer.js b/packages/particles/src/ParticleRenderer.js new file mode 100644 index 0000000..5464211 --- /dev/null +++ b/packages/particles/src/ParticleRenderer.js @@ -0,0 +1,455 @@ +import { ObjectRenderer } from '@pixi/core'; +import { correctBlendMode, premultiplyRgba, premultiplyTint } from '@pixi/utils'; +import { Matrix } from '@pixi/math'; +import ParticleShader from './ParticleShader'; +import ParticleBuffer from './ParticleBuffer'; + +/** + * @author Mat Groves + * + * Big thanks to the very clever Matt DesLauriers https://github.com/mattdesl/ + * for creating the original PixiJS version! + * Also a thanks to https://github.com/bchevalier for tweaking the tint and alpha so that they now + * share 4 bytes on the vertex buffer + * + * Heavily inspired by LibGDX's ParticleRenderer: + * https://github.com/libgdx/libgdx/blob/master/gdx/src/com/badlogic/gdx/graphics/g2d/ParticleRenderer.java + */ + +/** + * + * @class + * @memberof PIXI + */ +export default class ParticleRenderer extends ObjectRenderer +{ + /** + * @param {PIXI.Renderer} renderer - The renderer this sprite batch works for. + */ + constructor(renderer) + { + super(renderer); + + // 65535 is max vertex index in the index buffer (see ParticleRenderer) + // so max number of particles is 65536 / 4 = 16384 + // and max number of element in the index buffer is 16384 * 6 = 98304 + // Creating a full index buffer, overhead is 98304 * 2 = 196Ko + // let numIndices = 98304; + + /** + * The default shader that is used if a sprite doesn't have a more specific one. + * + * @member {PIXI.Shader} + */ + this.shader = null; + + this.indexBuffer = null; + + this.properties = null; + + this.tempMatrix = new Matrix(); + + this.CONTEXT_UID = 0; + } + + /** + * When there is a WebGL context change + * + * @private + */ + onContextChange() + { + const gl = this.renderer.gl; + + this.CONTEXT_UID = this.renderer.CONTEXT_UID; + + // setup default shader + this.shader = new ParticleShader(gl); + + this.properties = [ + // verticesData + { + attribute: this.shader.attributes.aVertexPosition, + size: 2, + uploadFunction: this.uploadVertices, + offset: 0, + }, + // positionData + { + attribute: this.shader.attributes.aPositionCoord, + size: 2, + uploadFunction: this.uploadPosition, + offset: 0, + }, + // rotationData + { + attribute: this.shader.attributes.aRotation, + size: 1, + uploadFunction: this.uploadRotation, + offset: 0, + }, + // uvsData + { + attribute: this.shader.attributes.aTextureCoord, + size: 2, + uploadFunction: this.uploadUvs, + offset: 0, + }, + // tintData + { + attribute: this.shader.attributes.aColor, + size: 1, + unsignedByte: true, + uploadFunction: this.uploadTint, + offset: 0, + }, + ]; + } + + /** + * Starts a new particle batch. + * + */ + start() + { + this.renderer._bindGLShader(this.shader); + } + + /** + * Renders the particle container object. + * + * @param {PIXI.ParticleContainer} container - The container to render using this ParticleRenderer + */ + render(container) + { + const children = container.children; + const maxSize = container._maxSize; + const batchSize = container._batchSize; + const renderer = this.renderer; + let totalChildren = children.length; + + if (totalChildren === 0) + { + return; + } + else if (totalChildren > maxSize) + { + totalChildren = maxSize; + } + + let buffers = container._glBuffers[renderer.CONTEXT_UID]; + + if (!buffers) + { + buffers = container._glBuffers[renderer.CONTEXT_UID] = this.generateBuffers(container); + } + + const baseTexture = children[0]._texture.baseTexture; + + // if the uvs have not updated then no point rendering just yet! + this.renderer.setBlendMode(correctBlendMode(container.blendMode, baseTexture.premultiplyAlpha)); + + const gl = renderer.gl; + + const m = container.worldTransform.copy(this.tempMatrix); + + m.prepend(renderer._activeRenderTarget.projectionMatrix); + + this.shader.uniforms.projectionMatrix = m.toArray(true); + + this.shader.uniforms.uColor = premultiplyRgba(container.tintRgb, + container.worldAlpha, this.shader.uniforms.uColor, baseTexture.premultiplyAlpha); + + // make sure the texture is bound.. + this.shader.uniforms.uSampler = renderer.bindTexture(baseTexture); + + // now lets upload and render the buffers.. + for (let i = 0, j = 0; i < totalChildren; i += batchSize, j += 1) + { + let amount = (totalChildren - i); + + if (amount > batchSize) + { + amount = batchSize; + } + + if (j >= buffers.length) + { + if (!container.autoResize) + { + break; + } + buffers.push(this._generateOneMoreBuffer(container)); + } + + const buffer = buffers[j]; + + // we always upload the dynamic + buffer.uploadDynamic(children, i, amount); + + // we only upload the static content when we have to! + if (container._bufferToUpdate === j) + { + buffer.uploadStatic(children, i, amount); + container._bufferToUpdate = j + 1; + } + + // bind the buffer + renderer.bindVao(buffer.vao); + buffer.vao.draw(gl.TRIANGLES, amount * 6); + } + } + + /** + * Creates one particle buffer for each child in the container we want to render and updates internal properties + * + * @param {PIXI.ParticleContainer} container - The container to render using this ParticleRenderer + * @return {PIXI.ParticleBuffer[]} The buffers + */ + generateBuffers(container) + { + const gl = this.renderer.gl; + const buffers = []; + const size = container._maxSize; + const batchSize = container._batchSize; + const dynamicPropertyFlags = container._properties; + + for (let i = 0; i < size; i += batchSize) + { + buffers.push(new ParticleBuffer(gl, this.properties, dynamicPropertyFlags, batchSize)); + } + + return buffers; + } + + /** + * Creates one more particle buffer, because container has autoResize feature + * + * @param {PIXI.ParticleContainer} container - The container to render using this ParticleRenderer + * @return {PIXI.ParticleBuffer} generated buffer + * @private + */ + _generateOneMoreBuffer(container) + { + const gl = this.renderer.gl; + const batchSize = container._batchSize; + const dynamicPropertyFlags = container._properties; + + return new ParticleBuffer(gl, this.properties, dynamicPropertyFlags, batchSize); + } + + /** + * Uploads the verticies. + * + * @param {PIXI.DisplayObject[]} children - the array of display objects to render + * @param {number} startIndex - the index to start from in the children array + * @param {number} amount - the amount of children that will have their vertices uploaded + * @param {number[]} array - The vertices to upload. + * @param {number} stride - Stride to use for iteration. + * @param {number} offset - Offset to start at. + */ + uploadVertices(children, startIndex, amount, array, stride, offset) + { + let w0 = 0; + let w1 = 0; + let h0 = 0; + let h1 = 0; + + for (let i = 0; i < amount; ++i) + { + const sprite = children[startIndex + i]; + const texture = sprite._texture; + const sx = sprite.scale.x; + const sy = sprite.scale.y; + const trim = texture.trim; + const orig = texture.orig; + + if (trim) + { + // if the sprite is trimmed and is not a tilingsprite then we need to add the + // extra space before transforming the sprite coords.. + w1 = trim.x - (sprite.anchor.x * orig.width); + w0 = w1 + trim.width; + + h1 = trim.y - (sprite.anchor.y * orig.height); + h0 = h1 + trim.height; + } + else + { + w0 = (orig.width) * (1 - sprite.anchor.x); + w1 = (orig.width) * -sprite.anchor.x; + + h0 = orig.height * (1 - sprite.anchor.y); + h1 = orig.height * -sprite.anchor.y; + } + + array[offset] = w1 * sx; + array[offset + 1] = h1 * sy; + + array[offset + stride] = w0 * sx; + array[offset + stride + 1] = h1 * sy; + + array[offset + (stride * 2)] = w0 * sx; + array[offset + (stride * 2) + 1] = h0 * sy; + + array[offset + (stride * 3)] = w1 * sx; + array[offset + (stride * 3) + 1] = h0 * sy; + + offset += stride * 4; + } + } + + /** + * + * @param {PIXI.DisplayObject[]} children - the array of display objects to render + * @param {number} startIndex - the index to start from in the children array + * @param {number} amount - the amount of children that will have their positions uploaded + * @param {number[]} array - The vertices to upload. + * @param {number} stride - Stride to use for iteration. + * @param {number} offset - Offset to start at. + */ + uploadPosition(children, startIndex, amount, array, stride, offset) + { + for (let i = 0; i < amount; i++) + { + const spritePosition = children[startIndex + i].position; + + array[offset] = spritePosition.x; + array[offset + 1] = spritePosition.y; + + array[offset + stride] = spritePosition.x; + array[offset + stride + 1] = spritePosition.y; + + array[offset + (stride * 2)] = spritePosition.x; + array[offset + (stride * 2) + 1] = spritePosition.y; + + array[offset + (stride * 3)] = spritePosition.x; + array[offset + (stride * 3) + 1] = spritePosition.y; + + offset += stride * 4; + } + } + + /** + * + * @param {PIXI.DisplayObject[]} children - the array of display objects to render + * @param {number} startIndex - the index to start from in the children array + * @param {number} amount - the amount of children that will have their rotation uploaded + * @param {number[]} array - The vertices to upload. + * @param {number} stride - Stride to use for iteration. + * @param {number} offset - Offset to start at. + */ + uploadRotation(children, startIndex, amount, array, stride, offset) + { + for (let i = 0; i < amount; i++) + { + const spriteRotation = children[startIndex + i].rotation; + + array[offset] = spriteRotation; + array[offset + stride] = spriteRotation; + array[offset + (stride * 2)] = spriteRotation; + array[offset + (stride * 3)] = spriteRotation; + + offset += stride * 4; + } + } + + /** + * + * @param {PIXI.DisplayObject[]} children - the array of display objects to render + * @param {number} startIndex - the index to start from in the children array + * @param {number} amount - the amount of children that will have their rotation uploaded + * @param {number[]} array - The vertices to upload. + * @param {number} stride - Stride to use for iteration. + * @param {number} offset - Offset to start at. + */ + uploadUvs(children, startIndex, amount, array, stride, offset) + { + for (let i = 0; i < amount; ++i) + { + const textureUvs = children[startIndex + i]._texture._uvs; + + if (textureUvs) + { + array[offset] = textureUvs.x0; + array[offset + 1] = textureUvs.y0; + + array[offset + stride] = textureUvs.x1; + array[offset + stride + 1] = textureUvs.y1; + + array[offset + (stride * 2)] = textureUvs.x2; + array[offset + (stride * 2) + 1] = textureUvs.y2; + + array[offset + (stride * 3)] = textureUvs.x3; + array[offset + (stride * 3) + 1] = textureUvs.y3; + + offset += stride * 4; + } + else + { + // TODO you know this can be easier! + array[offset] = 0; + array[offset + 1] = 0; + + array[offset + stride] = 0; + array[offset + stride + 1] = 0; + + array[offset + (stride * 2)] = 0; + array[offset + (stride * 2) + 1] = 0; + + array[offset + (stride * 3)] = 0; + array[offset + (stride * 3) + 1] = 0; + + offset += stride * 4; + } + } + } + + /** + * + * @param {PIXI.DisplayObject[]} children - the array of display objects to render + * @param {number} startIndex - the index to start from in the children array + * @param {number} amount - the amount of children that will have their rotation uploaded + * @param {number[]} array - The vertices to upload. + * @param {number} stride - Stride to use for iteration. + * @param {number} offset - Offset to start at. + */ + uploadTint(children, startIndex, amount, array, stride, offset) + { + for (let i = 0; i < amount; ++i) + { + const sprite = children[startIndex + i]; + const premultiplied = sprite._texture.baseTexture.premultiplyAlpha; + const alpha = sprite.alpha; + // we dont call extra function if alpha is 1.0, that's faster + const argb = alpha < 1.0 && premultiplied ? premultiplyTint(sprite._tintRGB, alpha) + : sprite._tintRGB + (alpha * 255 << 24); + + array[offset] = argb; + array[offset + stride] = argb; + array[offset + (stride * 2)] = argb; + array[offset + (stride * 3)] = argb; + + offset += stride * 4; + } + } + + /** + * Destroys the ParticleRenderer. + * + */ + destroy() + { + if (this.renderer.gl) + { + this.renderer.gl.deleteBuffer(this.indexBuffer); + } + + super.destroy(); + + this.shader.destroy(); + + this.indices = null; + this.tempMatrix = null; + } +} diff --git a/packages/particles/src/ParticleShader.js b/packages/particles/src/ParticleShader.js new file mode 100644 index 0000000..069afe6 --- /dev/null +++ b/packages/particles/src/ParticleShader.js @@ -0,0 +1,21 @@ +import { GLShader } from 'pixi-gl-core'; +import { PRECISION } from '@pixi/constants'; +import vertex from './particles.vert'; +import fragment from './particles.frag'; + +/** + * @class + * @extends PIXI.Shader + * @memberof PIXI + * @private + */ +export default class ParticleShader extends GLShader +{ + /** + * @param {PIXI.Shader} gl - The webgl shader manager this shader works for. + */ + constructor(gl) + { + super(gl, vertex, fragment, PRECISION.DEFAULT); + } +} diff --git a/packages/particles/src/index.js b/packages/particles/src/index.js index 629d599..d315119 100644 --- a/packages/particles/src/index.js +++ b/packages/particles/src/index.js @@ -1,5 +1,2 @@ -/** - * @namespace PIXI.particles - */ export { default as ParticleContainer } from './ParticleContainer'; -export { default as ParticleRenderer } from './webgl/ParticleRenderer'; +export { default as ParticleRenderer } from './ParticleRenderer'; diff --git a/packages/particles/src/particles.frag b/packages/particles/src/particles.frag new file mode 100644 index 0000000..a725046 --- /dev/null +++ b/packages/particles/src/particles.frag @@ -0,0 +1,9 @@ +varying vec2 vTextureCoord; +varying vec4 vColor; + +uniform sampler2D uSampler; + +void main(void){ + vec4 color = texture2D(uSampler, vTextureCoord) * vColor; + gl_FragColor = color; +} \ No newline at end of file diff --git a/packages/particles/src/particles.vert b/packages/particles/src/particles.vert new file mode 100644 index 0000000..bd5401c --- /dev/null +++ b/packages/particles/src/particles.vert @@ -0,0 +1,26 @@ +attribute vec2 aVertexPosition; +attribute vec2 aTextureCoord; +attribute vec4 aColor; + +attribute vec2 aPositionCoord; +attribute vec2 aScale; +attribute float aRotation; + +uniform mat3 projectionMatrix; +uniform vec4 uColor; + +varying vec2 vTextureCoord; +varying vec4 vColor; + +void main(void){ + float x = (aVertexPosition.x) * cos(aRotation) - (aVertexPosition.y) * sin(aRotation); + float y = (aVertexPosition.x) * sin(aRotation) + (aVertexPosition.y) * cos(aRotation); + + vec2 v = vec2(x, y); + v = v + aPositionCoord; + + gl_Position = vec4((projectionMatrix * vec3(v, 1.0)).xy, 0.0, 1.0); + + vTextureCoord = aTextureCoord; + vColor = aColor * uColor; +} \ No newline at end of file diff --git a/packages/particles/src/webgl/ParticleBuffer.js b/packages/particles/src/webgl/ParticleBuffer.js deleted file mode 100644 index 6306bda..0000000 --- a/packages/particles/src/webgl/ParticleBuffer.js +++ /dev/null @@ -1,268 +0,0 @@ -// import { VertexArrayObject } from 'pixi-gl-core'; -import { createIndicesForQuads } from '@pixi/utils'; -import { GLBuffer } from 'pixi-gl-core'; - -/** - * @author Mat Groves - * - * Big thanks to the very clever Matt DesLauriers https://github.com/mattdesl/ - * for creating the original PixiJS version! - * Also a thanks to https://github.com/bchevalier for tweaking the tint and alpha so that - * they now share 4 bytes on the vertex buffer - * - * Heavily inspired by LibGDX's ParticleBuffer: - * https://github.com/libgdx/libgdx/blob/master/gdx/src/com/badlogic/gdx/graphics/g2d/ParticleBuffer.java - */ - -/** - * The particle buffer manages the static and dynamic buffers for a particle container. - * - * @class - * @private - * @memberof PIXI - */ -export default class ParticleBuffer -{ - /** - * @param {WebGLRenderingContext} gl - The rendering context. - * @param {object} properties - The properties to upload. - * @param {boolean[]} dynamicPropertyFlags - Flags for which properties are dynamic. - * @param {number} size - The size of the batch. - */ - constructor(gl, properties, dynamicPropertyFlags, size) - { - /** - * The current WebGL drawing context. - * - * @member {WebGLRenderingContext} - */ - this.gl = gl; - - /** - * The number of particles the buffer can hold - * - * @member {number} - */ - this.size = size; - - /** - * A list of the properties that are dynamic. - * - * @member {object[]} - */ - this.dynamicProperties = []; - - /** - * A list of the properties that are static. - * - * @member {object[]} - */ - this.staticProperties = []; - - for (let i = 0; i < properties.length; ++i) - { - let property = properties[i]; - - // Make copy of properties object so that when we edit the offset it doesn't - // change all other instances of the object literal - property = { - attribute: property.attribute, - size: property.size, - uploadFunction: property.uploadFunction, - unsignedByte: property.unsignedByte, - offset: property.offset, - }; - - if (dynamicPropertyFlags[i]) - { - this.dynamicProperties.push(property); - } - else - { - this.staticProperties.push(property); - } - } - - this.staticStride = 0; - this.staticBuffer = null; - this.staticData = null; - this.staticDataUint32 = null; - - this.dynamicStride = 0; - this.dynamicBuffer = null; - this.dynamicData = null; - this.dynamicDataUint32 = null; - - this.initBuffers(); - } - - /** - * Sets up the renderer context and necessary buffers. - * - * @private - */ - initBuffers() - { - const gl = this.gl; - let dynamicOffset = 0; - - /** - * Holds the indices of the geometry (quads) to draw - * - * @member {Uint16Array} - */ - this.indices = createIndicesForQuads(this.size); - this.indexBuffer = GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); - - this.dynamicStride = 0; - - for (let i = 0; i < this.dynamicProperties.length; ++i) - { - const property = this.dynamicProperties[i]; - - property.offset = dynamicOffset; - dynamicOffset += property.size; - this.dynamicStride += property.size; - } - - this.dynamicData = new Float32Array(this.size * this.dynamicStride * 4); - this.dynamicBuffer = GLBuffer.createVertexBuffer(gl, this.dynamicData, gl.STREAM_DRAW); - - // static // - let staticOffset = 0; - - this.staticStride = 0; - - for (let i = 0; i < this.staticProperties.length; ++i) - { - const property = this.staticProperties[i]; - - property.offset = staticOffset; - staticOffset += property.size; - this.staticStride += property.size; - } - - this.staticData = new Float32Array(this.size * this.staticStride * 4); - this.staticBuffer = GLBuffer.createVertexBuffer(gl, this.staticData, gl.STATIC_DRAW); - - // this.vao = new VertexArrayObject(gl) - // .addIndex(this.indexBuffer); - - for (let i = 0; i < this.dynamicProperties.length; ++i) - { - const property = this.dynamicProperties[i]; - - if (property.unsignedByte) - { - this.vao.addAttribute( - this.dynamicBuffer, - property.attribute, - gl.UNSIGNED_BYTE, - true, - this.dynamicStride * 4, - property.offset * 4 - ); - } - else - { - this.vao.addAttribute( - this.dynamicBuffer, - property.attribute, - gl.FLOAT, - false, - this.dynamicStride * 4, - property.offset * 4 - ); - } - } - - for (let i = 0; i < this.staticProperties.length; ++i) - { - const property = this.staticProperties[i]; - - if (property.unsignedByte) - { - this.vao.addAttribute( - this.staticBuffer, - property.attribute, - gl.UNSIGNED_BYTE, - true, - this.staticStride * 4, - property.offset * 4 - ); - } - else - { - this.vao.addAttribute( - this.staticBuffer, - property.attribute, - gl.FLOAT, - false, - this.staticStride * 4, - property.offset * 4 - ); - } - } - } - - /** - * Uploads the dynamic properties. - * - * @param {PIXI.DisplayObject[]} children - The children to upload. - * @param {number} startIndex - The index to start at. - * @param {number} amount - The number to upload. - */ - uploadDynamic(children, startIndex, amount) - { - for (let i = 0; i < this.dynamicProperties.length; i++) - { - const property = this.dynamicProperties[i]; - - property.uploadFunction(children, startIndex, amount, - property.unsignedByte ? this.dynamicDataUint32 : this.dynamicData, - this.dynamicStride, property.offset); - } - - this.dynamicBuffer.upload(); - } - - /** - * Uploads the static properties. - * - * @param {PIXI.DisplayObject[]} children - The children to upload. - * @param {number} startIndex - The index to start at. - * @param {number} amount - The number to upload. - */ - uploadStatic(children, startIndex, amount) - { - for (let i = 0; i < this.staticProperties.length; i++) - { - const property = this.staticProperties[i]; - - property.uploadFunction(children, startIndex, amount, - property.unsignedByte ? this.staticDataUint32 : this.staticData, - this.staticStride, property.offset); - } - - this.staticBuffer.upload(); - } - - /** - * Destroys the ParticleBuffer. - * - */ - destroy() - { - this.dynamicProperties = null; - this.dynamicBuffer.destroy(); - this.dynamicBuffer = null; - this.dynamicData = null; - this.dynamicDataUint32 = null; - - this.staticProperties = null; - this.staticBuffer.destroy(); - this.staticBuffer = null; - this.staticData = null; - this.staticDataUint32 = null; - } -} diff --git a/packages/particles/src/webgl/ParticleRenderer.js b/packages/particles/src/webgl/ParticleRenderer.js deleted file mode 100644 index 8e50994..0000000 --- a/packages/particles/src/webgl/ParticleRenderer.js +++ /dev/null @@ -1,458 +0,0 @@ -import { ObjectRenderer, Renderer } from '@pixi/core'; -import { correctBlendMode, premultiplyRgba, premultiplyTint } from '@pixi/utils'; -import { Matrix } from '@pixi/math'; -import ParticleShader from './ParticleShader'; -import ParticleBuffer from './ParticleBuffer'; - -/** - * @author Mat Groves - * - * Big thanks to the very clever Matt DesLauriers https://github.com/mattdesl/ - * for creating the original PixiJS version! - * Also a thanks to https://github.com/bchevalier for tweaking the tint and alpha so that they now - * share 4 bytes on the vertex buffer - * - * Heavily inspired by LibGDX's ParticleRenderer: - * https://github.com/libgdx/libgdx/blob/master/gdx/src/com/badlogic/gdx/graphics/g2d/ParticleRenderer.java - */ - -/** - * - * @class - * @private - * @memberof PIXI - */ -export default class ParticleRenderer extends ObjectRenderer -{ - /** - * @param {PIXI.Renderer} renderer - The renderer this sprite batch works for. - */ - constructor(renderer) - { - super(renderer); - - // 65535 is max vertex index in the index buffer (see ParticleRenderer) - // so max number of particles is 65536 / 4 = 16384 - // and max number of element in the index buffer is 16384 * 6 = 98304 - // Creating a full index buffer, overhead is 98304 * 2 = 196Ko - // let numIndices = 98304; - - /** - * The default shader that is used if a sprite doesn't have a more specific one. - * - * @member {PIXI.Shader} - */ - this.shader = null; - - this.indexBuffer = null; - - this.properties = null; - - this.tempMatrix = new Matrix(); - - this.CONTEXT_UID = 0; - } - - /** - * When there is a WebGL context change - * - * @private - */ - onContextChange() - { - const gl = this.renderer.gl; - - this.CONTEXT_UID = this.renderer.CONTEXT_UID; - - // setup default shader - this.shader = new ParticleShader(gl); - - this.properties = [ - // verticesData - { - attribute: this.shader.attributes.aVertexPosition, - size: 2, - uploadFunction: this.uploadVertices, - offset: 0, - }, - // positionData - { - attribute: this.shader.attributes.aPositionCoord, - size: 2, - uploadFunction: this.uploadPosition, - offset: 0, - }, - // rotationData - { - attribute: this.shader.attributes.aRotation, - size: 1, - uploadFunction: this.uploadRotation, - offset: 0, - }, - // uvsData - { - attribute: this.shader.attributes.aTextureCoord, - size: 2, - uploadFunction: this.uploadUvs, - offset: 0, - }, - // tintData - { - attribute: this.shader.attributes.aColor, - size: 1, - unsignedByte: true, - uploadFunction: this.uploadTint, - offset: 0, - }, - ]; - } - - /** - * Starts a new particle batch. - * - */ - start() - { - this.renderer._bindGLShader(this.shader); - } - - /** - * Renders the particle container object. - * - * @param {PIXI.ParticleContainer} container - The container to render using this ParticleRenderer - */ - render(container) - { - const children = container.children; - const maxSize = container._maxSize; - const batchSize = container._batchSize; - const renderer = this.renderer; - let totalChildren = children.length; - - if (totalChildren === 0) - { - return; - } - else if (totalChildren > maxSize) - { - totalChildren = maxSize; - } - - let buffers = container._glBuffers[renderer.CONTEXT_UID]; - - if (!buffers) - { - buffers = container._glBuffers[renderer.CONTEXT_UID] = this.generateBuffers(container); - } - - const baseTexture = children[0]._texture.baseTexture; - - // if the uvs have not updated then no point rendering just yet! - this.renderer.setBlendMode(correctBlendMode(container.blendMode, baseTexture.premultiplyAlpha)); - - const gl = renderer.gl; - - const m = container.worldTransform.copy(this.tempMatrix); - - m.prepend(renderer._activeRenderTarget.projectionMatrix); - - this.shader.uniforms.projectionMatrix = m.toArray(true); - - this.shader.uniforms.uColor = premultiplyRgba(container.tintRgb, - container.worldAlpha, this.shader.uniforms.uColor, baseTexture.premultiplyAlpha); - - // make sure the texture is bound.. - this.shader.uniforms.uSampler = renderer.bindTexture(baseTexture); - - // now lets upload and render the buffers.. - for (let i = 0, j = 0; i < totalChildren; i += batchSize, j += 1) - { - let amount = (totalChildren - i); - - if (amount > batchSize) - { - amount = batchSize; - } - - if (j >= buffers.length) - { - if (!container.autoResize) - { - break; - } - buffers.push(this._generateOneMoreBuffer(container)); - } - - const buffer = buffers[j]; - - // we always upload the dynamic - buffer.uploadDynamic(children, i, amount); - - // we only upload the static content when we have to! - if (container._bufferToUpdate === j) - { - buffer.uploadStatic(children, i, amount); - container._bufferToUpdate = j + 1; - } - - // bind the buffer - renderer.bindVao(buffer.vao); - buffer.vao.draw(gl.TRIANGLES, amount * 6); - } - } - - /** - * Creates one particle buffer for each child in the container we want to render and updates internal properties - * - * @param {PIXI.ParticleContainer} container - The container to render using this ParticleRenderer - * @return {PIXI.ParticleBuffer[]} The buffers - */ - generateBuffers(container) - { - const gl = this.renderer.gl; - const buffers = []; - const size = container._maxSize; - const batchSize = container._batchSize; - const dynamicPropertyFlags = container._properties; - - for (let i = 0; i < size; i += batchSize) - { - buffers.push(new ParticleBuffer(gl, this.properties, dynamicPropertyFlags, batchSize)); - } - - return buffers; - } - - /** - * Creates one more particle buffer, because container has autoResize feature - * - * @param {PIXI.ParticleContainer} container - The container to render using this ParticleRenderer - * @return {PIXI.ParticleBuffer} generated buffer - * @private - */ - _generateOneMoreBuffer(container) - { - const gl = this.renderer.gl; - const batchSize = container._batchSize; - const dynamicPropertyFlags = container._properties; - - return new ParticleBuffer(gl, this.properties, dynamicPropertyFlags, batchSize); - } - - /** - * Uploads the verticies. - * - * @param {PIXI.DisplayObject[]} children - the array of display objects to render - * @param {number} startIndex - the index to start from in the children array - * @param {number} amount - the amount of children that will have their vertices uploaded - * @param {number[]} array - The vertices to upload. - * @param {number} stride - Stride to use for iteration. - * @param {number} offset - Offset to start at. - */ - uploadVertices(children, startIndex, amount, array, stride, offset) - { - let w0 = 0; - let w1 = 0; - let h0 = 0; - let h1 = 0; - - for (let i = 0; i < amount; ++i) - { - const sprite = children[startIndex + i]; - const texture = sprite._texture; - const sx = sprite.scale.x; - const sy = sprite.scale.y; - const trim = texture.trim; - const orig = texture.orig; - - if (trim) - { - // if the sprite is trimmed and is not a tilingsprite then we need to add the - // extra space before transforming the sprite coords.. - w1 = trim.x - (sprite.anchor.x * orig.width); - w0 = w1 + trim.width; - - h1 = trim.y - (sprite.anchor.y * orig.height); - h0 = h1 + trim.height; - } - else - { - w0 = (orig.width) * (1 - sprite.anchor.x); - w1 = (orig.width) * -sprite.anchor.x; - - h0 = orig.height * (1 - sprite.anchor.y); - h1 = orig.height * -sprite.anchor.y; - } - - array[offset] = w1 * sx; - array[offset + 1] = h1 * sy; - - array[offset + stride] = w0 * sx; - array[offset + stride + 1] = h1 * sy; - - array[offset + (stride * 2)] = w0 * sx; - array[offset + (stride * 2) + 1] = h0 * sy; - - array[offset + (stride * 3)] = w1 * sx; - array[offset + (stride * 3) + 1] = h0 * sy; - - offset += stride * 4; - } - } - - /** - * - * @param {PIXI.DisplayObject[]} children - the array of display objects to render - * @param {number} startIndex - the index to start from in the children array - * @param {number} amount - the amount of children that will have their positions uploaded - * @param {number[]} array - The vertices to upload. - * @param {number} stride - Stride to use for iteration. - * @param {number} offset - Offset to start at. - */ - uploadPosition(children, startIndex, amount, array, stride, offset) - { - for (let i = 0; i < amount; i++) - { - const spritePosition = children[startIndex + i].position; - - array[offset] = spritePosition.x; - array[offset + 1] = spritePosition.y; - - array[offset + stride] = spritePosition.x; - array[offset + stride + 1] = spritePosition.y; - - array[offset + (stride * 2)] = spritePosition.x; - array[offset + (stride * 2) + 1] = spritePosition.y; - - array[offset + (stride * 3)] = spritePosition.x; - array[offset + (stride * 3) + 1] = spritePosition.y; - - offset += stride * 4; - } - } - - /** - * - * @param {PIXI.DisplayObject[]} children - the array of display objects to render - * @param {number} startIndex - the index to start from in the children array - * @param {number} amount - the amount of children that will have their rotation uploaded - * @param {number[]} array - The vertices to upload. - * @param {number} stride - Stride to use for iteration. - * @param {number} offset - Offset to start at. - */ - uploadRotation(children, startIndex, amount, array, stride, offset) - { - for (let i = 0; i < amount; i++) - { - const spriteRotation = children[startIndex + i].rotation; - - array[offset] = spriteRotation; - array[offset + stride] = spriteRotation; - array[offset + (stride * 2)] = spriteRotation; - array[offset + (stride * 3)] = spriteRotation; - - offset += stride * 4; - } - } - - /** - * - * @param {PIXI.DisplayObject[]} children - the array of display objects to render - * @param {number} startIndex - the index to start from in the children array - * @param {number} amount - the amount of children that will have their rotation uploaded - * @param {number[]} array - The vertices to upload. - * @param {number} stride - Stride to use for iteration. - * @param {number} offset - Offset to start at. - */ - uploadUvs(children, startIndex, amount, array, stride, offset) - { - for (let i = 0; i < amount; ++i) - { - const textureUvs = children[startIndex + i]._texture._uvs; - - if (textureUvs) - { - array[offset] = textureUvs.x0; - array[offset + 1] = textureUvs.y0; - - array[offset + stride] = textureUvs.x1; - array[offset + stride + 1] = textureUvs.y1; - - array[offset + (stride * 2)] = textureUvs.x2; - array[offset + (stride * 2) + 1] = textureUvs.y2; - - array[offset + (stride * 3)] = textureUvs.x3; - array[offset + (stride * 3) + 1] = textureUvs.y3; - - offset += stride * 4; - } - else - { - // TODO you know this can be easier! - array[offset] = 0; - array[offset + 1] = 0; - - array[offset + stride] = 0; - array[offset + stride + 1] = 0; - - array[offset + (stride * 2)] = 0; - array[offset + (stride * 2) + 1] = 0; - - array[offset + (stride * 3)] = 0; - array[offset + (stride * 3) + 1] = 0; - - offset += stride * 4; - } - } - } - - /** - * - * @param {PIXI.DisplayObject[]} children - the array of display objects to render - * @param {number} startIndex - the index to start from in the children array - * @param {number} amount - the amount of children that will have their rotation uploaded - * @param {number[]} array - The vertices to upload. - * @param {number} stride - Stride to use for iteration. - * @param {number} offset - Offset to start at. - */ - uploadTint(children, startIndex, amount, array, stride, offset) - { - for (let i = 0; i < amount; ++i) - { - const sprite = children[startIndex + i]; - const premultiplied = sprite._texture.baseTexture.premultiplyAlpha; - const alpha = sprite.alpha; - // we dont call extra function if alpha is 1.0, that's faster - const argb = alpha < 1.0 && premultiplied ? premultiplyTint(sprite._tintRGB, alpha) - : sprite._tintRGB + (alpha * 255 << 24); - - array[offset] = argb; - array[offset + stride] = argb; - array[offset + (stride * 2)] = argb; - array[offset + (stride * 3)] = argb; - - offset += stride * 4; - } - } - - /** - * Destroys the ParticleRenderer. - * - */ - destroy() - { - if (this.renderer.gl) - { - this.renderer.gl.deleteBuffer(this.indexBuffer); - } - - super.destroy(); - - this.shader.destroy(); - - this.indices = null; - this.tempMatrix = null; - } -} - -Renderer.registerPlugin('particle', ParticleRenderer); diff --git a/packages/particles/src/webgl/ParticleShader.js b/packages/particles/src/webgl/ParticleShader.js deleted file mode 100644 index 2caeec5..0000000 --- a/packages/particles/src/webgl/ParticleShader.js +++ /dev/null @@ -1,62 +0,0 @@ -import { GLShader } from 'pixi-gl-core'; -import { PRECISION } from '@pixi/constants'; - -/** - * @class - * @extends PIXI.Shader - * @memberof PIXI - */ -export default class ParticleShader extends GLShader -{ - /** - * @param {PIXI.Shader} gl - The webgl shader manager this shader works for. - */ - constructor(gl) - { - super( - gl, - // vertex shader - [ - 'attribute vec2 aVertexPosition;', - 'attribute vec2 aTextureCoord;', - 'attribute vec4 aColor;', - - 'attribute vec2 aPositionCoord;', - 'attribute vec2 aScale;', - 'attribute float aRotation;', - - 'uniform mat3 projectionMatrix;', - 'uniform vec4 uColor;', - - 'varying vec2 vTextureCoord;', - 'varying vec4 vColor;', - - 'void main(void){', - ' float x = (aVertexPosition.x) * cos(aRotation) - (aVertexPosition.y) * sin(aRotation);', - ' float y = (aVertexPosition.x) * sin(aRotation) + (aVertexPosition.y) * cos(aRotation);', - - ' vec2 v = vec2(x, y);', - ' v = v + aPositionCoord;', - - ' gl_Position = vec4((projectionMatrix * vec3(v, 1.0)).xy, 0.0, 1.0);', - - ' vTextureCoord = aTextureCoord;', - ' vColor = aColor * uColor;', - '}', - ].join('\n'), - // hello - [ - 'varying vec2 vTextureCoord;', - 'varying vec4 vColor;', - - 'uniform sampler2D uSampler;', - - 'void main(void){', - ' vec4 color = texture2D(uSampler, vTextureCoord) * vColor;', - ' gl_FragColor = color;', - '}', - ].join('\n'), - PRECISION.DEFAULT - ); - } -} diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index e59c66a..a390a1a 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -332,6 +332,43 @@ /* * This namespace has been removed and items have been moved to * the top-level `PIXI` object. + * @namespace PIXI.particles + * @deprecated since 5.0.0 + */ + PIXI.particles = {}; + + Object.defineProperties(PIXI.particles, { + /** + * @class PIXI.particles.ParticleContainer + * @deprecated since 5.0.0 + * @see PIXI.ParticleContainer + */ + ParticleContainer: { + get() + { + warn('PIXI.particles.ParticleContainer has moved to PIXI.ParticleContainer'); + + return PIXI.ParticleContainer; + }, + }, + /** + * @class PIXI.particles.ParticleRenderer + * @deprecated since 5.0.0 + * @see PIXI.ParticleRenderer + */ + ParticleRenderer: { + get() + { + warn('PIXI.particles.ParticleRenderer has moved to PIXI.ParticleRenderer'); + + return PIXI.ParticleRenderer; + }, + }, + }); + + /* + * This namespace has been removed and items have been moved to + * the top-level `PIXI` object. * @namespace PIXI.ticker * @deprecated since 5.0.0 */ diff --git a/bundles/pixi.js/src/index.js b/bundles/pixi.js/src/index.js index 52d7cb4..fb839a0 100644 --- a/bundles/pixi.js/src/index.js +++ b/bundles/pixi.js/src/index.js @@ -9,6 +9,7 @@ import * as loaders from '@pixi/loaders'; import * as math from '@pixi/math'; import * as mesh from '@pixi/mesh'; +import * as particles from '@pixi/particles'; import * as prepare from '@pixi/prepare'; import * as sprite from '@pixi/sprite'; import * as spriteAnimated from '@pixi/sprite-animated'; @@ -37,6 +38,7 @@ core.Renderer.registerPlugin('graphics', graphics.GraphicsRenderer); core.Renderer.registerPlugin('interaction', interaction.InteractionManager); core.Renderer.registerPlugin('mesh', mesh.MeshRenderer); +core.Renderer.registerPlugin('particle', particles.ParticleRenderer); core.Renderer.registerPlugin('prepare', prepare.Prepare); core.Renderer.registerPlugin('sprite', sprite.SpriteRenderer); core.Renderer.registerPlugin('tilingSprite', spriteTiling.TilingSpriteRenderer); @@ -118,6 +120,7 @@ loaders, math, mesh, + particles, sprite, spriteAnimated, spritesheet, @@ -141,6 +144,7 @@ export * from '@pixi/loaders'; export * from '@pixi/math'; export * from '@pixi/mesh'; +export * from '@pixi/particles'; export * from '@pixi/sprite'; export * from '@pixi/spritesheet'; export * from '@pixi/sprite-animated'; diff --git a/packages/particles/README.md b/packages/particles/README.md index bca3a96..dca806d 100644 --- a/packages/particles/README.md +++ b/packages/particles/README.md @@ -9,5 +9,8 @@ ## Usage ```js -import '@pixi/particles'; +import {ParticleRenderer} from '@pixi/particles'; +import {Renderer} from '@pixi/core'; + +Renderer.registerPlugin('particle', ParticleRenderer); ``` \ No newline at end of file diff --git a/packages/particles/src/ParticleBuffer.js b/packages/particles/src/ParticleBuffer.js new file mode 100644 index 0000000..6306bda --- /dev/null +++ b/packages/particles/src/ParticleBuffer.js @@ -0,0 +1,268 @@ +// import { VertexArrayObject } from 'pixi-gl-core'; +import { createIndicesForQuads } from '@pixi/utils'; +import { GLBuffer } from 'pixi-gl-core'; + +/** + * @author Mat Groves + * + * Big thanks to the very clever Matt DesLauriers https://github.com/mattdesl/ + * for creating the original PixiJS version! + * Also a thanks to https://github.com/bchevalier for tweaking the tint and alpha so that + * they now share 4 bytes on the vertex buffer + * + * Heavily inspired by LibGDX's ParticleBuffer: + * https://github.com/libgdx/libgdx/blob/master/gdx/src/com/badlogic/gdx/graphics/g2d/ParticleBuffer.java + */ + +/** + * The particle buffer manages the static and dynamic buffers for a particle container. + * + * @class + * @private + * @memberof PIXI + */ +export default class ParticleBuffer +{ + /** + * @param {WebGLRenderingContext} gl - The rendering context. + * @param {object} properties - The properties to upload. + * @param {boolean[]} dynamicPropertyFlags - Flags for which properties are dynamic. + * @param {number} size - The size of the batch. + */ + constructor(gl, properties, dynamicPropertyFlags, size) + { + /** + * The current WebGL drawing context. + * + * @member {WebGLRenderingContext} + */ + this.gl = gl; + + /** + * The number of particles the buffer can hold + * + * @member {number} + */ + this.size = size; + + /** + * A list of the properties that are dynamic. + * + * @member {object[]} + */ + this.dynamicProperties = []; + + /** + * A list of the properties that are static. + * + * @member {object[]} + */ + this.staticProperties = []; + + for (let i = 0; i < properties.length; ++i) + { + let property = properties[i]; + + // Make copy of properties object so that when we edit the offset it doesn't + // change all other instances of the object literal + property = { + attribute: property.attribute, + size: property.size, + uploadFunction: property.uploadFunction, + unsignedByte: property.unsignedByte, + offset: property.offset, + }; + + if (dynamicPropertyFlags[i]) + { + this.dynamicProperties.push(property); + } + else + { + this.staticProperties.push(property); + } + } + + this.staticStride = 0; + this.staticBuffer = null; + this.staticData = null; + this.staticDataUint32 = null; + + this.dynamicStride = 0; + this.dynamicBuffer = null; + this.dynamicData = null; + this.dynamicDataUint32 = null; + + this.initBuffers(); + } + + /** + * Sets up the renderer context and necessary buffers. + * + * @private + */ + initBuffers() + { + const gl = this.gl; + let dynamicOffset = 0; + + /** + * Holds the indices of the geometry (quads) to draw + * + * @member {Uint16Array} + */ + this.indices = createIndicesForQuads(this.size); + this.indexBuffer = GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); + + this.dynamicStride = 0; + + for (let i = 0; i < this.dynamicProperties.length; ++i) + { + const property = this.dynamicProperties[i]; + + property.offset = dynamicOffset; + dynamicOffset += property.size; + this.dynamicStride += property.size; + } + + this.dynamicData = new Float32Array(this.size * this.dynamicStride * 4); + this.dynamicBuffer = GLBuffer.createVertexBuffer(gl, this.dynamicData, gl.STREAM_DRAW); + + // static // + let staticOffset = 0; + + this.staticStride = 0; + + for (let i = 0; i < this.staticProperties.length; ++i) + { + const property = this.staticProperties[i]; + + property.offset = staticOffset; + staticOffset += property.size; + this.staticStride += property.size; + } + + this.staticData = new Float32Array(this.size * this.staticStride * 4); + this.staticBuffer = GLBuffer.createVertexBuffer(gl, this.staticData, gl.STATIC_DRAW); + + // this.vao = new VertexArrayObject(gl) + // .addIndex(this.indexBuffer); + + for (let i = 0; i < this.dynamicProperties.length; ++i) + { + const property = this.dynamicProperties[i]; + + if (property.unsignedByte) + { + this.vao.addAttribute( + this.dynamicBuffer, + property.attribute, + gl.UNSIGNED_BYTE, + true, + this.dynamicStride * 4, + property.offset * 4 + ); + } + else + { + this.vao.addAttribute( + this.dynamicBuffer, + property.attribute, + gl.FLOAT, + false, + this.dynamicStride * 4, + property.offset * 4 + ); + } + } + + for (let i = 0; i < this.staticProperties.length; ++i) + { + const property = this.staticProperties[i]; + + if (property.unsignedByte) + { + this.vao.addAttribute( + this.staticBuffer, + property.attribute, + gl.UNSIGNED_BYTE, + true, + this.staticStride * 4, + property.offset * 4 + ); + } + else + { + this.vao.addAttribute( + this.staticBuffer, + property.attribute, + gl.FLOAT, + false, + this.staticStride * 4, + property.offset * 4 + ); + } + } + } + + /** + * Uploads the dynamic properties. + * + * @param {PIXI.DisplayObject[]} children - The children to upload. + * @param {number} startIndex - The index to start at. + * @param {number} amount - The number to upload. + */ + uploadDynamic(children, startIndex, amount) + { + for (let i = 0; i < this.dynamicProperties.length; i++) + { + const property = this.dynamicProperties[i]; + + property.uploadFunction(children, startIndex, amount, + property.unsignedByte ? this.dynamicDataUint32 : this.dynamicData, + this.dynamicStride, property.offset); + } + + this.dynamicBuffer.upload(); + } + + /** + * Uploads the static properties. + * + * @param {PIXI.DisplayObject[]} children - The children to upload. + * @param {number} startIndex - The index to start at. + * @param {number} amount - The number to upload. + */ + uploadStatic(children, startIndex, amount) + { + for (let i = 0; i < this.staticProperties.length; i++) + { + const property = this.staticProperties[i]; + + property.uploadFunction(children, startIndex, amount, + property.unsignedByte ? this.staticDataUint32 : this.staticData, + this.staticStride, property.offset); + } + + this.staticBuffer.upload(); + } + + /** + * Destroys the ParticleBuffer. + * + */ + destroy() + { + this.dynamicProperties = null; + this.dynamicBuffer.destroy(); + this.dynamicBuffer = null; + this.dynamicData = null; + this.dynamicDataUint32 = null; + + this.staticProperties = null; + this.staticBuffer.destroy(); + this.staticBuffer = null; + this.staticData = null; + this.staticDataUint32 = null; + } +} diff --git a/packages/particles/src/ParticleContainer.js b/packages/particles/src/ParticleContainer.js index c8ade96..9d25c5b 100644 --- a/packages/particles/src/ParticleContainer.js +++ b/packages/particles/src/ParticleContainer.js @@ -25,7 +25,7 @@ * * @class * @extends PIXI.Container - * @memberof PIXI.particles + * @memberof PIXI */ export default class ParticleContainer extends Container { diff --git a/packages/particles/src/ParticleRenderer.js b/packages/particles/src/ParticleRenderer.js new file mode 100644 index 0000000..5464211 --- /dev/null +++ b/packages/particles/src/ParticleRenderer.js @@ -0,0 +1,455 @@ +import { ObjectRenderer } from '@pixi/core'; +import { correctBlendMode, premultiplyRgba, premultiplyTint } from '@pixi/utils'; +import { Matrix } from '@pixi/math'; +import ParticleShader from './ParticleShader'; +import ParticleBuffer from './ParticleBuffer'; + +/** + * @author Mat Groves + * + * Big thanks to the very clever Matt DesLauriers https://github.com/mattdesl/ + * for creating the original PixiJS version! + * Also a thanks to https://github.com/bchevalier for tweaking the tint and alpha so that they now + * share 4 bytes on the vertex buffer + * + * Heavily inspired by LibGDX's ParticleRenderer: + * https://github.com/libgdx/libgdx/blob/master/gdx/src/com/badlogic/gdx/graphics/g2d/ParticleRenderer.java + */ + +/** + * + * @class + * @memberof PIXI + */ +export default class ParticleRenderer extends ObjectRenderer +{ + /** + * @param {PIXI.Renderer} renderer - The renderer this sprite batch works for. + */ + constructor(renderer) + { + super(renderer); + + // 65535 is max vertex index in the index buffer (see ParticleRenderer) + // so max number of particles is 65536 / 4 = 16384 + // and max number of element in the index buffer is 16384 * 6 = 98304 + // Creating a full index buffer, overhead is 98304 * 2 = 196Ko + // let numIndices = 98304; + + /** + * The default shader that is used if a sprite doesn't have a more specific one. + * + * @member {PIXI.Shader} + */ + this.shader = null; + + this.indexBuffer = null; + + this.properties = null; + + this.tempMatrix = new Matrix(); + + this.CONTEXT_UID = 0; + } + + /** + * When there is a WebGL context change + * + * @private + */ + onContextChange() + { + const gl = this.renderer.gl; + + this.CONTEXT_UID = this.renderer.CONTEXT_UID; + + // setup default shader + this.shader = new ParticleShader(gl); + + this.properties = [ + // verticesData + { + attribute: this.shader.attributes.aVertexPosition, + size: 2, + uploadFunction: this.uploadVertices, + offset: 0, + }, + // positionData + { + attribute: this.shader.attributes.aPositionCoord, + size: 2, + uploadFunction: this.uploadPosition, + offset: 0, + }, + // rotationData + { + attribute: this.shader.attributes.aRotation, + size: 1, + uploadFunction: this.uploadRotation, + offset: 0, + }, + // uvsData + { + attribute: this.shader.attributes.aTextureCoord, + size: 2, + uploadFunction: this.uploadUvs, + offset: 0, + }, + // tintData + { + attribute: this.shader.attributes.aColor, + size: 1, + unsignedByte: true, + uploadFunction: this.uploadTint, + offset: 0, + }, + ]; + } + + /** + * Starts a new particle batch. + * + */ + start() + { + this.renderer._bindGLShader(this.shader); + } + + /** + * Renders the particle container object. + * + * @param {PIXI.ParticleContainer} container - The container to render using this ParticleRenderer + */ + render(container) + { + const children = container.children; + const maxSize = container._maxSize; + const batchSize = container._batchSize; + const renderer = this.renderer; + let totalChildren = children.length; + + if (totalChildren === 0) + { + return; + } + else if (totalChildren > maxSize) + { + totalChildren = maxSize; + } + + let buffers = container._glBuffers[renderer.CONTEXT_UID]; + + if (!buffers) + { + buffers = container._glBuffers[renderer.CONTEXT_UID] = this.generateBuffers(container); + } + + const baseTexture = children[0]._texture.baseTexture; + + // if the uvs have not updated then no point rendering just yet! + this.renderer.setBlendMode(correctBlendMode(container.blendMode, baseTexture.premultiplyAlpha)); + + const gl = renderer.gl; + + const m = container.worldTransform.copy(this.tempMatrix); + + m.prepend(renderer._activeRenderTarget.projectionMatrix); + + this.shader.uniforms.projectionMatrix = m.toArray(true); + + this.shader.uniforms.uColor = premultiplyRgba(container.tintRgb, + container.worldAlpha, this.shader.uniforms.uColor, baseTexture.premultiplyAlpha); + + // make sure the texture is bound.. + this.shader.uniforms.uSampler = renderer.bindTexture(baseTexture); + + // now lets upload and render the buffers.. + for (let i = 0, j = 0; i < totalChildren; i += batchSize, j += 1) + { + let amount = (totalChildren - i); + + if (amount > batchSize) + { + amount = batchSize; + } + + if (j >= buffers.length) + { + if (!container.autoResize) + { + break; + } + buffers.push(this._generateOneMoreBuffer(container)); + } + + const buffer = buffers[j]; + + // we always upload the dynamic + buffer.uploadDynamic(children, i, amount); + + // we only upload the static content when we have to! + if (container._bufferToUpdate === j) + { + buffer.uploadStatic(children, i, amount); + container._bufferToUpdate = j + 1; + } + + // bind the buffer + renderer.bindVao(buffer.vao); + buffer.vao.draw(gl.TRIANGLES, amount * 6); + } + } + + /** + * Creates one particle buffer for each child in the container we want to render and updates internal properties + * + * @param {PIXI.ParticleContainer} container - The container to render using this ParticleRenderer + * @return {PIXI.ParticleBuffer[]} The buffers + */ + generateBuffers(container) + { + const gl = this.renderer.gl; + const buffers = []; + const size = container._maxSize; + const batchSize = container._batchSize; + const dynamicPropertyFlags = container._properties; + + for (let i = 0; i < size; i += batchSize) + { + buffers.push(new ParticleBuffer(gl, this.properties, dynamicPropertyFlags, batchSize)); + } + + return buffers; + } + + /** + * Creates one more particle buffer, because container has autoResize feature + * + * @param {PIXI.ParticleContainer} container - The container to render using this ParticleRenderer + * @return {PIXI.ParticleBuffer} generated buffer + * @private + */ + _generateOneMoreBuffer(container) + { + const gl = this.renderer.gl; + const batchSize = container._batchSize; + const dynamicPropertyFlags = container._properties; + + return new ParticleBuffer(gl, this.properties, dynamicPropertyFlags, batchSize); + } + + /** + * Uploads the verticies. + * + * @param {PIXI.DisplayObject[]} children - the array of display objects to render + * @param {number} startIndex - the index to start from in the children array + * @param {number} amount - the amount of children that will have their vertices uploaded + * @param {number[]} array - The vertices to upload. + * @param {number} stride - Stride to use for iteration. + * @param {number} offset - Offset to start at. + */ + uploadVertices(children, startIndex, amount, array, stride, offset) + { + let w0 = 0; + let w1 = 0; + let h0 = 0; + let h1 = 0; + + for (let i = 0; i < amount; ++i) + { + const sprite = children[startIndex + i]; + const texture = sprite._texture; + const sx = sprite.scale.x; + const sy = sprite.scale.y; + const trim = texture.trim; + const orig = texture.orig; + + if (trim) + { + // if the sprite is trimmed and is not a tilingsprite then we need to add the + // extra space before transforming the sprite coords.. + w1 = trim.x - (sprite.anchor.x * orig.width); + w0 = w1 + trim.width; + + h1 = trim.y - (sprite.anchor.y * orig.height); + h0 = h1 + trim.height; + } + else + { + w0 = (orig.width) * (1 - sprite.anchor.x); + w1 = (orig.width) * -sprite.anchor.x; + + h0 = orig.height * (1 - sprite.anchor.y); + h1 = orig.height * -sprite.anchor.y; + } + + array[offset] = w1 * sx; + array[offset + 1] = h1 * sy; + + array[offset + stride] = w0 * sx; + array[offset + stride + 1] = h1 * sy; + + array[offset + (stride * 2)] = w0 * sx; + array[offset + (stride * 2) + 1] = h0 * sy; + + array[offset + (stride * 3)] = w1 * sx; + array[offset + (stride * 3) + 1] = h0 * sy; + + offset += stride * 4; + } + } + + /** + * + * @param {PIXI.DisplayObject[]} children - the array of display objects to render + * @param {number} startIndex - the index to start from in the children array + * @param {number} amount - the amount of children that will have their positions uploaded + * @param {number[]} array - The vertices to upload. + * @param {number} stride - Stride to use for iteration. + * @param {number} offset - Offset to start at. + */ + uploadPosition(children, startIndex, amount, array, stride, offset) + { + for (let i = 0; i < amount; i++) + { + const spritePosition = children[startIndex + i].position; + + array[offset] = spritePosition.x; + array[offset + 1] = spritePosition.y; + + array[offset + stride] = spritePosition.x; + array[offset + stride + 1] = spritePosition.y; + + array[offset + (stride * 2)] = spritePosition.x; + array[offset + (stride * 2) + 1] = spritePosition.y; + + array[offset + (stride * 3)] = spritePosition.x; + array[offset + (stride * 3) + 1] = spritePosition.y; + + offset += stride * 4; + } + } + + /** + * + * @param {PIXI.DisplayObject[]} children - the array of display objects to render + * @param {number} startIndex - the index to start from in the children array + * @param {number} amount - the amount of children that will have their rotation uploaded + * @param {number[]} array - The vertices to upload. + * @param {number} stride - Stride to use for iteration. + * @param {number} offset - Offset to start at. + */ + uploadRotation(children, startIndex, amount, array, stride, offset) + { + for (let i = 0; i < amount; i++) + { + const spriteRotation = children[startIndex + i].rotation; + + array[offset] = spriteRotation; + array[offset + stride] = spriteRotation; + array[offset + (stride * 2)] = spriteRotation; + array[offset + (stride * 3)] = spriteRotation; + + offset += stride * 4; + } + } + + /** + * + * @param {PIXI.DisplayObject[]} children - the array of display objects to render + * @param {number} startIndex - the index to start from in the children array + * @param {number} amount - the amount of children that will have their rotation uploaded + * @param {number[]} array - The vertices to upload. + * @param {number} stride - Stride to use for iteration. + * @param {number} offset - Offset to start at. + */ + uploadUvs(children, startIndex, amount, array, stride, offset) + { + for (let i = 0; i < amount; ++i) + { + const textureUvs = children[startIndex + i]._texture._uvs; + + if (textureUvs) + { + array[offset] = textureUvs.x0; + array[offset + 1] = textureUvs.y0; + + array[offset + stride] = textureUvs.x1; + array[offset + stride + 1] = textureUvs.y1; + + array[offset + (stride * 2)] = textureUvs.x2; + array[offset + (stride * 2) + 1] = textureUvs.y2; + + array[offset + (stride * 3)] = textureUvs.x3; + array[offset + (stride * 3) + 1] = textureUvs.y3; + + offset += stride * 4; + } + else + { + // TODO you know this can be easier! + array[offset] = 0; + array[offset + 1] = 0; + + array[offset + stride] = 0; + array[offset + stride + 1] = 0; + + array[offset + (stride * 2)] = 0; + array[offset + (stride * 2) + 1] = 0; + + array[offset + (stride * 3)] = 0; + array[offset + (stride * 3) + 1] = 0; + + offset += stride * 4; + } + } + } + + /** + * + * @param {PIXI.DisplayObject[]} children - the array of display objects to render + * @param {number} startIndex - the index to start from in the children array + * @param {number} amount - the amount of children that will have their rotation uploaded + * @param {number[]} array - The vertices to upload. + * @param {number} stride - Stride to use for iteration. + * @param {number} offset - Offset to start at. + */ + uploadTint(children, startIndex, amount, array, stride, offset) + { + for (let i = 0; i < amount; ++i) + { + const sprite = children[startIndex + i]; + const premultiplied = sprite._texture.baseTexture.premultiplyAlpha; + const alpha = sprite.alpha; + // we dont call extra function if alpha is 1.0, that's faster + const argb = alpha < 1.0 && premultiplied ? premultiplyTint(sprite._tintRGB, alpha) + : sprite._tintRGB + (alpha * 255 << 24); + + array[offset] = argb; + array[offset + stride] = argb; + array[offset + (stride * 2)] = argb; + array[offset + (stride * 3)] = argb; + + offset += stride * 4; + } + } + + /** + * Destroys the ParticleRenderer. + * + */ + destroy() + { + if (this.renderer.gl) + { + this.renderer.gl.deleteBuffer(this.indexBuffer); + } + + super.destroy(); + + this.shader.destroy(); + + this.indices = null; + this.tempMatrix = null; + } +} diff --git a/packages/particles/src/ParticleShader.js b/packages/particles/src/ParticleShader.js new file mode 100644 index 0000000..069afe6 --- /dev/null +++ b/packages/particles/src/ParticleShader.js @@ -0,0 +1,21 @@ +import { GLShader } from 'pixi-gl-core'; +import { PRECISION } from '@pixi/constants'; +import vertex from './particles.vert'; +import fragment from './particles.frag'; + +/** + * @class + * @extends PIXI.Shader + * @memberof PIXI + * @private + */ +export default class ParticleShader extends GLShader +{ + /** + * @param {PIXI.Shader} gl - The webgl shader manager this shader works for. + */ + constructor(gl) + { + super(gl, vertex, fragment, PRECISION.DEFAULT); + } +} diff --git a/packages/particles/src/index.js b/packages/particles/src/index.js index 629d599..d315119 100644 --- a/packages/particles/src/index.js +++ b/packages/particles/src/index.js @@ -1,5 +1,2 @@ -/** - * @namespace PIXI.particles - */ export { default as ParticleContainer } from './ParticleContainer'; -export { default as ParticleRenderer } from './webgl/ParticleRenderer'; +export { default as ParticleRenderer } from './ParticleRenderer'; diff --git a/packages/particles/src/particles.frag b/packages/particles/src/particles.frag new file mode 100644 index 0000000..a725046 --- /dev/null +++ b/packages/particles/src/particles.frag @@ -0,0 +1,9 @@ +varying vec2 vTextureCoord; +varying vec4 vColor; + +uniform sampler2D uSampler; + +void main(void){ + vec4 color = texture2D(uSampler, vTextureCoord) * vColor; + gl_FragColor = color; +} \ No newline at end of file diff --git a/packages/particles/src/particles.vert b/packages/particles/src/particles.vert new file mode 100644 index 0000000..bd5401c --- /dev/null +++ b/packages/particles/src/particles.vert @@ -0,0 +1,26 @@ +attribute vec2 aVertexPosition; +attribute vec2 aTextureCoord; +attribute vec4 aColor; + +attribute vec2 aPositionCoord; +attribute vec2 aScale; +attribute float aRotation; + +uniform mat3 projectionMatrix; +uniform vec4 uColor; + +varying vec2 vTextureCoord; +varying vec4 vColor; + +void main(void){ + float x = (aVertexPosition.x) * cos(aRotation) - (aVertexPosition.y) * sin(aRotation); + float y = (aVertexPosition.x) * sin(aRotation) + (aVertexPosition.y) * cos(aRotation); + + vec2 v = vec2(x, y); + v = v + aPositionCoord; + + gl_Position = vec4((projectionMatrix * vec3(v, 1.0)).xy, 0.0, 1.0); + + vTextureCoord = aTextureCoord; + vColor = aColor * uColor; +} \ No newline at end of file diff --git a/packages/particles/src/webgl/ParticleBuffer.js b/packages/particles/src/webgl/ParticleBuffer.js deleted file mode 100644 index 6306bda..0000000 --- a/packages/particles/src/webgl/ParticleBuffer.js +++ /dev/null @@ -1,268 +0,0 @@ -// import { VertexArrayObject } from 'pixi-gl-core'; -import { createIndicesForQuads } from '@pixi/utils'; -import { GLBuffer } from 'pixi-gl-core'; - -/** - * @author Mat Groves - * - * Big thanks to the very clever Matt DesLauriers https://github.com/mattdesl/ - * for creating the original PixiJS version! - * Also a thanks to https://github.com/bchevalier for tweaking the tint and alpha so that - * they now share 4 bytes on the vertex buffer - * - * Heavily inspired by LibGDX's ParticleBuffer: - * https://github.com/libgdx/libgdx/blob/master/gdx/src/com/badlogic/gdx/graphics/g2d/ParticleBuffer.java - */ - -/** - * The particle buffer manages the static and dynamic buffers for a particle container. - * - * @class - * @private - * @memberof PIXI - */ -export default class ParticleBuffer -{ - /** - * @param {WebGLRenderingContext} gl - The rendering context. - * @param {object} properties - The properties to upload. - * @param {boolean[]} dynamicPropertyFlags - Flags for which properties are dynamic. - * @param {number} size - The size of the batch. - */ - constructor(gl, properties, dynamicPropertyFlags, size) - { - /** - * The current WebGL drawing context. - * - * @member {WebGLRenderingContext} - */ - this.gl = gl; - - /** - * The number of particles the buffer can hold - * - * @member {number} - */ - this.size = size; - - /** - * A list of the properties that are dynamic. - * - * @member {object[]} - */ - this.dynamicProperties = []; - - /** - * A list of the properties that are static. - * - * @member {object[]} - */ - this.staticProperties = []; - - for (let i = 0; i < properties.length; ++i) - { - let property = properties[i]; - - // Make copy of properties object so that when we edit the offset it doesn't - // change all other instances of the object literal - property = { - attribute: property.attribute, - size: property.size, - uploadFunction: property.uploadFunction, - unsignedByte: property.unsignedByte, - offset: property.offset, - }; - - if (dynamicPropertyFlags[i]) - { - this.dynamicProperties.push(property); - } - else - { - this.staticProperties.push(property); - } - } - - this.staticStride = 0; - this.staticBuffer = null; - this.staticData = null; - this.staticDataUint32 = null; - - this.dynamicStride = 0; - this.dynamicBuffer = null; - this.dynamicData = null; - this.dynamicDataUint32 = null; - - this.initBuffers(); - } - - /** - * Sets up the renderer context and necessary buffers. - * - * @private - */ - initBuffers() - { - const gl = this.gl; - let dynamicOffset = 0; - - /** - * Holds the indices of the geometry (quads) to draw - * - * @member {Uint16Array} - */ - this.indices = createIndicesForQuads(this.size); - this.indexBuffer = GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); - - this.dynamicStride = 0; - - for (let i = 0; i < this.dynamicProperties.length; ++i) - { - const property = this.dynamicProperties[i]; - - property.offset = dynamicOffset; - dynamicOffset += property.size; - this.dynamicStride += property.size; - } - - this.dynamicData = new Float32Array(this.size * this.dynamicStride * 4); - this.dynamicBuffer = GLBuffer.createVertexBuffer(gl, this.dynamicData, gl.STREAM_DRAW); - - // static // - let staticOffset = 0; - - this.staticStride = 0; - - for (let i = 0; i < this.staticProperties.length; ++i) - { - const property = this.staticProperties[i]; - - property.offset = staticOffset; - staticOffset += property.size; - this.staticStride += property.size; - } - - this.staticData = new Float32Array(this.size * this.staticStride * 4); - this.staticBuffer = GLBuffer.createVertexBuffer(gl, this.staticData, gl.STATIC_DRAW); - - // this.vao = new VertexArrayObject(gl) - // .addIndex(this.indexBuffer); - - for (let i = 0; i < this.dynamicProperties.length; ++i) - { - const property = this.dynamicProperties[i]; - - if (property.unsignedByte) - { - this.vao.addAttribute( - this.dynamicBuffer, - property.attribute, - gl.UNSIGNED_BYTE, - true, - this.dynamicStride * 4, - property.offset * 4 - ); - } - else - { - this.vao.addAttribute( - this.dynamicBuffer, - property.attribute, - gl.FLOAT, - false, - this.dynamicStride * 4, - property.offset * 4 - ); - } - } - - for (let i = 0; i < this.staticProperties.length; ++i) - { - const property = this.staticProperties[i]; - - if (property.unsignedByte) - { - this.vao.addAttribute( - this.staticBuffer, - property.attribute, - gl.UNSIGNED_BYTE, - true, - this.staticStride * 4, - property.offset * 4 - ); - } - else - { - this.vao.addAttribute( - this.staticBuffer, - property.attribute, - gl.FLOAT, - false, - this.staticStride * 4, - property.offset * 4 - ); - } - } - } - - /** - * Uploads the dynamic properties. - * - * @param {PIXI.DisplayObject[]} children - The children to upload. - * @param {number} startIndex - The index to start at. - * @param {number} amount - The number to upload. - */ - uploadDynamic(children, startIndex, amount) - { - for (let i = 0; i < this.dynamicProperties.length; i++) - { - const property = this.dynamicProperties[i]; - - property.uploadFunction(children, startIndex, amount, - property.unsignedByte ? this.dynamicDataUint32 : this.dynamicData, - this.dynamicStride, property.offset); - } - - this.dynamicBuffer.upload(); - } - - /** - * Uploads the static properties. - * - * @param {PIXI.DisplayObject[]} children - The children to upload. - * @param {number} startIndex - The index to start at. - * @param {number} amount - The number to upload. - */ - uploadStatic(children, startIndex, amount) - { - for (let i = 0; i < this.staticProperties.length; i++) - { - const property = this.staticProperties[i]; - - property.uploadFunction(children, startIndex, amount, - property.unsignedByte ? this.staticDataUint32 : this.staticData, - this.staticStride, property.offset); - } - - this.staticBuffer.upload(); - } - - /** - * Destroys the ParticleBuffer. - * - */ - destroy() - { - this.dynamicProperties = null; - this.dynamicBuffer.destroy(); - this.dynamicBuffer = null; - this.dynamicData = null; - this.dynamicDataUint32 = null; - - this.staticProperties = null; - this.staticBuffer.destroy(); - this.staticBuffer = null; - this.staticData = null; - this.staticDataUint32 = null; - } -} diff --git a/packages/particles/src/webgl/ParticleRenderer.js b/packages/particles/src/webgl/ParticleRenderer.js deleted file mode 100644 index 8e50994..0000000 --- a/packages/particles/src/webgl/ParticleRenderer.js +++ /dev/null @@ -1,458 +0,0 @@ -import { ObjectRenderer, Renderer } from '@pixi/core'; -import { correctBlendMode, premultiplyRgba, premultiplyTint } from '@pixi/utils'; -import { Matrix } from '@pixi/math'; -import ParticleShader from './ParticleShader'; -import ParticleBuffer from './ParticleBuffer'; - -/** - * @author Mat Groves - * - * Big thanks to the very clever Matt DesLauriers https://github.com/mattdesl/ - * for creating the original PixiJS version! - * Also a thanks to https://github.com/bchevalier for tweaking the tint and alpha so that they now - * share 4 bytes on the vertex buffer - * - * Heavily inspired by LibGDX's ParticleRenderer: - * https://github.com/libgdx/libgdx/blob/master/gdx/src/com/badlogic/gdx/graphics/g2d/ParticleRenderer.java - */ - -/** - * - * @class - * @private - * @memberof PIXI - */ -export default class ParticleRenderer extends ObjectRenderer -{ - /** - * @param {PIXI.Renderer} renderer - The renderer this sprite batch works for. - */ - constructor(renderer) - { - super(renderer); - - // 65535 is max vertex index in the index buffer (see ParticleRenderer) - // so max number of particles is 65536 / 4 = 16384 - // and max number of element in the index buffer is 16384 * 6 = 98304 - // Creating a full index buffer, overhead is 98304 * 2 = 196Ko - // let numIndices = 98304; - - /** - * The default shader that is used if a sprite doesn't have a more specific one. - * - * @member {PIXI.Shader} - */ - this.shader = null; - - this.indexBuffer = null; - - this.properties = null; - - this.tempMatrix = new Matrix(); - - this.CONTEXT_UID = 0; - } - - /** - * When there is a WebGL context change - * - * @private - */ - onContextChange() - { - const gl = this.renderer.gl; - - this.CONTEXT_UID = this.renderer.CONTEXT_UID; - - // setup default shader - this.shader = new ParticleShader(gl); - - this.properties = [ - // verticesData - { - attribute: this.shader.attributes.aVertexPosition, - size: 2, - uploadFunction: this.uploadVertices, - offset: 0, - }, - // positionData - { - attribute: this.shader.attributes.aPositionCoord, - size: 2, - uploadFunction: this.uploadPosition, - offset: 0, - }, - // rotationData - { - attribute: this.shader.attributes.aRotation, - size: 1, - uploadFunction: this.uploadRotation, - offset: 0, - }, - // uvsData - { - attribute: this.shader.attributes.aTextureCoord, - size: 2, - uploadFunction: this.uploadUvs, - offset: 0, - }, - // tintData - { - attribute: this.shader.attributes.aColor, - size: 1, - unsignedByte: true, - uploadFunction: this.uploadTint, - offset: 0, - }, - ]; - } - - /** - * Starts a new particle batch. - * - */ - start() - { - this.renderer._bindGLShader(this.shader); - } - - /** - * Renders the particle container object. - * - * @param {PIXI.ParticleContainer} container - The container to render using this ParticleRenderer - */ - render(container) - { - const children = container.children; - const maxSize = container._maxSize; - const batchSize = container._batchSize; - const renderer = this.renderer; - let totalChildren = children.length; - - if (totalChildren === 0) - { - return; - } - else if (totalChildren > maxSize) - { - totalChildren = maxSize; - } - - let buffers = container._glBuffers[renderer.CONTEXT_UID]; - - if (!buffers) - { - buffers = container._glBuffers[renderer.CONTEXT_UID] = this.generateBuffers(container); - } - - const baseTexture = children[0]._texture.baseTexture; - - // if the uvs have not updated then no point rendering just yet! - this.renderer.setBlendMode(correctBlendMode(container.blendMode, baseTexture.premultiplyAlpha)); - - const gl = renderer.gl; - - const m = container.worldTransform.copy(this.tempMatrix); - - m.prepend(renderer._activeRenderTarget.projectionMatrix); - - this.shader.uniforms.projectionMatrix = m.toArray(true); - - this.shader.uniforms.uColor = premultiplyRgba(container.tintRgb, - container.worldAlpha, this.shader.uniforms.uColor, baseTexture.premultiplyAlpha); - - // make sure the texture is bound.. - this.shader.uniforms.uSampler = renderer.bindTexture(baseTexture); - - // now lets upload and render the buffers.. - for (let i = 0, j = 0; i < totalChildren; i += batchSize, j += 1) - { - let amount = (totalChildren - i); - - if (amount > batchSize) - { - amount = batchSize; - } - - if (j >= buffers.length) - { - if (!container.autoResize) - { - break; - } - buffers.push(this._generateOneMoreBuffer(container)); - } - - const buffer = buffers[j]; - - // we always upload the dynamic - buffer.uploadDynamic(children, i, amount); - - // we only upload the static content when we have to! - if (container._bufferToUpdate === j) - { - buffer.uploadStatic(children, i, amount); - container._bufferToUpdate = j + 1; - } - - // bind the buffer - renderer.bindVao(buffer.vao); - buffer.vao.draw(gl.TRIANGLES, amount * 6); - } - } - - /** - * Creates one particle buffer for each child in the container we want to render and updates internal properties - * - * @param {PIXI.ParticleContainer} container - The container to render using this ParticleRenderer - * @return {PIXI.ParticleBuffer[]} The buffers - */ - generateBuffers(container) - { - const gl = this.renderer.gl; - const buffers = []; - const size = container._maxSize; - const batchSize = container._batchSize; - const dynamicPropertyFlags = container._properties; - - for (let i = 0; i < size; i += batchSize) - { - buffers.push(new ParticleBuffer(gl, this.properties, dynamicPropertyFlags, batchSize)); - } - - return buffers; - } - - /** - * Creates one more particle buffer, because container has autoResize feature - * - * @param {PIXI.ParticleContainer} container - The container to render using this ParticleRenderer - * @return {PIXI.ParticleBuffer} generated buffer - * @private - */ - _generateOneMoreBuffer(container) - { - const gl = this.renderer.gl; - const batchSize = container._batchSize; - const dynamicPropertyFlags = container._properties; - - return new ParticleBuffer(gl, this.properties, dynamicPropertyFlags, batchSize); - } - - /** - * Uploads the verticies. - * - * @param {PIXI.DisplayObject[]} children - the array of display objects to render - * @param {number} startIndex - the index to start from in the children array - * @param {number} amount - the amount of children that will have their vertices uploaded - * @param {number[]} array - The vertices to upload. - * @param {number} stride - Stride to use for iteration. - * @param {number} offset - Offset to start at. - */ - uploadVertices(children, startIndex, amount, array, stride, offset) - { - let w0 = 0; - let w1 = 0; - let h0 = 0; - let h1 = 0; - - for (let i = 0; i < amount; ++i) - { - const sprite = children[startIndex + i]; - const texture = sprite._texture; - const sx = sprite.scale.x; - const sy = sprite.scale.y; - const trim = texture.trim; - const orig = texture.orig; - - if (trim) - { - // if the sprite is trimmed and is not a tilingsprite then we need to add the - // extra space before transforming the sprite coords.. - w1 = trim.x - (sprite.anchor.x * orig.width); - w0 = w1 + trim.width; - - h1 = trim.y - (sprite.anchor.y * orig.height); - h0 = h1 + trim.height; - } - else - { - w0 = (orig.width) * (1 - sprite.anchor.x); - w1 = (orig.width) * -sprite.anchor.x; - - h0 = orig.height * (1 - sprite.anchor.y); - h1 = orig.height * -sprite.anchor.y; - } - - array[offset] = w1 * sx; - array[offset + 1] = h1 * sy; - - array[offset + stride] = w0 * sx; - array[offset + stride + 1] = h1 * sy; - - array[offset + (stride * 2)] = w0 * sx; - array[offset + (stride * 2) + 1] = h0 * sy; - - array[offset + (stride * 3)] = w1 * sx; - array[offset + (stride * 3) + 1] = h0 * sy; - - offset += stride * 4; - } - } - - /** - * - * @param {PIXI.DisplayObject[]} children - the array of display objects to render - * @param {number} startIndex - the index to start from in the children array - * @param {number} amount - the amount of children that will have their positions uploaded - * @param {number[]} array - The vertices to upload. - * @param {number} stride - Stride to use for iteration. - * @param {number} offset - Offset to start at. - */ - uploadPosition(children, startIndex, amount, array, stride, offset) - { - for (let i = 0; i < amount; i++) - { - const spritePosition = children[startIndex + i].position; - - array[offset] = spritePosition.x; - array[offset + 1] = spritePosition.y; - - array[offset + stride] = spritePosition.x; - array[offset + stride + 1] = spritePosition.y; - - array[offset + (stride * 2)] = spritePosition.x; - array[offset + (stride * 2) + 1] = spritePosition.y; - - array[offset + (stride * 3)] = spritePosition.x; - array[offset + (stride * 3) + 1] = spritePosition.y; - - offset += stride * 4; - } - } - - /** - * - * @param {PIXI.DisplayObject[]} children - the array of display objects to render - * @param {number} startIndex - the index to start from in the children array - * @param {number} amount - the amount of children that will have their rotation uploaded - * @param {number[]} array - The vertices to upload. - * @param {number} stride - Stride to use for iteration. - * @param {number} offset - Offset to start at. - */ - uploadRotation(children, startIndex, amount, array, stride, offset) - { - for (let i = 0; i < amount; i++) - { - const spriteRotation = children[startIndex + i].rotation; - - array[offset] = spriteRotation; - array[offset + stride] = spriteRotation; - array[offset + (stride * 2)] = spriteRotation; - array[offset + (stride * 3)] = spriteRotation; - - offset += stride * 4; - } - } - - /** - * - * @param {PIXI.DisplayObject[]} children - the array of display objects to render - * @param {number} startIndex - the index to start from in the children array - * @param {number} amount - the amount of children that will have their rotation uploaded - * @param {number[]} array - The vertices to upload. - * @param {number} stride - Stride to use for iteration. - * @param {number} offset - Offset to start at. - */ - uploadUvs(children, startIndex, amount, array, stride, offset) - { - for (let i = 0; i < amount; ++i) - { - const textureUvs = children[startIndex + i]._texture._uvs; - - if (textureUvs) - { - array[offset] = textureUvs.x0; - array[offset + 1] = textureUvs.y0; - - array[offset + stride] = textureUvs.x1; - array[offset + stride + 1] = textureUvs.y1; - - array[offset + (stride * 2)] = textureUvs.x2; - array[offset + (stride * 2) + 1] = textureUvs.y2; - - array[offset + (stride * 3)] = textureUvs.x3; - array[offset + (stride * 3) + 1] = textureUvs.y3; - - offset += stride * 4; - } - else - { - // TODO you know this can be easier! - array[offset] = 0; - array[offset + 1] = 0; - - array[offset + stride] = 0; - array[offset + stride + 1] = 0; - - array[offset + (stride * 2)] = 0; - array[offset + (stride * 2) + 1] = 0; - - array[offset + (stride * 3)] = 0; - array[offset + (stride * 3) + 1] = 0; - - offset += stride * 4; - } - } - } - - /** - * - * @param {PIXI.DisplayObject[]} children - the array of display objects to render - * @param {number} startIndex - the index to start from in the children array - * @param {number} amount - the amount of children that will have their rotation uploaded - * @param {number[]} array - The vertices to upload. - * @param {number} stride - Stride to use for iteration. - * @param {number} offset - Offset to start at. - */ - uploadTint(children, startIndex, amount, array, stride, offset) - { - for (let i = 0; i < amount; ++i) - { - const sprite = children[startIndex + i]; - const premultiplied = sprite._texture.baseTexture.premultiplyAlpha; - const alpha = sprite.alpha; - // we dont call extra function if alpha is 1.0, that's faster - const argb = alpha < 1.0 && premultiplied ? premultiplyTint(sprite._tintRGB, alpha) - : sprite._tintRGB + (alpha * 255 << 24); - - array[offset] = argb; - array[offset + stride] = argb; - array[offset + (stride * 2)] = argb; - array[offset + (stride * 3)] = argb; - - offset += stride * 4; - } - } - - /** - * Destroys the ParticleRenderer. - * - */ - destroy() - { - if (this.renderer.gl) - { - this.renderer.gl.deleteBuffer(this.indexBuffer); - } - - super.destroy(); - - this.shader.destroy(); - - this.indices = null; - this.tempMatrix = null; - } -} - -Renderer.registerPlugin('particle', ParticleRenderer); diff --git a/packages/particles/src/webgl/ParticleShader.js b/packages/particles/src/webgl/ParticleShader.js deleted file mode 100644 index 2caeec5..0000000 --- a/packages/particles/src/webgl/ParticleShader.js +++ /dev/null @@ -1,62 +0,0 @@ -import { GLShader } from 'pixi-gl-core'; -import { PRECISION } from '@pixi/constants'; - -/** - * @class - * @extends PIXI.Shader - * @memberof PIXI - */ -export default class ParticleShader extends GLShader -{ - /** - * @param {PIXI.Shader} gl - The webgl shader manager this shader works for. - */ - constructor(gl) - { - super( - gl, - // vertex shader - [ - 'attribute vec2 aVertexPosition;', - 'attribute vec2 aTextureCoord;', - 'attribute vec4 aColor;', - - 'attribute vec2 aPositionCoord;', - 'attribute vec2 aScale;', - 'attribute float aRotation;', - - 'uniform mat3 projectionMatrix;', - 'uniform vec4 uColor;', - - 'varying vec2 vTextureCoord;', - 'varying vec4 vColor;', - - 'void main(void){', - ' float x = (aVertexPosition.x) * cos(aRotation) - (aVertexPosition.y) * sin(aRotation);', - ' float y = (aVertexPosition.x) * sin(aRotation) + (aVertexPosition.y) * cos(aRotation);', - - ' vec2 v = vec2(x, y);', - ' v = v + aPositionCoord;', - - ' gl_Position = vec4((projectionMatrix * vec3(v, 1.0)).xy, 0.0, 1.0);', - - ' vTextureCoord = aTextureCoord;', - ' vColor = aColor * uColor;', - '}', - ].join('\n'), - // hello - [ - 'varying vec2 vTextureCoord;', - 'varying vec4 vColor;', - - 'uniform sampler2D uSampler;', - - 'void main(void){', - ' vec4 color = texture2D(uSampler, vTextureCoord) * vColor;', - ' gl_FragColor = color;', - '}', - ].join('\n'), - PRECISION.DEFAULT - ); - } -} diff --git a/rollup.config.js b/rollup.config.js index c480ca4..8809677 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -41,7 +41,7 @@ commonjs({ namedExports: { 'resource-loader': ['Resource'], - 'pixi-gl-core': ['GLFramebuffer'], // TODO: remove pixi-gl-core + 'pixi-gl-core': ['GLFramebuffer', 'GLShader', 'GLBuffer'], // TODO: remove pixi-gl-core }, }), string({