UNPKG

pixi-batch-renderer

Version:

Batch rendering library for PixiJS applications

1,754 lines (1,450 loc) 77.9 kB
/* eslint-disable */ /*! * pixi-batch-renderer - v3.0.0 * Compiled Sun, 05 Mar 2023 04:07:30 UTC * * pixi-batch-renderer is licensed under the MIT License. * http://www.opensource.org/licenses/mit-license * * Copyright 2019-2020, Shukant Pal <shukantpal@outlook.com>, All Rights Reserved */ import { ViewableBuffer, Geometry, Buffer, ObjectRenderer, State, Shader, BaseTexture } from '@pixi/core'; import { TYPES, ENV } from '@pixi/constants'; import { nextPow2, log2 } from '@pixi/utils'; import { settings } from '@pixi/settings'; import { Point, Matrix } from '@pixi/math'; /** * Redirects are used to aggregate the resources needed by the WebGL pipeline to render * a display-object. This includes the base primitives (geometry), uniforms, and * textures (which are handled as "special" uniforms). * * @see AttributeRedirect */ class Redirect { /** * The property on the display-object that holds the resource. * * Instead of a property, you can provide a callback that generates the resource * on invokation. */ /** The shader variable that references the resource, e.g. attribute or uniform name. */ constructor(source, glslIdentifer) { this.source = source; this.glslIdentifer = glslIdentifer; } } /** * This redirect defines an attribute of a display-object's geometry. The attribute * data is expected to be stored in a `PIXI.ViewableBuffer`, in an array, or (if * just one element) as the property itself. * * @example * // This attribute redirect calculates the tint used on top of a texture. Since the * // tintMode can change anytime, it is better to use a derived source (function). * // * // Furthermore, the color is uploaded as four bytes (`attribute vec4 aTint`) while the * // source returns an integer. This is done by splitting the 32-bit integer into four * // 8-bit bytes. * new AttributeRedirect({ * source: (tgt: ExampleDisplay) => (tgt.alpha < 1.0 && tgt.tintMode === PREMULTIPLY) * ? premultiplyTint(tgt.rgb, tgt.alpha) * : tgt.rgb + (tgt.alpha << 24); * attrib: 'aTint', * type: 'int32', * size: '%notarray%', // optional/default * glType: PIXI.TYPES.UNSIGNED_BYTE, * glSize: 4, * normalize: true // We are using [0, 255] range for RGBA here. Must normalize to [0, 1]. * }); */ class AttributeRedirect extends Redirect { /** * The type of data stored in the source buffer. This can be any of: `int8`, `uint8`, * `int16`, `uint16`, `int32`, `uint32`, or (by default) `float32`. * * @member {string} * @see [PIXI.ViewableBuffer#view]{@link https://pixijs.download/dev/docs/PIXI.ViewableBuffer.html} * @default 'float32' */ /** * Number of elements to extract out of `source` with * the given view type, for one vertex. * * If source isn't an array (only one element), then * you can set this to `'%notarray%'`. * * @member {number | '%notarray%'} */ /** * Type of attribute, when uploading. * * Normally, you would use the corresponding type for * the view on source. However, to speed up uploads * you can aggregate attribute values in larger data * types. For example, an RGBA vec4 (byte-sized channels) * can be represented as one `Uint32`, while having * a `glType` of `UNSIGNED_BYTE`. */ /** * Size of attribute in terms of `glType`. * * Note that `glSize * glType <= size * type` * * @readonly */ /** * Whether to normalize the attribute values. * * @readonly */ /** This is equal to `size` or 1 if size is `%notarray%`. */ /** * @param {object} options * @param {string | Function} options.source - redirect source * @param {string} options.attrib - shader attribute variable * @param {string}[options.type='float32'] - the type of data stored in the source * @param {number | '%notarray%'}[options.size=0] - size of the source array ('%notarray' if not an array & just one element) * @param {PIXI.TYPES}[options.glType=PIXI.TYPES.FLOAT] - data format to be uploaded in * @param {number} options.glSize - number of elements to be uploaded as (size of source and upload must match) * @param {boolean}[options.normalize=false] - whether to normalize the data before uploading */ constructor(options) { super(options.source, options.attrib); this.type = options.type; this.size = options.size; this.properSize = (options.size === '%notarray%' || options.size === undefined) ? 1 : options.size; this.glType = options.glType; this.glSize = options.glSize; this.normalize = !!options.normalize; } static vertexSizeFor(attributeRedirects) { return attributeRedirects.reduce( (acc, redirect) => (ViewableBuffer.sizeOf(redirect.type) * redirect.properSize) + acc, 0); } } /** * This redirect is used to aggregate & upload uniforms required for shading the * display-object. * * @example * // The data-type of this uniform is defined in your shader. * new UniformRedirect({ * source: (dob: PIXI.DisplayObject) => dob.transform.worldTransform, * uniform: "transform" * }); */ class UniformRedirect extends Redirect { constructor(options) { super(options.source, options.uniform); } } /** * Resources that need to be uploaded to WebGL to render one batch. * * To customize batches, you must create your own batch factory by extending the * {@link StdBatchFactory} class. */ class StdBatch { constructor(geometryOffset) { /** * Index of the first vertex of this batch's geometry in the uploaded geometry. * * @member {number} */ this.geometryOffset = geometryOffset; /** * Textures that are used by the display-object's in this batch. * * @member {Array<PIXI.Texture>} */ this.textureBuffer = null; /** * Map of base-texture UIDs to texture indices into `uSamplers`. * * @member {Map<number, number>} */ this.uidMap = null; /** * State required to render this batch. * * @member {PIXI.State} */ this.state = null; } /** * Uploads the resources required before rendering this batch. If you override * this, you must call `super.upload`. * * @param {PIXI.Renderer} renderer */ upload(renderer) { this.textureBuffer.forEach((tex, i) => { renderer.texture.bind(tex, i); }); renderer.state.set(this.state); } /** * Reset this batch to become "fresh"! */ reset() { this.textureBuffer = this.uidMap = this.state = null; if (this.batchBuffer) { this.batchBuffer.length = 0; } } } /** * Factory for producing "standard" (based on state, geometry, & textures) batches of * display-objects. * * **NOTE:** Instead of "building" batches, this factory actually keeps the batches in * a buffer so they can be accessed together at the end. * * **Shared Textures**: If display-objects in the same batch use the same base-texture, * then that base-texture is not uploaded twice. This allows for more better batch density * when you use texture atlases (textures with same base-texture). This is one reason why * textures are treated as "special" uniforms. */ class StdBatchFactory { /** @internal */ // _putTexture is optimized for the one texture/display-object case. /** * @param renderer */ constructor(renderer) { /** * @protected */ this._renderer = renderer; this._state = null; /** * Textures per display-object * @member {number} */ this._textureCount = renderer._texturesPerObject; /** * Property in which textures are kept of display-objects * @member {string} */ this._textureProperty = renderer._textureProperty; /** * Max. no of textures per batch (should be <= texture units of GPU) * @member {number} */ this._textureLimit = renderer.MAX_TEXTURES; /** * @member {object} */ this._textureBuffer = {}; // uid : texture map this._textureBufferLength = 0; this._textureIndexedBuffer = []; // array of textures this._textureIndexMap = {}; // uid : index in above /** * Display-objects in current batch * @protected */ this._batchBuffer = []; /** * Pool to batch objects into which data is fed. * @member {any[]} * @protected */ this._batchPool = []; /** * Number of batches created since last reset. * @member {number} * @protected */ this._batchCount = 0; if (this._textureCount === 1) { this._putTexture = this._putSingleTexture; } else { this._putTexture = this._putAllTextures; } } /** * Puts the display-object into the current batch, if possible. * * @param targetObject - object to add * @param state - state required by that object * @return whether the object was added to the batch. If it wasn't, you should "build" it. */ put(targetObject, state) { // State compat if (!this._state) { this._state = state; } else if (this._state.data !== state.data) { return false; } // Customized compat if (!this._put(targetObject)) { return false; } // Texture compat if (this._textureCount > 0 && !this._putTexture((targetObject )[this._textureProperty])) { return false; } this._batchBuffer.push(targetObject); return true; } /** * Creates the batch object and pushes it into the pool This also resets any state * so that a new batch can be started again. * * @param batch */ build(geometryOffset) { const batch = this._nextBatch() ; batch.geometryOffset = geometryOffset; this._buildBatch(batch); this._state = null; this._batchBuffer = []; this._textureBuffer = {}; this._textureIndexMap = {}; this._textureBufferLength = 0; this._textureIndexedBuffer = []; } /** * @returns {boolean} - whether this factory is ready to start a new batch from * "start". If not, then the current batch must be built before starting a new one. */ ready() { return this._batchBuffer.length === 0; } /** * Clears the batch pool. */ reset() { this._batchCount = 0; } /** * Returns the built batch pool. The array returned may be larger than the pool * itself. * * @returns {Array<object>} */ access() { return this._batchPool; } /** * Size of the batch pool built since last reset. */ size() { return this._batchCount; } /** * Should store any information from the display-object to be put into * the batch. * * @param displayObject * @returns {boolean} - whether the display-object was "compatible" with * other display-objects in the batch. If not, it should not have been * added. */ // eslint-disable-next-line @typescript-eslint/no-unused-vars _put(_displayObject) { // Override this return true; } /** * @returns {object} a new batch * @protected * @example * _newBatch(): CustomBatch * { * return new CustomBatch(); * } */ _newBatch() { return new StdBatch(); } /** * @param {number} geometryOffset */ _nextBatch(geometryOffset) { if (this._batchCount === this._batchPool.length) { this._batchPool.push(this._newBatch()); } const batch = this._batchPool[this._batchCount++]; batch.reset(); batch.geometryOffset = geometryOffset; return batch; } /** * Should add any information required to render the batch. If you override this, * you must call `super._buildBatch` and clear any state. * @param {object} batch * @protected * @example * _buildBatch(batch: any): void * { * super._buildBatch(batch); * batch.depth = this.generateDepth(); * * // if applicable * this.resetDepthGenerator(); * } */ _buildBatch(batch) { batch.batchBuffer = this._batchBuffer; batch.textureBuffer = this._textureIndexedBuffer; batch.uidMap = this._textureIndexMap; batch.state = this._state; } // Optimized _putTexture case. _putSingleTexture(texture) { if ('baseTexture' in texture) { texture = texture.baseTexture; } const baseTexture = texture ; if (this._textureBuffer[baseTexture.uid]) { return true; } else if (this._textureBufferLength + 1 <= this._textureLimit) { this._textureBuffer[baseTexture.uid] = texture; this._textureBufferLength += 1; const newLength = this._textureIndexedBuffer.push(baseTexture); const index = newLength - 1; this._textureIndexMap[baseTexture.uid] = index; return true; } return false; } _putAllTextures(textureArray) { let deltaBufferLength = 0; for (let i = 0; i < textureArray.length; i++) { const texture = ('baseTexture' in textureArray[i] ? (textureArray[i] ).baseTexture : textureArray[i]) ; if (!this._textureBuffer[texture.uid]) { ++deltaBufferLength; } } if (deltaBufferLength + this._textureBufferLength > this._textureLimit) { return false; } for (let i = 0; i < textureArray.length; i++) { const texture = 'baseTexture' in textureArray[i] ? (textureArray[i] ).baseTexture : (textureArray[i] ); if (!this._textureBuffer[texture.uid]) { this._textureBuffer[texture.uid] = texture; this._textureBufferLength += 1; const newLength = this._textureIndexedBuffer.push(texture); const index = newLength - 1; this._textureIndexMap[texture.uid] = index; } } return true; } } // BatchGeometryFactory uses this class internally to setup the attributes of // the batches. // // Supports Uniforms+Standard Pipeline's in-batch/uniform ID. class BatchGeometry extends Geometry { // Interleaved attribute data buffer // Batched indicies constructor(attributeRedirects, hasIndex, texIDAttrib, texturesPerObject, inBatchIDAttrib, uniformIDAttrib, masterIDAttrib, attributeBuffer, indexBuffer ) { super(); attributeBuffer = attributeBuffer || new Buffer(null, false, false); indexBuffer = indexBuffer || (hasIndex ? new Buffer(null, false, true) : null); attributeRedirects.forEach((redirect) => { const { glslIdentifer, glType, glSize, normalize } = redirect; this.addAttribute(glslIdentifer, attributeBuffer, glSize, normalize, glType); }); if (!masterIDAttrib) { if (texIDAttrib && texturesPerObject > 0) { this.addAttribute(texIDAttrib, attributeBuffer, texturesPerObject, true, TYPES.FLOAT); } if (inBatchIDAttrib) { this.addAttribute(inBatchIDAttrib, attributeBuffer, 1, false, TYPES.FLOAT); } if (uniformIDAttrib) { this.addAttribute(uniformIDAttrib, attributeBuffer, 1, false, TYPES.FLOAT); } } else { this.addAttribute(masterIDAttrib, attributeBuffer, 1, false, TYPES.FLOAT); } if (hasIndex) { this.addIndex(indexBuffer); } this.attribBuffer = attributeBuffer; this.indexBuffer = indexBuffer; } } // To define the constructor shape, this is defined as an abstract class but documented // as an interface. class IBatchGeometryFactory { // eslint-disable-next-line @typescript-eslint/no-useless-constructor, @typescript-eslint/no-unused-vars constructor(renderer) { // Implementation this._renderer = renderer; } } /** * This interface defines the methods you need to implement to creating your own batch * geometry factory. * * The constructor of an implementation should take only one argument - the batch renderer. * * @interface IBatchGeometryFactory */ /** * Called before the batch renderer starts feeding the display-objects. This can be used * to pre-allocated space for the batch geometry. * * @memberof IBatchGeometryFactory# * @method init * @param {number} verticesBatched * @param {number}[indiciesBatched] - optional when display-object's don't use a index buffer */ /** * Adds the display-object to the batch geometry. * * If the display-object's shader also uses textures (in `uSamplers` uniform), then it will * be given a texture-ID to get the texture from the `uSamplers` array. If it uses multiple * textures, then the texture-ID is an array of indices into `uSamplers`. The texture-attrib * passed to the batch renderer sets the name of the texture-ID attribute (defualt is `aTextureId`). * * @memberof IBatchGeometryFactory# * @method append * @param {PIXI.DisplayObject} displayObject * @param {object} batch - the batch */ /** * This should wrap up the batch geometry in a `PIXI.Geometry` object. * * @memberof IBatchGeometryFactory# * @method build * @returns {PIXI.Geometry} batch geometry */ /** * This is used to return a batch geometry so it can be pooled and reused in a future `build()` * call. * * @memberof IBatchGeometryFactory# * @method release * @param {PIXI.Geometry} geom */ /** * Factory class that generates the geometry for a whole batch by feeding on * the individual display-object geometries. This factory is reusable, i.e. you * can build another geometry after a {@link build} call. * * **Optimizations:** To speed up geometry generation, this compiles an optimized * packing function that pushes attributes without looping through the attribute * redirects. * * **Default Format:** If you are not using a custom draw-call issuer, then * the batch geometry must have an interleaved attribute data buffer and one * index buffer. * * **Customization:** If you want to customize the batch geometry, then you must * also define your draw call issuer. * * **inBatchID Support**: If you specified an `inBatchID` attribute in the batch-renderer, * then this will support it automatically. The aggregate-uniforms pipeline doesn't need a custom * geometry factory. */ class BatchGeometryFactory extends IBatchGeometryFactory { // These properties are not protected because GeometryMerger uses them! // Standard Pipeline // Uniform+Standard Pipeline // Master-ID attribute feature /* Set to the indicies of the display-object's textures in uSamplers uniform before invoking geometryMerger(). */ /** * @param renderer */ constructor(renderer) { super(renderer); this._targetCompositeAttributeBuffer = null; this._targetCompositeIndexBuffer = null; this._aIndex = 0; this._iIndex = 0; this._attribRedirects = renderer._attribRedirects; this._indexProperty = renderer._indexProperty; this._vertexCountProperty = renderer._vertexCountProperty; this._vertexSize = AttributeRedirect.vertexSizeFor(this._attribRedirects); this._texturesPerObject = renderer._texturesPerObject; this._textureProperty = renderer._textureProperty; this._texIDAttrib = renderer._texIDAttrib; this._inBatchIDAttrib = renderer._inBatchIDAttrib; this._uniformIDAttrib = renderer._uniformIDAttrib; this._masterIDAttrib = renderer._masterIDAttrib; if (!this._masterIDAttrib) { this._vertexSize += this._texturesPerObject * 4;// texture indices are also passed if (this._inBatchIDAttrib) { this._vertexSize += 4; } if (this._uniformIDAttrib) { this._vertexSize += 4; } } else { this._vertexSize += 4; } if (this._texturesPerObject === 1) { this._texID = 0; } else if (this._texturesPerObject > 1) { this._texID = new Array(this._texturesPerObject); } this._aBuffers = [];// @see _getAttributeBuffer this._iBuffers = [];// @see _getIndexBuffer /** * Batch geometries that can be reused. * * @member {PIXI.Geometry} * @protected * @see IBatchGeometryFactory#release */ this._geometryPool = []; } /** * Ensures this factory has enough space to buffer the given number of vertices * and indices. This should be called before feeding display-objects from the * batch. * * @param {number} verticesBatched * @param {number} indiciesBatched */ init(verticesBatched, indiciesBatched) { this._targetCompositeAttributeBuffer = this.getAttributeBuffer(verticesBatched); if (this._indexProperty) { this._targetCompositeIndexBuffer = this.getIndexBuffer(indiciesBatched); } this._aIndex = this._iIndex = 0; } /** * Append's the display-object geometry to this batch's geometry. You must override * this you need to "modify" the geometry of the display-object before merging into * the composite geometry (for example, adding an ID to a special uniform) */ append(targetObject, batch_) { const batch = batch_ ; const tex = (targetObject )[this._textureProperty]; // GeometryMerger uses _texID for texIDAttrib if (this._texturesPerObject === 1) { const texUID = tex.baseTexture ? tex.baseTexture.uid : tex.uid; this._texID = batch.uidMap[texUID]; } else if (this._texturesPerObject > 1) { let _tex; for (let k = 0; k < tex.length; k++) { _tex = tex[k]; const texUID = _tex.baseTexture ? _tex.baseTexture.uid : _tex.uid; (this._texID )[k] = batch.uidMap[texUID]; } } // GeometryMerger uses this if (this._inBatchIDAttrib || this._uniformIDAttrib) { this._inBatchID = batch.batchBuffer.indexOf(targetObject); } if (this._uniformIDAttrib) { this._uniformID = (batch ).uniformMap[this._inBatchID]; } // If _masterIDAttrib, then it is expected you override this function. this.geometryMerger(targetObject, this); } /** * @override * @returns {PIXI.Geometry} the generated batch geometry * @example * build(): PIXI.Geometry * { * // Make sure you're not allocating new geometries if _geometryPool has some * // already. (Otherwise, a memory leak will result!) * const geom: ExampleGeometry = (this._geometryPool.pop() || new ExampleGeometry( * // ...your arguments... //)) as ExampleGeometry; * * // Put data into geometry's buffer * * return geom; * } */ build() { const geom = (this._geometryPool.pop() || new BatchGeometry( this._attribRedirects, true, this._texIDAttrib, this._texturesPerObject, this._inBatchIDAttrib, this._uniformIDAttrib, this._masterIDAttrib, )) ; // We don't really have to remove the buffers because BatchRenderer won't reuse // the data in these buffers after the next build() call. geom.attribBuffer.update(this._targetCompositeAttributeBuffer.float32View); geom.indexBuffer.update(this._targetCompositeIndexBuffer); return geom; } /** * @param {PIXI.Geometry} geom - releases back the geometry to be reused. It is expected * that it is not used externally again. * @override */ release(geom) { this._geometryPool.push(geom); } /** * This lazy getter returns the geometry-merger function. This function * takes one argument - the display-object to be appended to the batch - * and pushes its geometry to the batch geometry. * * You can overwrite this property with a custom geometry-merger function * if customizing `BatchGeometryFactory`. * * @member {IGeometryMerger} */ get geometryMerger() { if (!this._geometryMerger) { // eslint-disable-next-line @typescript-eslint/no-use-before-define this._geometryMerger = new GeometryMergerFactory(this).compile(); } return this._geometryMerger; } // eslint-disable-next-line require-jsdoc set geometryMerger(func) { this._geometryMerger = func; } /** * @protected */ get _indexCountProperty() { return this._renderer._indexCountProperty; } /** * Allocates an attribute buffer with sufficient capacity to hold `size` elements. * * @param {number} size * @protected */ getAttributeBuffer(size) { const roundedP2 = nextPow2(size); const roundedSizeIndex = log2(roundedP2); const roundedSize = roundedP2; if (this._aBuffers.length <= roundedSizeIndex) { this._aBuffers.length = roundedSizeIndex + 1; } let buffer = this._aBuffers[roundedSizeIndex]; if (!buffer) { this._aBuffers[roundedSizeIndex] = buffer = new ViewableBuffer(roundedSize * this._vertexSize); } return buffer; } /** * Allocates an index buffer (`Uint16Array`) with sufficient capacity to hold `size` indices. * * @param size * @protected */ getIndexBuffer(size) { // 12 indices is enough for 2 quads const roundedP2 = nextPow2(Math.ceil(size / 12)); const roundedSizeIndex = log2(roundedP2); const roundedSize = roundedP2 * 12; if (this._iBuffers.length <= roundedSizeIndex) { this._iBuffers.length = roundedSizeIndex + 1; } let buffer = this._iBuffers[roundedSizeIndex]; if (!buffer) { this._iBuffers[roundedSizeIndex] = buffer = new Uint16Array(roundedSize); } return buffer; } } // GeometryMergerFactory uses these variable names. const CompilerConstants = { INDICES_OFFSET: '__offset_indices_', FUNC_SOURCE_BUFFER: 'getSourceBuffer', // Argument names for the geometryMerger() function. packerArguments: [ 'targetObject', 'factory', ], }; // This was intended to be an inner class of BatchGeometryFactory; however, due to // a bug in JSDoc, it was placed outside. // https://github.com/jsdoc/jsdoc/issues/1673 // Factory for generating a geometry-merger function (which appends the geometry of // a display-object to the batch geometry). const GeometryMergerFactory = class { // We need the BatchGeometryFactory for attribute redirect information. constructor(packer) { this.packer = packer; } compile() { const packer = this.packer; // The function's body/code is placed here. let packerBody = ` const compositeAttributes = factory._targetCompositeAttributeBuffer; const compositeIndices = factory._targetCompositeIndexBuffer; let aIndex = factory._aIndex; let iIndex = factory._iIndex; const textureId = factory._texID; const attributeRedirects = factory._attribRedirects; `; // Define __offset_${i}, the offset of each attribute in the display-object's // geometry, __buffer_${i} the source buffer of the attribute data. packer._attribRedirects.forEach((redirect, i) => { packerBody += ` let __offset_${i} = 0; const __buffer_${i} = ( ${this.generateSourceBufferExpr(redirect, i)}); `; }); // This loops through each vertex in the display-object's geometry and appends // them (attributes are interleaved, so each attribute element is pushed per vertex) packerBody += ` const { int8View, uint8View, int16View, uint16View, int32View, uint32View, float32View, } = compositeAttributes; const vertexCount = ${this.generateVertexCountExpr()}; let adjustedAIndex = 0; for (let vertexIndex = 0; vertexIndex < vertexCount; vertexIndex++) { `; // Eliminate offset conversion when adjacent attributes // have similar source-types. let skipByteIndexConversion = false; // Appends a vertice's attributes (inside the for-loop above). for (let i = 0; i < packer._attribRedirects.length; i++) { const redirect = packer._attribRedirects[i]; // Initialize adjustedAIndex in terms of source type. if (!skipByteIndexConversion) { packerBody += ` adjustedAIndex = aIndex / ${this._sizeOf(i)}; `; } if (typeof redirect.size === 'number') { for (let j = 0; j < redirect.size; j++) { packerBody += ` ${redirect.type}View[adjustedAIndex++] = __buffer_${i}[__offset_${i}++]; `; } } else { packerBody += ` ${redirect.type}View[adjustedAIndex++] = __buffer_${i}; `; } if (packer._attribRedirects[i + 1] && (this._sizeOf(i + 1) !== this._sizeOf(i))) { packerBody += ` aIndex = adjustedAIndex * ${this._sizeOf(i)}; `; } else { skipByteIndexConversion = true; } } if (skipByteIndexConversion) { if (this._sizeOf(packer._attribRedirects.length - 1) !== 4) { packerBody += ` aIndex = adjustedAIndex * ${this._sizeOf(packer._attribRedirects.length - 1)} `; skipByteIndexConversion = false; } } if (!packer._masterIDAttrib) { if (packer._texturesPerObject > 0) { if (packer._texturesPerObject > 1) { if (!skipByteIndexConversion) { packerBody += ` adjustedAIndex = aIndex / 4; `; } for (let k = 0; k < packer._texturesPerObject; k++) { packerBody += ` float32View[adjustedAIndex++] = textureId[${k}]; `; } packerBody += ` aIndex = adjustedAIndex * 4; `; } else if (!skipByteIndexConversion) { packerBody += ` float32View[aIndex / 4] = textureId; `; } else { packerBody += ` float32View[adjustedAIndex++] = textureId; aIndex = adjustedAIndex * 4; `; } } if (packer._inBatchIDAttrib) { packerBody += ` float32View[adjustedAIndex++] = factory._inBatchID; aIndex = adjustedAIndex * 4; `; } if (packer._uniformIDAttrib) { packerBody += ` float32View[adjustedAIndex++] = factory._uniformID; aIndex = adjustedAIndex * 4; `; } } else { if (!skipByteIndexConversion) { packerBody += ` adjustedAIndex = aIndex / 4; `; } packerBody += ` float32View[adjustedAIndex++] = factory._masterID; aIndex = adjustedAIndex * 4; `; } /* Close the packing for-loop. */ packerBody += `} ${this.packer._indexProperty ? `const oldAIndex = this._aIndex;` : ''} this._aIndex = aIndex; `; if (this.packer._indexProperty) { packerBody += ` const verticesBefore = oldAIndex / ${this.packer._vertexSize}; const indexCount = ${this.generateIndexCountExpr()}; for (let j = 0; j < indexCount; j++) { compositeIndices[iIndex++] = verticesBefore + targetObject['${this.packer._indexProperty}'][j]; } this._iIndex = iIndex; `; } // eslint-disable-next-line no-new-func return new Function( ...CompilerConstants.packerArguments, packerBody) ; } // Returns an expression that fetches the attribute data source from // targetObject (DisplayObject). generateSourceBufferExpr(redirect, i) { return (typeof redirect.source === 'string') ? `targetObject['${redirect.source}']` : `attributeRedirects[${i}].source(targetObject, factory._renderer)`; } generateVertexCountExpr() { if (!this.packer._vertexCountProperty) { // auto-calculate based on primary attribute return `__buffer_0.length / ${this.packer._attribRedirects[0].size}`; } if (typeof this.packer._vertexCountProperty === 'function') { return `factory._vertexCountProperty(targetObject)`; } return ( (typeof this.packer._vertexCountProperty === 'string') ? `targetObject.${this.packer._vertexCountProperty}` : `${this.packer._vertexCountProperty}` ); } generateIndexCountExpr() { const idxCountProp = this.packer._indexCountProperty; const idxProp = this.packer._indexProperty; if (!idxCountProp) { return `targetObject['${idxProp}'].length`; } if (typeof idxCountProp === 'function') { return `factory._indexCountProperty(targetObject)`; } else if (typeof idxCountProp === 'string') { return `targetObject['${idxCountProp}']`; } return `${idxCountProp}`; } _sizeOf(i) { return ViewableBuffer.sizeOf( this.packer._attribRedirects[i].type); } }; /** * Executes the final stage of batch rendering - drawing. The drawer can assume that * all display-objects have been into the batch-factory and the batch-geometry factory. */ class BatchDrawer { /** The batch renderer */ constructor(renderer) { this.renderer = renderer; } /** * This method will be called after all display-object have been fed into the * batch and batch-geometry factories. * * **Hint**: You will call some form of `BatchGeometryFactory#build`; be sure to release * that geometry for reuse in next render pass via `BatchGeometryFactory#release(geom)`. */ draw() { const { renderer, _batchFactory: batchFactory, _geometryFactory: geometryFactory, _indexProperty: indexProperty, } = this.renderer; const batchList = batchFactory.access(); const batchCount = batchFactory.size(); const geom = geometryFactory.build(); const { gl } = renderer; // PixiJS bugs - the shader can't be bound before uploading because uniform sync caching // and geometry requires the shader to be bound. batchList[0].upload(renderer); renderer.shader.bind(this.renderer._shader, false); renderer.geometry.bind(geom); for (let i = 0; i < batchCount; i++) { const batch = batchList[i]; batch.upload(renderer); renderer.shader.bind(this.renderer._shader, false); if (indexProperty) { // TODO: Get rid of the $indexCount black magic! gl.drawElements(gl.TRIANGLES, batch.$indexCount, gl.UNSIGNED_SHORT, batch.geometryOffset * 2); } else { // TODO: Get rid of the $vertexCount black magic! gl.drawArrays(gl.TRIANGLES, batch.geometryOffset, batch.$vertexCount); } batch.reset(); } geometryFactory.release(geom); } } /** * A resolvable configures specific settings of how a display-object is rendered by a batch renderer. It * is resolved to a number, for a given display object, using {@link resolveProperty}. * * @ignore */ /** * Resolves a resolvable for the passed {@link DisplayObject}. It is expected to evaluate to a * number. If the passed {@code prop} is a string, it is dereferenced from the object. If the passed * {@code prop} is a function, then it is invoked by passing the object. * * @ignore * @param object - The object for which the parameter property is to be resolved. * @param prop - The property that is to be resolved to a numeric value. * @param def - The value that will be resolved if {@code prop} is undefined. * @return The resolved value of the {@code prop}. */ function resolve( object, prop, def ) { switch(typeof prop) { case 'string': return (object )[prop]; case 'function': return (prop )(object); case 'undefined': return def; } return prop; } function resolveFunctionOrProperty(targetObject, property) { return (typeof property === 'string') ? (targetObject )[property] : property(targetObject); } /** * This object renderer renders multiple display-objects in batches. It can greatly * reduce the number of draw calls issued per frame. * * ## Batch Rendering Pipeline * * The batch rendering pipeline consists of the following stages: * * * **Display-Object Buffering**: Each display-object is kept in a buffer until it fills up or a * flush is required. * * * **Batch Generation**: In a sliding window, display-object batches are generated based off of certain * constraints like GPU texture units and the uniforms used in each display-object. This is done using an * instance of {@link BatchFactory}. * * * **Geometry Composition**: The geometries of all display-objects are merged together in a * composite geometry. This is done using an instance of {@link BatchGeometryFactory}. * * * **Drawing**: Each batch is rendered in-order using `gl.draw*`. The textures and * uniforms of each display-object are uploaded as arrays. This is done using an instance of * {@link BatchDrawer}. * * Each stage in this pipeline can be configured by overriding the appropriate component and passing that * class to `BatchRendererPluginFactory.from*`. * * ## Shaders * * ### Shader templates * * Since the max. display-object count per batch is not known until the WebGL context is created, * shaders are generated at runtime by processing shader templates. A shader templates has "%macros%" * that are replaced by constants at runtime. * * To use shader templates, simply use {@link BatchShaderFactory#derive}. This will generate a * function that derives a shader from your template at runtime. * * ### Textures * * The batch renderer uploads textures in the `uniform sampler2D uSamplers[%texturesPerBatch%];`. The * `varying float vTextureId` defines the index into this array that holds the current display-object's * texture. * * ### Uniforms * * This renderer currently does not support customized uniforms for display-objects. This is a * work-in-progress feature. * * ## Learn more * This batch renderer uses the PixiJS object-renderer API to hook itself: * * 1. [PIXI.ObjectRenderer]{@link http://pixijs.download/release/docs/PIXI.ObjectRenderer.html} * * 2. [PIXI.AbstractBatchRenderer]{@link http://pixijs.download/release/docs/PIXI.AbstractBatchRenderer.html} * * @example * import * as PIXI from 'pixi.js'; * import { BatchRendererPluginFactory } from 'pixi-batch-renderer'; * * // Define the geometry of your display-object and create a BatchRenderer using * // BatchRendererPluginFactory. Register it as a plugin with PIXI.Renderer. * PIXI.Renderer.registerPlugin('ExampleBatchRenderer', BatchRendererPluginFactory.from(...)); * * class ExampleObject extends PIXI.Container * { * _render(renderer: PIXI.Renderer): void * { * // BatchRenderer will handle the whole rendering process for you! * renderer.batch.setObjectRenderer(renderer.plugins['ExampleBatchRenderer']); * renderer.plugins['ExampleBatchRenderer'].render(this); * } * } */ class BatchRenderer extends ObjectRenderer { /** @protected */ // Standard pipeline /** * Attribute redirects * * @access protected */ /** * Indices property * * @access protected */ /** * A manual resolution of the number of indicies in a display object's geometry. This is ignored if the * index buffer is not used (see _indexProperty). If not provided, the index buffer's entire length * is used. * * @access protected */ /** * A manual resolution of the number of vertices in a display object's geometry. If not provided, this is * calculated as the number of element in the first attribute's buffer. * * @access protected */ // Uniforms+Standard Pipeline // Master-ID optimization // API Visiblity Note: These properties are used by component/factories and must be public; // however, they are prefixed with an underscore because they are not for exposure to the end-user. // Components // Display-object buffering // Drawer // WebGL Context config // Component ctors // Additional args /** * Creates a batch renderer the renders display-objects with the described geometry. * * To register a batch-renderer plugin, you must use the API provided by `BatchRendererPluginFactory`. * * @param {PIXI.Renderer} renderer - renderer to attach to * @param {object} options * @param {AttributeRedirect[]} options.attribSet * @param {string | null} options.indexProperty * @param {string | number} [options.vertexCountProperty] * @param {string | null} options.textureProperty * @param {number} [options.texturesPerObject=1] * @param {string} options.texIDAttrib - name of texture-id attribute variable * @param {Function}[options.stateFunction=PIXI.State.for2d()] - returns a `PIXI.State` for an object * @param {Function} options.shaderFunction - generates a shader given this instance * @param {Class} [options.BatchGeometryFactory=BatchGeometry] * @param {Class} [options.BatchFactoryClass=StdBatchFactory] * @param {Class} [options.BatchDrawer=BatchDrawer] * @see BatchShaderFactory * @see StdBatchFactory * @see BatchGeometryFactory * @see BatchDrawer */ constructor(renderer, options) { super(renderer); this._attribRedirects = options.attribSet; this._indexProperty = options.indexProperty; this._indexCountProperty = options.indexCountProperty; this._vertexCountProperty = options.vertexCountProperty; /** * Texture(s) property * @member {string} * @protected * @readonly */ this._textureProperty = options.textureProperty; /** * Textures per display-object * @member {number} * @protected * @readonly * @default 1 */ this._texturesPerObject = typeof options.texturesPerObject !== 'undefined' ? options.texturesPerObject : 1; /** * Texture ID attribute * @member {string} * @protected * @readonly */ this._texIDAttrib = options.texIDAttrib; /** * Indexes the display-object in the batch. * @member {string} * @protected * @readonly */ this._inBatchIDAttrib = options.inBatchIDAttrib; /** * State generating function (takes a display-object) * @member {Function} * @default () => PIXI.State.for2d() * @protected * @readonly */ this._stateFunction = options.stateFunction || (() => State.for2d()); /** * Shader generating function (takes the batch renderer) * @member {Function} * @protected * @see BatchShaderFactory * @readonly */ this._shaderFunction = options.shaderFunction; /** * Batch-factory class. * @member {Class} * @protected * @default StdBatchFactory * @readonly */ this._BatchFactoryClass = options.BatchFactoryClass || StdBatchFactory; /** * Batch-geometry factory class. Its constructor takes one argument - this batch renderer. * @member {Class} * @protected * @default BatchGeometryFactory * @readonly */ this._BatchGeometryFactoryClass = options.BatchGeometryFactoryClass || BatchGeometryFactory; /** * Batch drawer class. Its constructor takes one argument - this batch renderer. * @member {Class} * @protected * @default BatchDrawer * @readonly */ this._BatchDrawerClass = options.BatchDrawerClass || BatchDrawer; /** * Uniform redirects. If you use uniforms in your shader, be sure to use one the compatible * batch factories (like {@link AggregateUniformsBatchFactory}). * @member {UniformRedirect[]} * @protected * @default null * @readonly */ this._uniformRedirects = options.uniformSet || null; /** * Indexes the uniforms of the display-object in the uniform arrays. This is not equal to the * in-batch ID because equal uniforms are not uploaded twice. * @member {string} * @protected * @readonly */ this._uniformIDAttrib = options.uniformIDAttrib; /** * This is an advanced feature that allows you to pack the {@code _texIDAttrib}, {@code _uniformIDAttrib}, * {@code _inBatchIDAttrib}, and other information into one 32-bit float attribute. You can then unpack * them in the vertex shader and pass varyings to the fragment shader (because {@code int} varyings are not * supported). * * To use it, you must provide your own {@link BatchGeometryFactory} that overrides * {@link BatchGeometryFactory#append} and sets the {@code _masterIDAttrib}. */ this._masterIDAttrib = options.masterIDAttrib; /** * The options used to create this batch renderer. * @readonly {object} * @protected * @readonly */ this.options = options; if (options.masterIDAttrib) { this._texIDAttrib = this._masterIDAttrib; this._uniformIDAttrib = this._masterIDAttrib; this._inBatchIDAttrib = this._masterIDAttrib; } // Although the runners property is not a public API, it is required to // handle contextChange events. this.renderer.runners.contextChange.add(this); // If the WebGL context has