pixi-batch-renderer
Version:
Batch rendering library for PixiJS applications
1,754 lines (1,450 loc) • 77.9 kB
JavaScript
/* 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