diff --git a/packages/core/src/batch/AbstractBatchRenderer.js b/packages/core/src/batch/AbstractBatchRenderer.js new file mode 100644 index 0000000..39a50f6 --- /dev/null +++ b/packages/core/src/batch/AbstractBatchRenderer.js @@ -0,0 +1,632 @@ +import BatchDrawCall from './BatchDrawCall'; +import BaseTexture from '../textures/BaseTexture'; +import ObjectRenderer from './ObjectRenderer'; +import State from '../state/State'; +import ViewableBuffer from '../geometry/ViewableBuffer'; + +import checkMaxIfStatementsInShader from '../shader/utils/checkMaxIfStatementsInShader'; + +import { settings } from '@pixi/settings'; +import { premultiplyBlendMode, premultiplyTint, nextPow2, log2 } from '@pixi/utils'; +import { ENV } from '@pixi/constants'; + +/** + * Renderer dedicated to drawing and batching sprites. + * + * This is the default batch renderer. It buffers objects + * with texture-based geometries and renders them in + * batches. It uploads multiple textures to the GPU to + * reduce to the number of draw calls. + * + * @class + * @protected + * @memberof PIXI + * @extends PIXI.ObjectRenderer + */ +export default class AbstractBatchRenderer extends ObjectRenderer +{ + /** + * This will hook onto the renderer's `contextChange` + * and `prerender` signals. + * + * @param {PIXI.Renderer} renderer - The renderer this works for. + */ + constructor(renderer) + { + super(renderer); + + /** + * This is used to generate a shader that can + * color each vertex based on a `aTextureId` + * attribute that points to an texture in `uSampler`. + * + * This enables the objects with different textures + * to be drawn in the same draw call. + * + * You can customize your shader by creating your + * custom shader generator. + * + * @member {PIXI.BatchShaderGenerator} + * @protected + */ + this.shaderGenerator = null; + + /** + * The class that represents the geometry of objects + * that are going to be batched with this. + * + * @member {object} + * @default PIXI.BatchGeometry + * @protected + */ + this.geometryClass = null; + + /** + * Size of data being buffered per vertex in the + * attribute buffers (in floats). By default, the + * batch-renderer plugin uses 6: + * + * | aVertexPosition | 2 | + * |-----------------|---| + * | aTextureCoords | 2 | + * | aColor | 1 | + * | aTextureId | 1 | + * + * @member {number} vertexSize + * @readonly + */ + this.vertexSize = null; + + /** + * The WebGL state in which this renderer will work. + * + * @member {PIXI.State} + * @readonly + */ + this.state = State.for2d(); + + /** + * The number of bufferable objects before a flush + * occurs automatically. + * + * @member {number} + * @default settings.SPRITE_MAX_TEXTURES + */ + this.size = 2000 * 4;// settings.SPRITE_BATCH_SIZE, 2000 is a nice balance between mobile/desktop + + /** + * Total count of all vertices used by the currently + * buffered objects. + * + * @member {number} + * @private + */ + this._vertexCount = 0; + + /** + * Total count of all indices used by the currently + * buffered objects. + * + * @member {number} + * @private + */ + this._indexCount = 0; + + /** + * Buffer of objects that are yet to be rendered. + * + * @member {PIXI.DisplayObject[]} + * @private + */ + this._bufferedElements = []; + + /** + * Number of elements that are buffered and are + * waiting to be flushed. + * + * @member {number} + * @private + */ + this._bufferSize = 0; + + /** + * This shader is generated by `this.shaderGenerator`. + * + * It is generated specifically to handle the required + * number of textures being batched together. + * + * @member {PIXI.Shader} + * @protected + */ + this._shader = null; + + /** + * Pool of `this.geometryClass` geometry objects + * that store buffers. They are used to pass data + * to the shader on each draw call. + * + * These are never re-allocated again, unless a + * context change occurs; however, the pool may + * be expanded if required. + * + * @member {PIXI.Geometry[]} + * @private + * @see PIXI.AbstractBatchRenderer.contextChange + */ + this._packedGeometries = []; + + /** + * Size of `this._packedGeometries`. It can be expanded + * if more than `this._packedGeometryPoolSize` flushes + * occur in a single frame. + * + * @member {number} + * @private + */ + this._packedGeometryPoolSize = 2; + + /** + * A flush may occur multiple times in a single + * frame. On iOS devices or when + * `settings.CAN_UPLOAD_SAME_BUFFER` is false, the + * batch renderer does not upload data to the same + * `WebGLBuffer` for performance reasons. + * + * This is the index into `packedGeometries` that points to + * geometry holding the most recent buffers. + * + * @member {number} + * @private + */ + this._flushId = 0; + + /** + * Pool of `BatchDrawCall` objects that `flush` used + * to create "batches" of the objects being rendered. + * + * These are never re-allocated again. + * + * @member BatchDrawCall[] + * @private + */ + this._drawCalls = []; + + for (let k = 0; k < this.size / 4; k++) + { // initialize the draw-calls pool to max size. + this._drawCalls[k] = new BatchDrawCall(); + } + + /** + * Pool of `ViewableBuffer` objects that are sorted in + * order of increasing size. The flush method uses + * the buffer with the least size above the amount + * it requires. These are used for passing attributes. + * + * The first buffer has a size of 8; each subsequent + * buffer has double capacity of its previous. + * + * @member {PIXI.ViewableBuffer} + * @private + * @see PIXI.AbstractBatchRenderer#getAttributeBuffer + */ + this._aBuffers = {}; + + /** + * Pool of `Uint16Array` objects that are sorted in + * order of increasing size. The flush method uses + * the buffer with the least size above the amount + * it requires. These are used for passing indices. + * + * The first buffer has a size of 12; each subsequent + * buffer has double capacity of its previous. + * + * @member {Uint16Array[]} + * @private + * @see PIXI.AbstractBatchRenderer#getIndexBuffer + */ + this._iBuffers = {}; + + /** + * Maximum number of textures that can be uploaded to + * the GPU under the current context. It is initialized + * properly in `this.contextChange`. + * + * @member {number} + * @see PIXI.AbstractBatchRenderer#contextChange + * @readonly + */ + this.MAX_TEXTURES = 1; + + this.renderer.on('prerender', this.onPrerender, this); + renderer.runners.contextChange.add(this); + } + + /** + * Handles the `contextChange` signal. + * + * It calculates `this.MAX_TEXTURES` and allocating the + * packed-geometry object pool. + */ + contextChange() + { + const gl = this.renderer.gl; + + if (settings.PREFER_ENV === ENV.WEBGL_LEGACY) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min( + gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), + settings.SPRITE_MAX_TEXTURES); + + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatementsInShader( + this.MAX_TEXTURES, gl); + } + + this._shader = this.shaderGenerator.generateShader(this.MAX_TEXTURES); + + // we use the second shader as the first one depending on your browser + // may omit aTextureId as it is not used by the shader so is optimized out. + for (let i = 0; i < this._packedGeometryPoolSize; i++) + { + /* eslint-disable max-len */ + this._packedGeometries[i] = new (this.geometryClass)(); + } + } + + /** + * Handles the `prerender` signal. + * + * It ensures that flushes start from the first geometry + * object again. + */ + onPrerender() + { + this._flushId = 0; + } + + /** + * Buffers the "batchable" object. It need not be rendered + * immediately. + * + * @param {PIXI.Sprite} sprite - the sprite to render when + * using this spritebatch + */ + render(element) + { + if (!element._texture.valid) + { + return; + } + + if (this._vertexCount + (element.vertexData.length / 2) > this.size) + { + this.flush(); + } + + this._vertexCount += element.vertexData.length / 2; + this._indexCount += element.indices.length; + this._bufferedElements[this._bufferSize++] = element; + } + + /** + * Renders the content _now_ and empties the current batch. + */ + flush() + { + if (this._vertexCount === 0) + { + return; + } + + const attributeBuffer = this.getAttributeBuffer(this._vertexCount); + const indexBuffer = this.getIndexBuffer(this._indexCount); + const gl = this.renderer.gl; + + const { + _bufferedElements: elements, + _drawCalls: drawCalls, + MAX_TEXTURES, + _packedGeometries: packedGeometries, + vertexSize, + } = this; + + const touch = this.renderer.textureGC.count; + + let index = 0; + let _indexCount = 0; + + let nextTexture; + let currentTexture; + let textureCount = 0; + + let currentGroup = drawCalls[0]; + let groupCount = 0; + + let blendMode = -1;// blend-mode of previous element/sprite/object! + + currentGroup.textureCount = 0; + currentGroup.start = 0; + currentGroup.blend = blendMode; + + let TICK = ++BaseTexture._globalBatch; + let i; + + for (i = 0; i < this._bufferSize; ++i) + { + const sprite = elements[i]; + + elements[i] = null; + nextTexture = sprite._texture.baseTexture; + + const spriteBlendMode = premultiplyBlendMode[ + nextTexture.premultiplyAlpha ? 1 : 0][sprite.blendMode]; + + if (blendMode !== spriteBlendMode) + { + blendMode = spriteBlendMode; + + // force the batch to break! + currentTexture = null; + textureCount = MAX_TEXTURES; + TICK++; + } + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._batchEnabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + + textureCount = 0; + + currentGroup.size = _indexCount - currentGroup.start; + + currentGroup = drawCalls[groupCount++]; + currentGroup.textureCount = 0; + currentGroup.blend = blendMode; + currentGroup.start = _indexCount; + } + + nextTexture.touched = touch; + nextTexture._batchEnabled = TICK; + nextTexture._id = textureCount; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + this.packInterleavedGeometry(sprite, attributeBuffer, + indexBuffer, index, _indexCount); + + // push a graphics.. + index += (sprite.vertexData.length / 2) * vertexSize; + _indexCount += sprite.indices.length; + } + + BaseTexture._globalBatch = TICK; + currentGroup.size = _indexCount - currentGroup.start; + + if (!settings.CAN_UPLOAD_SAME_BUFFER) + { /* Usually on iOS devices, where the browser doesn't + like uploads to the same buffer in a single frame. */ + if (this._packedGeometryPoolSize <= this._flushId) + { + this._packedGeometryPoolSize++; + packedGeometries[this._flushId] = new (this.geometryClass)(); + } + + packedGeometries[this._flushId]._buffer.update(attributeBuffer.rawBinaryData, 0); + packedGeometries[this._flushId]._indexBuffer.update(indexBuffer, 0); + + this.renderer.geometry.bind(packedGeometries[this._flushId]); + this.renderer.geometry.updateBuffers(); + this._flushId++; + } + else + { + // lets use the faster option, always use buffer number 0 + packedGeometries[this._flushId]._buffer.update(attributeBuffer.rawBinaryData, 0); + packedGeometries[this._flushId]._indexBuffer.update(indexBuffer, 0); + + this.renderer.geometry.updateBuffers(); + } + + const textureSystem = this.renderer.texture; + const stateSystem = this.renderer.state; + + // Upload textures and do the draw calls + for (i = 0; i < groupCount; i++) + { + const group = drawCalls[i]; + const groupTextureCount = group.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + textureSystem.bind(group.textures[j], j); + group.textures[j] = null; + } + + stateSystem.setBlendMode(group.blend); + gl.drawElements(group.type, group.size, gl.UNSIGNED_SHORT, group.start * 2); + } + + // reset elements for the next flush + this._bufferSize = 0; + this._vertexCount = 0; + this._indexCount = 0; + } + + /** + * Starts a new sprite batch. + */ + start() + { + this.renderer.state.set(this.state); + + this.renderer.shader.bind(this._shader); + + if (settings.CAN_UPLOAD_SAME_BUFFER) + { + // bind buffer #0, we don't need others + this.renderer.geometry.bind(this._packedGeometries[this._flushId]); + } + } + + /** + * Stops and flushes the current batch. + */ + stop() + { + this.flush(); + } + + /** + * Destroys this `AbstractBatchRenderer`. It cannot be used again. + */ + destroy() + { + for (let i = 0; i < this._packedGeometryPoolSize; i++) + { + if (this._packedGeometries[i]) + { + this._packedGeometries[i].destroy(); + } + } + + this.renderer.off('prerender', this.onPrerender, this); + + this._aBuffers = null; + this._iBuffers = null; + this._packedGeometries = null; + this._drawCalls = null; + + if (this._shader) + { + this._shader.destroy(); + this._shader = null; + } + + super.destroy(); + } + + /** + * Fetches an attribute buffer from `this._aBuffers` that + * can hold atleast `size` floats. + * + * @param {number} size - minimum capacity required + * @return {ViewableBuffer} - buffer than can hold atleast `size` floats + * @private + */ + getAttributeBuffer(size) + { + // 8 vertices is enough for 2 quads + const roundedP2 = nextPow2(Math.ceil(size / 8)); + const roundedSizeIndex = log2(roundedP2); + const roundedSize = roundedP2 * 8; + + if (this._aBuffers.length <= roundedSizeIndex) + { + this._iBuffers.length = roundedSizeIndex + 1; + } + + let buffer = this._aBuffers[roundedSize]; + + if (!buffer) + { + this._aBuffers[roundedSize] = buffer = new ViewableBuffer(roundedSize * this.vertexSize * 4); + } + + return buffer; + } + + /** + * Fetches an index buffer from `this._iBuffers` that can + * has atleast `size` capacity. + * + * @param {number} size - minimum required capacity + * @return {Uint16Array} - buffer that can fit `size` + * indices. + * @private + */ + 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; + } + + /** + * Takes the four batching parameters of `element`, interleaves + * and pushes them into the batching attribute/index buffers given. + * + * It uses these properties: `vertexData` `uvs`, `textureId` and + * `indicies`. It also uses the "tint" of the base-texture, if + * present. + * + * @param {PIXI.Sprite} element - element being rendered + * @param {PIXI.ViewableBuffer} attributeBuffer - attribute buffer. + * @param {Uint16Array} indexBuffer - index buffer + * @param {number} aIndex - number of floats already in the attribute buffer + * @param {number} iIndex - number of indices already in `indexBuffer` + */ + packInterleavedGeometry(element, attributeBuffer, indexBuffer, aIndex, iIndex) + { + const { + uint32View, + float32View, + } = attributeBuffer; + + const packedVertices = aIndex / this.vertexSize; + const uvs = element.uvs; + const indicies = element.indices; + const vertexData = element.vertexData; + const textureId = element._texture.baseTexture._id; + + const alpha = Math.min(element.worldAlpha, 1.0); + const argb = (alpha < 1.0 + && element._texture.baseTexture.premultiplyAlpha) + ? premultiplyTint(element._tintRGB, alpha) + : element._tintRGB + (alpha * 255 << 24); + + // lets not worry about tint! for now.. + for (let i = 0; i < vertexData.length; i += 2) + { + float32View[aIndex++] = vertexData[i]; + float32View[aIndex++] = vertexData[i + 1]; + float32View[aIndex++] = uvs[i]; + float32View[aIndex++] = uvs[i + 1]; + uint32View[aIndex++] = argb; + float32View[aIndex++] = textureId; + } + + for (let i = 0; i < indicies.length; i++) + { + indexBuffer[iIndex++] = packedVertices + indicies[i]; + } + } +} diff --git a/packages/core/src/batch/AbstractBatchRenderer.js b/packages/core/src/batch/AbstractBatchRenderer.js new file mode 100644 index 0000000..39a50f6 --- /dev/null +++ b/packages/core/src/batch/AbstractBatchRenderer.js @@ -0,0 +1,632 @@ +import BatchDrawCall from './BatchDrawCall'; +import BaseTexture from '../textures/BaseTexture'; +import ObjectRenderer from './ObjectRenderer'; +import State from '../state/State'; +import ViewableBuffer from '../geometry/ViewableBuffer'; + +import checkMaxIfStatementsInShader from '../shader/utils/checkMaxIfStatementsInShader'; + +import { settings } from '@pixi/settings'; +import { premultiplyBlendMode, premultiplyTint, nextPow2, log2 } from '@pixi/utils'; +import { ENV } from '@pixi/constants'; + +/** + * Renderer dedicated to drawing and batching sprites. + * + * This is the default batch renderer. It buffers objects + * with texture-based geometries and renders them in + * batches. It uploads multiple textures to the GPU to + * reduce to the number of draw calls. + * + * @class + * @protected + * @memberof PIXI + * @extends PIXI.ObjectRenderer + */ +export default class AbstractBatchRenderer extends ObjectRenderer +{ + /** + * This will hook onto the renderer's `contextChange` + * and `prerender` signals. + * + * @param {PIXI.Renderer} renderer - The renderer this works for. + */ + constructor(renderer) + { + super(renderer); + + /** + * This is used to generate a shader that can + * color each vertex based on a `aTextureId` + * attribute that points to an texture in `uSampler`. + * + * This enables the objects with different textures + * to be drawn in the same draw call. + * + * You can customize your shader by creating your + * custom shader generator. + * + * @member {PIXI.BatchShaderGenerator} + * @protected + */ + this.shaderGenerator = null; + + /** + * The class that represents the geometry of objects + * that are going to be batched with this. + * + * @member {object} + * @default PIXI.BatchGeometry + * @protected + */ + this.geometryClass = null; + + /** + * Size of data being buffered per vertex in the + * attribute buffers (in floats). By default, the + * batch-renderer plugin uses 6: + * + * | aVertexPosition | 2 | + * |-----------------|---| + * | aTextureCoords | 2 | + * | aColor | 1 | + * | aTextureId | 1 | + * + * @member {number} vertexSize + * @readonly + */ + this.vertexSize = null; + + /** + * The WebGL state in which this renderer will work. + * + * @member {PIXI.State} + * @readonly + */ + this.state = State.for2d(); + + /** + * The number of bufferable objects before a flush + * occurs automatically. + * + * @member {number} + * @default settings.SPRITE_MAX_TEXTURES + */ + this.size = 2000 * 4;// settings.SPRITE_BATCH_SIZE, 2000 is a nice balance between mobile/desktop + + /** + * Total count of all vertices used by the currently + * buffered objects. + * + * @member {number} + * @private + */ + this._vertexCount = 0; + + /** + * Total count of all indices used by the currently + * buffered objects. + * + * @member {number} + * @private + */ + this._indexCount = 0; + + /** + * Buffer of objects that are yet to be rendered. + * + * @member {PIXI.DisplayObject[]} + * @private + */ + this._bufferedElements = []; + + /** + * Number of elements that are buffered and are + * waiting to be flushed. + * + * @member {number} + * @private + */ + this._bufferSize = 0; + + /** + * This shader is generated by `this.shaderGenerator`. + * + * It is generated specifically to handle the required + * number of textures being batched together. + * + * @member {PIXI.Shader} + * @protected + */ + this._shader = null; + + /** + * Pool of `this.geometryClass` geometry objects + * that store buffers. They are used to pass data + * to the shader on each draw call. + * + * These are never re-allocated again, unless a + * context change occurs; however, the pool may + * be expanded if required. + * + * @member {PIXI.Geometry[]} + * @private + * @see PIXI.AbstractBatchRenderer.contextChange + */ + this._packedGeometries = []; + + /** + * Size of `this._packedGeometries`. It can be expanded + * if more than `this._packedGeometryPoolSize` flushes + * occur in a single frame. + * + * @member {number} + * @private + */ + this._packedGeometryPoolSize = 2; + + /** + * A flush may occur multiple times in a single + * frame. On iOS devices or when + * `settings.CAN_UPLOAD_SAME_BUFFER` is false, the + * batch renderer does not upload data to the same + * `WebGLBuffer` for performance reasons. + * + * This is the index into `packedGeometries` that points to + * geometry holding the most recent buffers. + * + * @member {number} + * @private + */ + this._flushId = 0; + + /** + * Pool of `BatchDrawCall` objects that `flush` used + * to create "batches" of the objects being rendered. + * + * These are never re-allocated again. + * + * @member BatchDrawCall[] + * @private + */ + this._drawCalls = []; + + for (let k = 0; k < this.size / 4; k++) + { // initialize the draw-calls pool to max size. + this._drawCalls[k] = new BatchDrawCall(); + } + + /** + * Pool of `ViewableBuffer` objects that are sorted in + * order of increasing size. The flush method uses + * the buffer with the least size above the amount + * it requires. These are used for passing attributes. + * + * The first buffer has a size of 8; each subsequent + * buffer has double capacity of its previous. + * + * @member {PIXI.ViewableBuffer} + * @private + * @see PIXI.AbstractBatchRenderer#getAttributeBuffer + */ + this._aBuffers = {}; + + /** + * Pool of `Uint16Array` objects that are sorted in + * order of increasing size. The flush method uses + * the buffer with the least size above the amount + * it requires. These are used for passing indices. + * + * The first buffer has a size of 12; each subsequent + * buffer has double capacity of its previous. + * + * @member {Uint16Array[]} + * @private + * @see PIXI.AbstractBatchRenderer#getIndexBuffer + */ + this._iBuffers = {}; + + /** + * Maximum number of textures that can be uploaded to + * the GPU under the current context. It is initialized + * properly in `this.contextChange`. + * + * @member {number} + * @see PIXI.AbstractBatchRenderer#contextChange + * @readonly + */ + this.MAX_TEXTURES = 1; + + this.renderer.on('prerender', this.onPrerender, this); + renderer.runners.contextChange.add(this); + } + + /** + * Handles the `contextChange` signal. + * + * It calculates `this.MAX_TEXTURES` and allocating the + * packed-geometry object pool. + */ + contextChange() + { + const gl = this.renderer.gl; + + if (settings.PREFER_ENV === ENV.WEBGL_LEGACY) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min( + gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), + settings.SPRITE_MAX_TEXTURES); + + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatementsInShader( + this.MAX_TEXTURES, gl); + } + + this._shader = this.shaderGenerator.generateShader(this.MAX_TEXTURES); + + // we use the second shader as the first one depending on your browser + // may omit aTextureId as it is not used by the shader so is optimized out. + for (let i = 0; i < this._packedGeometryPoolSize; i++) + { + /* eslint-disable max-len */ + this._packedGeometries[i] = new (this.geometryClass)(); + } + } + + /** + * Handles the `prerender` signal. + * + * It ensures that flushes start from the first geometry + * object again. + */ + onPrerender() + { + this._flushId = 0; + } + + /** + * Buffers the "batchable" object. It need not be rendered + * immediately. + * + * @param {PIXI.Sprite} sprite - the sprite to render when + * using this spritebatch + */ + render(element) + { + if (!element._texture.valid) + { + return; + } + + if (this._vertexCount + (element.vertexData.length / 2) > this.size) + { + this.flush(); + } + + this._vertexCount += element.vertexData.length / 2; + this._indexCount += element.indices.length; + this._bufferedElements[this._bufferSize++] = element; + } + + /** + * Renders the content _now_ and empties the current batch. + */ + flush() + { + if (this._vertexCount === 0) + { + return; + } + + const attributeBuffer = this.getAttributeBuffer(this._vertexCount); + const indexBuffer = this.getIndexBuffer(this._indexCount); + const gl = this.renderer.gl; + + const { + _bufferedElements: elements, + _drawCalls: drawCalls, + MAX_TEXTURES, + _packedGeometries: packedGeometries, + vertexSize, + } = this; + + const touch = this.renderer.textureGC.count; + + let index = 0; + let _indexCount = 0; + + let nextTexture; + let currentTexture; + let textureCount = 0; + + let currentGroup = drawCalls[0]; + let groupCount = 0; + + let blendMode = -1;// blend-mode of previous element/sprite/object! + + currentGroup.textureCount = 0; + currentGroup.start = 0; + currentGroup.blend = blendMode; + + let TICK = ++BaseTexture._globalBatch; + let i; + + for (i = 0; i < this._bufferSize; ++i) + { + const sprite = elements[i]; + + elements[i] = null; + nextTexture = sprite._texture.baseTexture; + + const spriteBlendMode = premultiplyBlendMode[ + nextTexture.premultiplyAlpha ? 1 : 0][sprite.blendMode]; + + if (blendMode !== spriteBlendMode) + { + blendMode = spriteBlendMode; + + // force the batch to break! + currentTexture = null; + textureCount = MAX_TEXTURES; + TICK++; + } + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._batchEnabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + + textureCount = 0; + + currentGroup.size = _indexCount - currentGroup.start; + + currentGroup = drawCalls[groupCount++]; + currentGroup.textureCount = 0; + currentGroup.blend = blendMode; + currentGroup.start = _indexCount; + } + + nextTexture.touched = touch; + nextTexture._batchEnabled = TICK; + nextTexture._id = textureCount; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + this.packInterleavedGeometry(sprite, attributeBuffer, + indexBuffer, index, _indexCount); + + // push a graphics.. + index += (sprite.vertexData.length / 2) * vertexSize; + _indexCount += sprite.indices.length; + } + + BaseTexture._globalBatch = TICK; + currentGroup.size = _indexCount - currentGroup.start; + + if (!settings.CAN_UPLOAD_SAME_BUFFER) + { /* Usually on iOS devices, where the browser doesn't + like uploads to the same buffer in a single frame. */ + if (this._packedGeometryPoolSize <= this._flushId) + { + this._packedGeometryPoolSize++; + packedGeometries[this._flushId] = new (this.geometryClass)(); + } + + packedGeometries[this._flushId]._buffer.update(attributeBuffer.rawBinaryData, 0); + packedGeometries[this._flushId]._indexBuffer.update(indexBuffer, 0); + + this.renderer.geometry.bind(packedGeometries[this._flushId]); + this.renderer.geometry.updateBuffers(); + this._flushId++; + } + else + { + // lets use the faster option, always use buffer number 0 + packedGeometries[this._flushId]._buffer.update(attributeBuffer.rawBinaryData, 0); + packedGeometries[this._flushId]._indexBuffer.update(indexBuffer, 0); + + this.renderer.geometry.updateBuffers(); + } + + const textureSystem = this.renderer.texture; + const stateSystem = this.renderer.state; + + // Upload textures and do the draw calls + for (i = 0; i < groupCount; i++) + { + const group = drawCalls[i]; + const groupTextureCount = group.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + textureSystem.bind(group.textures[j], j); + group.textures[j] = null; + } + + stateSystem.setBlendMode(group.blend); + gl.drawElements(group.type, group.size, gl.UNSIGNED_SHORT, group.start * 2); + } + + // reset elements for the next flush + this._bufferSize = 0; + this._vertexCount = 0; + this._indexCount = 0; + } + + /** + * Starts a new sprite batch. + */ + start() + { + this.renderer.state.set(this.state); + + this.renderer.shader.bind(this._shader); + + if (settings.CAN_UPLOAD_SAME_BUFFER) + { + // bind buffer #0, we don't need others + this.renderer.geometry.bind(this._packedGeometries[this._flushId]); + } + } + + /** + * Stops and flushes the current batch. + */ + stop() + { + this.flush(); + } + + /** + * Destroys this `AbstractBatchRenderer`. It cannot be used again. + */ + destroy() + { + for (let i = 0; i < this._packedGeometryPoolSize; i++) + { + if (this._packedGeometries[i]) + { + this._packedGeometries[i].destroy(); + } + } + + this.renderer.off('prerender', this.onPrerender, this); + + this._aBuffers = null; + this._iBuffers = null; + this._packedGeometries = null; + this._drawCalls = null; + + if (this._shader) + { + this._shader.destroy(); + this._shader = null; + } + + super.destroy(); + } + + /** + * Fetches an attribute buffer from `this._aBuffers` that + * can hold atleast `size` floats. + * + * @param {number} size - minimum capacity required + * @return {ViewableBuffer} - buffer than can hold atleast `size` floats + * @private + */ + getAttributeBuffer(size) + { + // 8 vertices is enough for 2 quads + const roundedP2 = nextPow2(Math.ceil(size / 8)); + const roundedSizeIndex = log2(roundedP2); + const roundedSize = roundedP2 * 8; + + if (this._aBuffers.length <= roundedSizeIndex) + { + this._iBuffers.length = roundedSizeIndex + 1; + } + + let buffer = this._aBuffers[roundedSize]; + + if (!buffer) + { + this._aBuffers[roundedSize] = buffer = new ViewableBuffer(roundedSize * this.vertexSize * 4); + } + + return buffer; + } + + /** + * Fetches an index buffer from `this._iBuffers` that can + * has atleast `size` capacity. + * + * @param {number} size - minimum required capacity + * @return {Uint16Array} - buffer that can fit `size` + * indices. + * @private + */ + 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; + } + + /** + * Takes the four batching parameters of `element`, interleaves + * and pushes them into the batching attribute/index buffers given. + * + * It uses these properties: `vertexData` `uvs`, `textureId` and + * `indicies`. It also uses the "tint" of the base-texture, if + * present. + * + * @param {PIXI.Sprite} element - element being rendered + * @param {PIXI.ViewableBuffer} attributeBuffer - attribute buffer. + * @param {Uint16Array} indexBuffer - index buffer + * @param {number} aIndex - number of floats already in the attribute buffer + * @param {number} iIndex - number of indices already in `indexBuffer` + */ + packInterleavedGeometry(element, attributeBuffer, indexBuffer, aIndex, iIndex) + { + const { + uint32View, + float32View, + } = attributeBuffer; + + const packedVertices = aIndex / this.vertexSize; + const uvs = element.uvs; + const indicies = element.indices; + const vertexData = element.vertexData; + const textureId = element._texture.baseTexture._id; + + const alpha = Math.min(element.worldAlpha, 1.0); + const argb = (alpha < 1.0 + && element._texture.baseTexture.premultiplyAlpha) + ? premultiplyTint(element._tintRGB, alpha) + : element._tintRGB + (alpha * 255 << 24); + + // lets not worry about tint! for now.. + for (let i = 0; i < vertexData.length; i += 2) + { + float32View[aIndex++] = vertexData[i]; + float32View[aIndex++] = vertexData[i + 1]; + float32View[aIndex++] = uvs[i]; + float32View[aIndex++] = uvs[i + 1]; + uint32View[aIndex++] = argb; + float32View[aIndex++] = textureId; + } + + for (let i = 0; i < indicies.length; i++) + { + indexBuffer[iIndex++] = packedVertices + indicies[i]; + } + } +} diff --git a/packages/core/src/batch/BatchPluginFactory.js b/packages/core/src/batch/BatchPluginFactory.js index f2785ae..63a9696 100644 --- a/packages/core/src/batch/BatchPluginFactory.js +++ b/packages/core/src/batch/BatchPluginFactory.js @@ -1,6 +1,6 @@ import BatchShaderGenerator from './BatchShaderGenerator'; import BatchGeometry from './BatchGeometry'; -import BaseBatchRenderer from './BatchRenderer'; +import AbstractBatchRenderer from './AbstractBatchRenderer'; import defaultVertex from './texture.vert'; import defaultFragment from './texture.frag'; @@ -50,7 +50,7 @@ vertexSize: 6, }, options); - return class BatchPlugin extends BaseBatchRenderer + return class BatchPlugin extends AbstractBatchRenderer { constructor(renderer) { diff --git a/packages/core/src/batch/AbstractBatchRenderer.js b/packages/core/src/batch/AbstractBatchRenderer.js new file mode 100644 index 0000000..39a50f6 --- /dev/null +++ b/packages/core/src/batch/AbstractBatchRenderer.js @@ -0,0 +1,632 @@ +import BatchDrawCall from './BatchDrawCall'; +import BaseTexture from '../textures/BaseTexture'; +import ObjectRenderer from './ObjectRenderer'; +import State from '../state/State'; +import ViewableBuffer from '../geometry/ViewableBuffer'; + +import checkMaxIfStatementsInShader from '../shader/utils/checkMaxIfStatementsInShader'; + +import { settings } from '@pixi/settings'; +import { premultiplyBlendMode, premultiplyTint, nextPow2, log2 } from '@pixi/utils'; +import { ENV } from '@pixi/constants'; + +/** + * Renderer dedicated to drawing and batching sprites. + * + * This is the default batch renderer. It buffers objects + * with texture-based geometries and renders them in + * batches. It uploads multiple textures to the GPU to + * reduce to the number of draw calls. + * + * @class + * @protected + * @memberof PIXI + * @extends PIXI.ObjectRenderer + */ +export default class AbstractBatchRenderer extends ObjectRenderer +{ + /** + * This will hook onto the renderer's `contextChange` + * and `prerender` signals. + * + * @param {PIXI.Renderer} renderer - The renderer this works for. + */ + constructor(renderer) + { + super(renderer); + + /** + * This is used to generate a shader that can + * color each vertex based on a `aTextureId` + * attribute that points to an texture in `uSampler`. + * + * This enables the objects with different textures + * to be drawn in the same draw call. + * + * You can customize your shader by creating your + * custom shader generator. + * + * @member {PIXI.BatchShaderGenerator} + * @protected + */ + this.shaderGenerator = null; + + /** + * The class that represents the geometry of objects + * that are going to be batched with this. + * + * @member {object} + * @default PIXI.BatchGeometry + * @protected + */ + this.geometryClass = null; + + /** + * Size of data being buffered per vertex in the + * attribute buffers (in floats). By default, the + * batch-renderer plugin uses 6: + * + * | aVertexPosition | 2 | + * |-----------------|---| + * | aTextureCoords | 2 | + * | aColor | 1 | + * | aTextureId | 1 | + * + * @member {number} vertexSize + * @readonly + */ + this.vertexSize = null; + + /** + * The WebGL state in which this renderer will work. + * + * @member {PIXI.State} + * @readonly + */ + this.state = State.for2d(); + + /** + * The number of bufferable objects before a flush + * occurs automatically. + * + * @member {number} + * @default settings.SPRITE_MAX_TEXTURES + */ + this.size = 2000 * 4;// settings.SPRITE_BATCH_SIZE, 2000 is a nice balance between mobile/desktop + + /** + * Total count of all vertices used by the currently + * buffered objects. + * + * @member {number} + * @private + */ + this._vertexCount = 0; + + /** + * Total count of all indices used by the currently + * buffered objects. + * + * @member {number} + * @private + */ + this._indexCount = 0; + + /** + * Buffer of objects that are yet to be rendered. + * + * @member {PIXI.DisplayObject[]} + * @private + */ + this._bufferedElements = []; + + /** + * Number of elements that are buffered and are + * waiting to be flushed. + * + * @member {number} + * @private + */ + this._bufferSize = 0; + + /** + * This shader is generated by `this.shaderGenerator`. + * + * It is generated specifically to handle the required + * number of textures being batched together. + * + * @member {PIXI.Shader} + * @protected + */ + this._shader = null; + + /** + * Pool of `this.geometryClass` geometry objects + * that store buffers. They are used to pass data + * to the shader on each draw call. + * + * These are never re-allocated again, unless a + * context change occurs; however, the pool may + * be expanded if required. + * + * @member {PIXI.Geometry[]} + * @private + * @see PIXI.AbstractBatchRenderer.contextChange + */ + this._packedGeometries = []; + + /** + * Size of `this._packedGeometries`. It can be expanded + * if more than `this._packedGeometryPoolSize` flushes + * occur in a single frame. + * + * @member {number} + * @private + */ + this._packedGeometryPoolSize = 2; + + /** + * A flush may occur multiple times in a single + * frame. On iOS devices or when + * `settings.CAN_UPLOAD_SAME_BUFFER` is false, the + * batch renderer does not upload data to the same + * `WebGLBuffer` for performance reasons. + * + * This is the index into `packedGeometries` that points to + * geometry holding the most recent buffers. + * + * @member {number} + * @private + */ + this._flushId = 0; + + /** + * Pool of `BatchDrawCall` objects that `flush` used + * to create "batches" of the objects being rendered. + * + * These are never re-allocated again. + * + * @member BatchDrawCall[] + * @private + */ + this._drawCalls = []; + + for (let k = 0; k < this.size / 4; k++) + { // initialize the draw-calls pool to max size. + this._drawCalls[k] = new BatchDrawCall(); + } + + /** + * Pool of `ViewableBuffer` objects that are sorted in + * order of increasing size. The flush method uses + * the buffer with the least size above the amount + * it requires. These are used for passing attributes. + * + * The first buffer has a size of 8; each subsequent + * buffer has double capacity of its previous. + * + * @member {PIXI.ViewableBuffer} + * @private + * @see PIXI.AbstractBatchRenderer#getAttributeBuffer + */ + this._aBuffers = {}; + + /** + * Pool of `Uint16Array` objects that are sorted in + * order of increasing size. The flush method uses + * the buffer with the least size above the amount + * it requires. These are used for passing indices. + * + * The first buffer has a size of 12; each subsequent + * buffer has double capacity of its previous. + * + * @member {Uint16Array[]} + * @private + * @see PIXI.AbstractBatchRenderer#getIndexBuffer + */ + this._iBuffers = {}; + + /** + * Maximum number of textures that can be uploaded to + * the GPU under the current context. It is initialized + * properly in `this.contextChange`. + * + * @member {number} + * @see PIXI.AbstractBatchRenderer#contextChange + * @readonly + */ + this.MAX_TEXTURES = 1; + + this.renderer.on('prerender', this.onPrerender, this); + renderer.runners.contextChange.add(this); + } + + /** + * Handles the `contextChange` signal. + * + * It calculates `this.MAX_TEXTURES` and allocating the + * packed-geometry object pool. + */ + contextChange() + { + const gl = this.renderer.gl; + + if (settings.PREFER_ENV === ENV.WEBGL_LEGACY) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min( + gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), + settings.SPRITE_MAX_TEXTURES); + + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatementsInShader( + this.MAX_TEXTURES, gl); + } + + this._shader = this.shaderGenerator.generateShader(this.MAX_TEXTURES); + + // we use the second shader as the first one depending on your browser + // may omit aTextureId as it is not used by the shader so is optimized out. + for (let i = 0; i < this._packedGeometryPoolSize; i++) + { + /* eslint-disable max-len */ + this._packedGeometries[i] = new (this.geometryClass)(); + } + } + + /** + * Handles the `prerender` signal. + * + * It ensures that flushes start from the first geometry + * object again. + */ + onPrerender() + { + this._flushId = 0; + } + + /** + * Buffers the "batchable" object. It need not be rendered + * immediately. + * + * @param {PIXI.Sprite} sprite - the sprite to render when + * using this spritebatch + */ + render(element) + { + if (!element._texture.valid) + { + return; + } + + if (this._vertexCount + (element.vertexData.length / 2) > this.size) + { + this.flush(); + } + + this._vertexCount += element.vertexData.length / 2; + this._indexCount += element.indices.length; + this._bufferedElements[this._bufferSize++] = element; + } + + /** + * Renders the content _now_ and empties the current batch. + */ + flush() + { + if (this._vertexCount === 0) + { + return; + } + + const attributeBuffer = this.getAttributeBuffer(this._vertexCount); + const indexBuffer = this.getIndexBuffer(this._indexCount); + const gl = this.renderer.gl; + + const { + _bufferedElements: elements, + _drawCalls: drawCalls, + MAX_TEXTURES, + _packedGeometries: packedGeometries, + vertexSize, + } = this; + + const touch = this.renderer.textureGC.count; + + let index = 0; + let _indexCount = 0; + + let nextTexture; + let currentTexture; + let textureCount = 0; + + let currentGroup = drawCalls[0]; + let groupCount = 0; + + let blendMode = -1;// blend-mode of previous element/sprite/object! + + currentGroup.textureCount = 0; + currentGroup.start = 0; + currentGroup.blend = blendMode; + + let TICK = ++BaseTexture._globalBatch; + let i; + + for (i = 0; i < this._bufferSize; ++i) + { + const sprite = elements[i]; + + elements[i] = null; + nextTexture = sprite._texture.baseTexture; + + const spriteBlendMode = premultiplyBlendMode[ + nextTexture.premultiplyAlpha ? 1 : 0][sprite.blendMode]; + + if (blendMode !== spriteBlendMode) + { + blendMode = spriteBlendMode; + + // force the batch to break! + currentTexture = null; + textureCount = MAX_TEXTURES; + TICK++; + } + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._batchEnabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + + textureCount = 0; + + currentGroup.size = _indexCount - currentGroup.start; + + currentGroup = drawCalls[groupCount++]; + currentGroup.textureCount = 0; + currentGroup.blend = blendMode; + currentGroup.start = _indexCount; + } + + nextTexture.touched = touch; + nextTexture._batchEnabled = TICK; + nextTexture._id = textureCount; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + this.packInterleavedGeometry(sprite, attributeBuffer, + indexBuffer, index, _indexCount); + + // push a graphics.. + index += (sprite.vertexData.length / 2) * vertexSize; + _indexCount += sprite.indices.length; + } + + BaseTexture._globalBatch = TICK; + currentGroup.size = _indexCount - currentGroup.start; + + if (!settings.CAN_UPLOAD_SAME_BUFFER) + { /* Usually on iOS devices, where the browser doesn't + like uploads to the same buffer in a single frame. */ + if (this._packedGeometryPoolSize <= this._flushId) + { + this._packedGeometryPoolSize++; + packedGeometries[this._flushId] = new (this.geometryClass)(); + } + + packedGeometries[this._flushId]._buffer.update(attributeBuffer.rawBinaryData, 0); + packedGeometries[this._flushId]._indexBuffer.update(indexBuffer, 0); + + this.renderer.geometry.bind(packedGeometries[this._flushId]); + this.renderer.geometry.updateBuffers(); + this._flushId++; + } + else + { + // lets use the faster option, always use buffer number 0 + packedGeometries[this._flushId]._buffer.update(attributeBuffer.rawBinaryData, 0); + packedGeometries[this._flushId]._indexBuffer.update(indexBuffer, 0); + + this.renderer.geometry.updateBuffers(); + } + + const textureSystem = this.renderer.texture; + const stateSystem = this.renderer.state; + + // Upload textures and do the draw calls + for (i = 0; i < groupCount; i++) + { + const group = drawCalls[i]; + const groupTextureCount = group.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + textureSystem.bind(group.textures[j], j); + group.textures[j] = null; + } + + stateSystem.setBlendMode(group.blend); + gl.drawElements(group.type, group.size, gl.UNSIGNED_SHORT, group.start * 2); + } + + // reset elements for the next flush + this._bufferSize = 0; + this._vertexCount = 0; + this._indexCount = 0; + } + + /** + * Starts a new sprite batch. + */ + start() + { + this.renderer.state.set(this.state); + + this.renderer.shader.bind(this._shader); + + if (settings.CAN_UPLOAD_SAME_BUFFER) + { + // bind buffer #0, we don't need others + this.renderer.geometry.bind(this._packedGeometries[this._flushId]); + } + } + + /** + * Stops and flushes the current batch. + */ + stop() + { + this.flush(); + } + + /** + * Destroys this `AbstractBatchRenderer`. It cannot be used again. + */ + destroy() + { + for (let i = 0; i < this._packedGeometryPoolSize; i++) + { + if (this._packedGeometries[i]) + { + this._packedGeometries[i].destroy(); + } + } + + this.renderer.off('prerender', this.onPrerender, this); + + this._aBuffers = null; + this._iBuffers = null; + this._packedGeometries = null; + this._drawCalls = null; + + if (this._shader) + { + this._shader.destroy(); + this._shader = null; + } + + super.destroy(); + } + + /** + * Fetches an attribute buffer from `this._aBuffers` that + * can hold atleast `size` floats. + * + * @param {number} size - minimum capacity required + * @return {ViewableBuffer} - buffer than can hold atleast `size` floats + * @private + */ + getAttributeBuffer(size) + { + // 8 vertices is enough for 2 quads + const roundedP2 = nextPow2(Math.ceil(size / 8)); + const roundedSizeIndex = log2(roundedP2); + const roundedSize = roundedP2 * 8; + + if (this._aBuffers.length <= roundedSizeIndex) + { + this._iBuffers.length = roundedSizeIndex + 1; + } + + let buffer = this._aBuffers[roundedSize]; + + if (!buffer) + { + this._aBuffers[roundedSize] = buffer = new ViewableBuffer(roundedSize * this.vertexSize * 4); + } + + return buffer; + } + + /** + * Fetches an index buffer from `this._iBuffers` that can + * has atleast `size` capacity. + * + * @param {number} size - minimum required capacity + * @return {Uint16Array} - buffer that can fit `size` + * indices. + * @private + */ + 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; + } + + /** + * Takes the four batching parameters of `element`, interleaves + * and pushes them into the batching attribute/index buffers given. + * + * It uses these properties: `vertexData` `uvs`, `textureId` and + * `indicies`. It also uses the "tint" of the base-texture, if + * present. + * + * @param {PIXI.Sprite} element - element being rendered + * @param {PIXI.ViewableBuffer} attributeBuffer - attribute buffer. + * @param {Uint16Array} indexBuffer - index buffer + * @param {number} aIndex - number of floats already in the attribute buffer + * @param {number} iIndex - number of indices already in `indexBuffer` + */ + packInterleavedGeometry(element, attributeBuffer, indexBuffer, aIndex, iIndex) + { + const { + uint32View, + float32View, + } = attributeBuffer; + + const packedVertices = aIndex / this.vertexSize; + const uvs = element.uvs; + const indicies = element.indices; + const vertexData = element.vertexData; + const textureId = element._texture.baseTexture._id; + + const alpha = Math.min(element.worldAlpha, 1.0); + const argb = (alpha < 1.0 + && element._texture.baseTexture.premultiplyAlpha) + ? premultiplyTint(element._tintRGB, alpha) + : element._tintRGB + (alpha * 255 << 24); + + // lets not worry about tint! for now.. + for (let i = 0; i < vertexData.length; i += 2) + { + float32View[aIndex++] = vertexData[i]; + float32View[aIndex++] = vertexData[i + 1]; + float32View[aIndex++] = uvs[i]; + float32View[aIndex++] = uvs[i + 1]; + uint32View[aIndex++] = argb; + float32View[aIndex++] = textureId; + } + + for (let i = 0; i < indicies.length; i++) + { + indexBuffer[iIndex++] = packedVertices + indicies[i]; + } + } +} diff --git a/packages/core/src/batch/BatchPluginFactory.js b/packages/core/src/batch/BatchPluginFactory.js index f2785ae..63a9696 100644 --- a/packages/core/src/batch/BatchPluginFactory.js +++ b/packages/core/src/batch/BatchPluginFactory.js @@ -1,6 +1,6 @@ import BatchShaderGenerator from './BatchShaderGenerator'; import BatchGeometry from './BatchGeometry'; -import BaseBatchRenderer from './BatchRenderer'; +import AbstractBatchRenderer from './AbstractBatchRenderer'; import defaultVertex from './texture.vert'; import defaultFragment from './texture.frag'; @@ -50,7 +50,7 @@ vertexSize: 6, }, options); - return class BatchPlugin extends BaseBatchRenderer + return class BatchPlugin extends AbstractBatchRenderer { constructor(renderer) { diff --git a/packages/core/src/batch/BatchRenderer.js b/packages/core/src/batch/BatchRenderer.js deleted file mode 100644 index d75a35d..0000000 --- a/packages/core/src/batch/BatchRenderer.js +++ /dev/null @@ -1,632 +0,0 @@ -import BatchDrawCall from './BatchDrawCall'; -import BaseTexture from '../textures/BaseTexture'; -import ObjectRenderer from './ObjectRenderer'; -import State from '../state/State'; -import ViewableBuffer from '../geometry/ViewableBuffer'; - -import checkMaxIfStatementsInShader from '../shader/utils/checkMaxIfStatementsInShader'; - -import { settings } from '@pixi/settings'; -import { premultiplyBlendMode, premultiplyTint, nextPow2, log2 } from '@pixi/utils'; -import { ENV } from '@pixi/constants'; - -/** - * Renderer dedicated to drawing and batching sprites. - * - * This is the default batch renderer. It buffers objects - * with texture-based geometries and renders them in - * batches. It uploads multiple textures to the GPU to - * reduce to the number of draw calls. - * - * @class - * @protected - * @memberof PIXI - * @extends PIXI.ObjectRenderer - */ -export default class BatchRenderer extends ObjectRenderer -{ - /** - * This will hook onto the renderer's `contextChange` - * and `prerender` signals. - * - * @param {PIXI.Renderer} renderer - The renderer this works for. - */ - constructor(renderer) - { - super(renderer); - - /** - * This is used to generate a shader that can - * color each vertex based on a `aTextureId` - * attribute that points to an texture in `uSampler`. - * - * This enables the objects with different textures - * to be drawn in the same draw call. - * - * You can customize your shader by creating your - * custom shader generator. - * - * @member {PIXI.BatchShaderGenerator} - * @protected - */ - this.shaderGenerator = null; - - /** - * The class that represents the geometry of objects - * that are going to be batched with this. - * - * @member {object} - * @default PIXI.BatchGeometry - * @protected - */ - this.geometryClass = null; - - /** - * Size of data being buffered per vertex in the - * attribute buffers (in floats). By default, the - * batch-renderer plugin uses 6: - * - * | aVertexPosition | 2 | - * |-----------------|---| - * | aTextureCoords | 2 | - * | aColor | 1 | - * | aTextureId | 1 | - * - * @member {number} vertexSize - * @readonly - */ - this.vertexSize = null; - - /** - * The WebGL state in which this renderer will work. - * - * @member {PIXI.State} - * @readonly - */ - this.state = State.for2d(); - - /** - * The number of bufferable objects before a flush - * occurs automatically. - * - * @member {number} - * @default settings.SPRITE_MAX_TEXTURES - */ - this.size = 2000 * 4;// settings.SPRITE_BATCH_SIZE, 2000 is a nice balance between mobile/desktop - - /** - * Total count of all vertices used by the currently - * buffered objects. - * - * @member {number} - * @private - */ - this._vertexCount = 0; - - /** - * Total count of all indices used by the currently - * buffered objects. - * - * @member {number} - * @private - */ - this._indexCount = 0; - - /** - * Buffer of objects that are yet to be rendered. - * - * @member {PIXI.DisplayObject[]} - * @private - */ - this._bufferedElements = []; - - /** - * Number of elements that are buffered and are - * waiting to be flushed. - * - * @member {number} - * @private - */ - this._bufferSize = 0; - - /** - * This shader is generated by `this.shaderGenerator`. - * - * It is generated specifically to handle the required - * number of textures being batched together. - * - * @member {PIXI.Shader} - * @protected - */ - this._shader = null; - - /** - * Pool of `this.geometryClass` geometry objects - * that store buffers. They are used to pass data - * to the shader on each draw call. - * - * These are never re-allocated again, unless a - * context change occurs; however, the pool may - * be expanded if required. - * - * @member {PIXI.Geometry[]} - * @private - * @see PIXI.BatchRenderer.contextChange - */ - this._packedGeometries = []; - - /** - * Size of `this._packedGeometries`. It can be expanded - * if more than `this._packedGeometryPoolSize` flushes - * occur in a single frame. - * - * @member {number} - * @private - */ - this._packedGeometryPoolSize = 2; - - /** - * A flush may occur multiple times in a single - * frame. On iOS devices or when - * `settings.CAN_UPLOAD_SAME_BUFFER` is false, the - * batch renderer does not upload data to the same - * `WebGLBuffer` for performance reasons. - * - * This is the index into `packedGeometries` that points to - * geometry holding the most recent buffers. - * - * @member {number} - * @private - */ - this._flushId = 0; - - /** - * Pool of `BatchDrawCall` objects that `flush` used - * to create "batches" of the objects being rendered. - * - * These are never re-allocated again. - * - * @member BatchDrawCall[] - * @private - */ - this._drawCalls = []; - - for (let k = 0; k < this.size / 4; k++) - { // initialize the draw-calls pool to max size. - this._drawCalls[k] = new BatchDrawCall(); - } - - /** - * Pool of `ViewableBuffer` objects that are sorted in - * order of increasing size. The flush method uses - * the buffer with the least size above the amount - * it requires. These are used for passing attributes. - * - * The first buffer has a size of 8; each subsequent - * buffer has double capacity of its previous. - * - * @member {PIXI.ViewableBuffer} - * @private - * @see PIXI.BatchRenderer#getAttributeBuffer - */ - this._aBuffers = {}; - - /** - * Pool of `Uint16Array` objects that are sorted in - * order of increasing size. The flush method uses - * the buffer with the least size above the amount - * it requires. These are used for passing indices. - * - * The first buffer has a size of 12; each subsequent - * buffer has double capacity of its previous. - * - * @member {Uint16Array[]} - * @private - * @see PIXI.BatchRenderer#getIndexBuffer - */ - this._iBuffers = {}; - - /** - * Maximum number of textures that can be uploaded to - * the GPU under the current context. It is initialized - * properly in `this.contextChange`. - * - * @member {number} - * @see PIXI.BatchRenderer#contextChange - * @readonly - */ - this.MAX_TEXTURES = 1; - - this.renderer.on('prerender', this.onPrerender, this); - renderer.runners.contextChange.add(this); - } - - /** - * Handles the `contextChange` signal. - * - * It calculates `this.MAX_TEXTURES` and allocating the - * packed-geometry object pool. - */ - contextChange() - { - const gl = this.renderer.gl; - - if (settings.PREFER_ENV === ENV.WEBGL_LEGACY) - { - this.MAX_TEXTURES = 1; - } - else - { - // step 1: first check max textures the GPU can handle. - this.MAX_TEXTURES = Math.min( - gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), - settings.SPRITE_MAX_TEXTURES); - - // step 2: check the maximum number of if statements the shader can have too.. - this.MAX_TEXTURES = checkMaxIfStatementsInShader( - this.MAX_TEXTURES, gl); - } - - this._shader = this.shaderGenerator.generateShader(this.MAX_TEXTURES); - - // we use the second shader as the first one depending on your browser - // may omit aTextureId as it is not used by the shader so is optimized out. - for (let i = 0; i < this._packedGeometryPoolSize; i++) - { - /* eslint-disable max-len */ - this._packedGeometries[i] = new (this.geometryClass)(); - } - } - - /** - * Handles the `prerender` signal. - * - * It ensures that flushes start from the first geometry - * object again. - */ - onPrerender() - { - this._flushId = 0; - } - - /** - * Buffers the "batchable" object. It need not be rendered - * immediately. - * - * @param {PIXI.Sprite} sprite - the sprite to render when - * using this spritebatch - */ - render(element) - { - if (!element._texture.valid) - { - return; - } - - if (this._vertexCount + (element.vertexData.length / 2) > this.size) - { - this.flush(); - } - - this._vertexCount += element.vertexData.length / 2; - this._indexCount += element.indices.length; - this._bufferedElements[this._bufferSize++] = element; - } - - /** - * Renders the content _now_ and empties the current batch. - */ - flush() - { - if (this._vertexCount === 0) - { - return; - } - - const attributeBuffer = this.getAttributeBuffer(this._vertexCount); - const indexBuffer = this.getIndexBuffer(this._indexCount); - const gl = this.renderer.gl; - - const { - _bufferedElements: elements, - _drawCalls: drawCalls, - MAX_TEXTURES, - _packedGeometries: packedGeometries, - vertexSize, - } = this; - - const touch = this.renderer.textureGC.count; - - let index = 0; - let _indexCount = 0; - - let nextTexture; - let currentTexture; - let textureCount = 0; - - let currentGroup = drawCalls[0]; - let groupCount = 0; - - let blendMode = -1;// blend-mode of previous element/sprite/object! - - currentGroup.textureCount = 0; - currentGroup.start = 0; - currentGroup.blend = blendMode; - - let TICK = ++BaseTexture._globalBatch; - let i; - - for (i = 0; i < this._bufferSize; ++i) - { - const sprite = elements[i]; - - elements[i] = null; - nextTexture = sprite._texture.baseTexture; - - const spriteBlendMode = premultiplyBlendMode[ - nextTexture.premultiplyAlpha ? 1 : 0][sprite.blendMode]; - - if (blendMode !== spriteBlendMode) - { - blendMode = spriteBlendMode; - - // force the batch to break! - currentTexture = null; - textureCount = MAX_TEXTURES; - TICK++; - } - - if (currentTexture !== nextTexture) - { - currentTexture = nextTexture; - - if (nextTexture._batchEnabled !== TICK) - { - if (textureCount === MAX_TEXTURES) - { - TICK++; - - textureCount = 0; - - currentGroup.size = _indexCount - currentGroup.start; - - currentGroup = drawCalls[groupCount++]; - currentGroup.textureCount = 0; - currentGroup.blend = blendMode; - currentGroup.start = _indexCount; - } - - nextTexture.touched = touch; - nextTexture._batchEnabled = TICK; - nextTexture._id = textureCount; - - currentGroup.textures[currentGroup.textureCount++] = nextTexture; - textureCount++; - } - } - - this.packInterleavedGeometry(sprite, attributeBuffer, - indexBuffer, index, _indexCount); - - // push a graphics.. - index += (sprite.vertexData.length / 2) * vertexSize; - _indexCount += sprite.indices.length; - } - - BaseTexture._globalBatch = TICK; - currentGroup.size = _indexCount - currentGroup.start; - - if (!settings.CAN_UPLOAD_SAME_BUFFER) - { /* Usually on iOS devices, where the browser doesn't - like uploads to the same buffer in a single frame. */ - if (this._packedGeometryPoolSize <= this._flushId) - { - this._packedGeometryPoolSize++; - packedGeometries[this._flushId] = new (this.geometryClass)(); - } - - packedGeometries[this._flushId]._buffer.update(attributeBuffer.rawBinaryData, 0); - packedGeometries[this._flushId]._indexBuffer.update(indexBuffer, 0); - - this.renderer.geometry.bind(packedGeometries[this._flushId]); - this.renderer.geometry.updateBuffers(); - this._flushId++; - } - else - { - // lets use the faster option, always use buffer number 0 - packedGeometries[this._flushId]._buffer.update(attributeBuffer.rawBinaryData, 0); - packedGeometries[this._flushId]._indexBuffer.update(indexBuffer, 0); - - this.renderer.geometry.updateBuffers(); - } - - const textureSystem = this.renderer.texture; - const stateSystem = this.renderer.state; - - // Upload textures and do the draw calls - for (i = 0; i < groupCount; i++) - { - const group = drawCalls[i]; - const groupTextureCount = group.textureCount; - - for (let j = 0; j < groupTextureCount; j++) - { - textureSystem.bind(group.textures[j], j); - group.textures[j] = null; - } - - stateSystem.setBlendMode(group.blend); - gl.drawElements(group.type, group.size, gl.UNSIGNED_SHORT, group.start * 2); - } - - // reset elements for the next flush - this._bufferSize = 0; - this._vertexCount = 0; - this._indexCount = 0; - } - - /** - * Starts a new sprite batch. - */ - start() - { - this.renderer.state.set(this.state); - - this.renderer.shader.bind(this._shader); - - if (settings.CAN_UPLOAD_SAME_BUFFER) - { - // bind buffer #0, we don't need others - this.renderer.geometry.bind(this._packedGeometries[this._flushId]); - } - } - - /** - * Stops and flushes the current batch. - */ - stop() - { - this.flush(); - } - - /** - * Destroys this `BatchRenderer`. It cannot be used again. - */ - destroy() - { - for (let i = 0; i < this._packedGeometryPoolSize; i++) - { - if (this._packedGeometries[i]) - { - this._packedGeometries[i].destroy(); - } - } - - this.renderer.off('prerender', this.onPrerender, this); - - this._aBuffers = null; - this._iBuffers = null; - this._packedGeometries = null; - this._drawCalls = null; - - if (this._shader) - { - this._shader.destroy(); - this._shader = null; - } - - super.destroy(); - } - - /** - * Fetches an attribute buffer from `this._aBuffers` that - * can hold atleast `size` floats. - * - * @param {number} size - minimum capacity required - * @return {ViewableBuffer} - buffer than can hold atleast `size` floats - * @private - */ - getAttributeBuffer(size) - { - // 8 vertices is enough for 2 quads - const roundedP2 = nextPow2(Math.ceil(size / 8)); - const roundedSizeIndex = log2(roundedP2); - const roundedSize = roundedP2 * 8; - - if (this._aBuffers.length <= roundedSizeIndex) - { - this._iBuffers.length = roundedSizeIndex + 1; - } - - let buffer = this._aBuffers[roundedSize]; - - if (!buffer) - { - this._aBuffers[roundedSize] = buffer = new ViewableBuffer(roundedSize * this.vertexSize * 4); - } - - return buffer; - } - - /** - * Fetches an index buffer from `this._iBuffers` that can - * has atleast `size` capacity. - * - * @param {number} size - minimum required capacity - * @return {Uint16Array} - buffer that can fit `size` - * indices. - * @private - */ - 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; - } - - /** - * Takes the four batching parameters of `element`, interleaves - * and pushes them into the batching attribute/index buffers given. - * - * It uses these properties: `vertexData` `uvs`, `textureId` and - * `indicies`. It also uses the "tint" of the base-texture, if - * present. - * - * @param {PIXI.Sprite} element - element being rendered - * @param {PIXI.ViewableBuffer} attributeBuffer - attribute buffer. - * @param {Uint16Array} indexBuffer - index buffer - * @param {number} aIndex - number of floats already in the attribute buffer - * @param {number} iIndex - number of indices already in `indexBuffer` - */ - packInterleavedGeometry(element, attributeBuffer, indexBuffer, aIndex, iIndex) - { - const { - uint32View, - float32View, - } = attributeBuffer; - - const packedVertices = aIndex / this.vertexSize; - const uvs = element.uvs; - const indicies = element.indices; - const vertexData = element.vertexData; - const textureId = element._texture.baseTexture._id; - - const alpha = Math.min(element.worldAlpha, 1.0); - const argb = (alpha < 1.0 - && element._texture.baseTexture.premultiplyAlpha) - ? premultiplyTint(element._tintRGB, alpha) - : element._tintRGB + (alpha * 255 << 24); - - // lets not worry about tint! for now.. - for (let i = 0; i < vertexData.length; i += 2) - { - float32View[aIndex++] = vertexData[i]; - float32View[aIndex++] = vertexData[i + 1]; - float32View[aIndex++] = uvs[i]; - float32View[aIndex++] = uvs[i + 1]; - uint32View[aIndex++] = argb; - float32View[aIndex++] = textureId; - } - - for (let i = 0; i < indicies.length; i++) - { - indexBuffer[iIndex++] = packedVertices + indicies[i]; - } - } -} diff --git a/packages/core/src/batch/AbstractBatchRenderer.js b/packages/core/src/batch/AbstractBatchRenderer.js new file mode 100644 index 0000000..39a50f6 --- /dev/null +++ b/packages/core/src/batch/AbstractBatchRenderer.js @@ -0,0 +1,632 @@ +import BatchDrawCall from './BatchDrawCall'; +import BaseTexture from '../textures/BaseTexture'; +import ObjectRenderer from './ObjectRenderer'; +import State from '../state/State'; +import ViewableBuffer from '../geometry/ViewableBuffer'; + +import checkMaxIfStatementsInShader from '../shader/utils/checkMaxIfStatementsInShader'; + +import { settings } from '@pixi/settings'; +import { premultiplyBlendMode, premultiplyTint, nextPow2, log2 } from '@pixi/utils'; +import { ENV } from '@pixi/constants'; + +/** + * Renderer dedicated to drawing and batching sprites. + * + * This is the default batch renderer. It buffers objects + * with texture-based geometries and renders them in + * batches. It uploads multiple textures to the GPU to + * reduce to the number of draw calls. + * + * @class + * @protected + * @memberof PIXI + * @extends PIXI.ObjectRenderer + */ +export default class AbstractBatchRenderer extends ObjectRenderer +{ + /** + * This will hook onto the renderer's `contextChange` + * and `prerender` signals. + * + * @param {PIXI.Renderer} renderer - The renderer this works for. + */ + constructor(renderer) + { + super(renderer); + + /** + * This is used to generate a shader that can + * color each vertex based on a `aTextureId` + * attribute that points to an texture in `uSampler`. + * + * This enables the objects with different textures + * to be drawn in the same draw call. + * + * You can customize your shader by creating your + * custom shader generator. + * + * @member {PIXI.BatchShaderGenerator} + * @protected + */ + this.shaderGenerator = null; + + /** + * The class that represents the geometry of objects + * that are going to be batched with this. + * + * @member {object} + * @default PIXI.BatchGeometry + * @protected + */ + this.geometryClass = null; + + /** + * Size of data being buffered per vertex in the + * attribute buffers (in floats). By default, the + * batch-renderer plugin uses 6: + * + * | aVertexPosition | 2 | + * |-----------------|---| + * | aTextureCoords | 2 | + * | aColor | 1 | + * | aTextureId | 1 | + * + * @member {number} vertexSize + * @readonly + */ + this.vertexSize = null; + + /** + * The WebGL state in which this renderer will work. + * + * @member {PIXI.State} + * @readonly + */ + this.state = State.for2d(); + + /** + * The number of bufferable objects before a flush + * occurs automatically. + * + * @member {number} + * @default settings.SPRITE_MAX_TEXTURES + */ + this.size = 2000 * 4;// settings.SPRITE_BATCH_SIZE, 2000 is a nice balance between mobile/desktop + + /** + * Total count of all vertices used by the currently + * buffered objects. + * + * @member {number} + * @private + */ + this._vertexCount = 0; + + /** + * Total count of all indices used by the currently + * buffered objects. + * + * @member {number} + * @private + */ + this._indexCount = 0; + + /** + * Buffer of objects that are yet to be rendered. + * + * @member {PIXI.DisplayObject[]} + * @private + */ + this._bufferedElements = []; + + /** + * Number of elements that are buffered and are + * waiting to be flushed. + * + * @member {number} + * @private + */ + this._bufferSize = 0; + + /** + * This shader is generated by `this.shaderGenerator`. + * + * It is generated specifically to handle the required + * number of textures being batched together. + * + * @member {PIXI.Shader} + * @protected + */ + this._shader = null; + + /** + * Pool of `this.geometryClass` geometry objects + * that store buffers. They are used to pass data + * to the shader on each draw call. + * + * These are never re-allocated again, unless a + * context change occurs; however, the pool may + * be expanded if required. + * + * @member {PIXI.Geometry[]} + * @private + * @see PIXI.AbstractBatchRenderer.contextChange + */ + this._packedGeometries = []; + + /** + * Size of `this._packedGeometries`. It can be expanded + * if more than `this._packedGeometryPoolSize` flushes + * occur in a single frame. + * + * @member {number} + * @private + */ + this._packedGeometryPoolSize = 2; + + /** + * A flush may occur multiple times in a single + * frame. On iOS devices or when + * `settings.CAN_UPLOAD_SAME_BUFFER` is false, the + * batch renderer does not upload data to the same + * `WebGLBuffer` for performance reasons. + * + * This is the index into `packedGeometries` that points to + * geometry holding the most recent buffers. + * + * @member {number} + * @private + */ + this._flushId = 0; + + /** + * Pool of `BatchDrawCall` objects that `flush` used + * to create "batches" of the objects being rendered. + * + * These are never re-allocated again. + * + * @member BatchDrawCall[] + * @private + */ + this._drawCalls = []; + + for (let k = 0; k < this.size / 4; k++) + { // initialize the draw-calls pool to max size. + this._drawCalls[k] = new BatchDrawCall(); + } + + /** + * Pool of `ViewableBuffer` objects that are sorted in + * order of increasing size. The flush method uses + * the buffer with the least size above the amount + * it requires. These are used for passing attributes. + * + * The first buffer has a size of 8; each subsequent + * buffer has double capacity of its previous. + * + * @member {PIXI.ViewableBuffer} + * @private + * @see PIXI.AbstractBatchRenderer#getAttributeBuffer + */ + this._aBuffers = {}; + + /** + * Pool of `Uint16Array` objects that are sorted in + * order of increasing size. The flush method uses + * the buffer with the least size above the amount + * it requires. These are used for passing indices. + * + * The first buffer has a size of 12; each subsequent + * buffer has double capacity of its previous. + * + * @member {Uint16Array[]} + * @private + * @see PIXI.AbstractBatchRenderer#getIndexBuffer + */ + this._iBuffers = {}; + + /** + * Maximum number of textures that can be uploaded to + * the GPU under the current context. It is initialized + * properly in `this.contextChange`. + * + * @member {number} + * @see PIXI.AbstractBatchRenderer#contextChange + * @readonly + */ + this.MAX_TEXTURES = 1; + + this.renderer.on('prerender', this.onPrerender, this); + renderer.runners.contextChange.add(this); + } + + /** + * Handles the `contextChange` signal. + * + * It calculates `this.MAX_TEXTURES` and allocating the + * packed-geometry object pool. + */ + contextChange() + { + const gl = this.renderer.gl; + + if (settings.PREFER_ENV === ENV.WEBGL_LEGACY) + { + this.MAX_TEXTURES = 1; + } + else + { + // step 1: first check max textures the GPU can handle. + this.MAX_TEXTURES = Math.min( + gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), + settings.SPRITE_MAX_TEXTURES); + + // step 2: check the maximum number of if statements the shader can have too.. + this.MAX_TEXTURES = checkMaxIfStatementsInShader( + this.MAX_TEXTURES, gl); + } + + this._shader = this.shaderGenerator.generateShader(this.MAX_TEXTURES); + + // we use the second shader as the first one depending on your browser + // may omit aTextureId as it is not used by the shader so is optimized out. + for (let i = 0; i < this._packedGeometryPoolSize; i++) + { + /* eslint-disable max-len */ + this._packedGeometries[i] = new (this.geometryClass)(); + } + } + + /** + * Handles the `prerender` signal. + * + * It ensures that flushes start from the first geometry + * object again. + */ + onPrerender() + { + this._flushId = 0; + } + + /** + * Buffers the "batchable" object. It need not be rendered + * immediately. + * + * @param {PIXI.Sprite} sprite - the sprite to render when + * using this spritebatch + */ + render(element) + { + if (!element._texture.valid) + { + return; + } + + if (this._vertexCount + (element.vertexData.length / 2) > this.size) + { + this.flush(); + } + + this._vertexCount += element.vertexData.length / 2; + this._indexCount += element.indices.length; + this._bufferedElements[this._bufferSize++] = element; + } + + /** + * Renders the content _now_ and empties the current batch. + */ + flush() + { + if (this._vertexCount === 0) + { + return; + } + + const attributeBuffer = this.getAttributeBuffer(this._vertexCount); + const indexBuffer = this.getIndexBuffer(this._indexCount); + const gl = this.renderer.gl; + + const { + _bufferedElements: elements, + _drawCalls: drawCalls, + MAX_TEXTURES, + _packedGeometries: packedGeometries, + vertexSize, + } = this; + + const touch = this.renderer.textureGC.count; + + let index = 0; + let _indexCount = 0; + + let nextTexture; + let currentTexture; + let textureCount = 0; + + let currentGroup = drawCalls[0]; + let groupCount = 0; + + let blendMode = -1;// blend-mode of previous element/sprite/object! + + currentGroup.textureCount = 0; + currentGroup.start = 0; + currentGroup.blend = blendMode; + + let TICK = ++BaseTexture._globalBatch; + let i; + + for (i = 0; i < this._bufferSize; ++i) + { + const sprite = elements[i]; + + elements[i] = null; + nextTexture = sprite._texture.baseTexture; + + const spriteBlendMode = premultiplyBlendMode[ + nextTexture.premultiplyAlpha ? 1 : 0][sprite.blendMode]; + + if (blendMode !== spriteBlendMode) + { + blendMode = spriteBlendMode; + + // force the batch to break! + currentTexture = null; + textureCount = MAX_TEXTURES; + TICK++; + } + + if (currentTexture !== nextTexture) + { + currentTexture = nextTexture; + + if (nextTexture._batchEnabled !== TICK) + { + if (textureCount === MAX_TEXTURES) + { + TICK++; + + textureCount = 0; + + currentGroup.size = _indexCount - currentGroup.start; + + currentGroup = drawCalls[groupCount++]; + currentGroup.textureCount = 0; + currentGroup.blend = blendMode; + currentGroup.start = _indexCount; + } + + nextTexture.touched = touch; + nextTexture._batchEnabled = TICK; + nextTexture._id = textureCount; + + currentGroup.textures[currentGroup.textureCount++] = nextTexture; + textureCount++; + } + } + + this.packInterleavedGeometry(sprite, attributeBuffer, + indexBuffer, index, _indexCount); + + // push a graphics.. + index += (sprite.vertexData.length / 2) * vertexSize; + _indexCount += sprite.indices.length; + } + + BaseTexture._globalBatch = TICK; + currentGroup.size = _indexCount - currentGroup.start; + + if (!settings.CAN_UPLOAD_SAME_BUFFER) + { /* Usually on iOS devices, where the browser doesn't + like uploads to the same buffer in a single frame. */ + if (this._packedGeometryPoolSize <= this._flushId) + { + this._packedGeometryPoolSize++; + packedGeometries[this._flushId] = new (this.geometryClass)(); + } + + packedGeometries[this._flushId]._buffer.update(attributeBuffer.rawBinaryData, 0); + packedGeometries[this._flushId]._indexBuffer.update(indexBuffer, 0); + + this.renderer.geometry.bind(packedGeometries[this._flushId]); + this.renderer.geometry.updateBuffers(); + this._flushId++; + } + else + { + // lets use the faster option, always use buffer number 0 + packedGeometries[this._flushId]._buffer.update(attributeBuffer.rawBinaryData, 0); + packedGeometries[this._flushId]._indexBuffer.update(indexBuffer, 0); + + this.renderer.geometry.updateBuffers(); + } + + const textureSystem = this.renderer.texture; + const stateSystem = this.renderer.state; + + // Upload textures and do the draw calls + for (i = 0; i < groupCount; i++) + { + const group = drawCalls[i]; + const groupTextureCount = group.textureCount; + + for (let j = 0; j < groupTextureCount; j++) + { + textureSystem.bind(group.textures[j], j); + group.textures[j] = null; + } + + stateSystem.setBlendMode(group.blend); + gl.drawElements(group.type, group.size, gl.UNSIGNED_SHORT, group.start * 2); + } + + // reset elements for the next flush + this._bufferSize = 0; + this._vertexCount = 0; + this._indexCount = 0; + } + + /** + * Starts a new sprite batch. + */ + start() + { + this.renderer.state.set(this.state); + + this.renderer.shader.bind(this._shader); + + if (settings.CAN_UPLOAD_SAME_BUFFER) + { + // bind buffer #0, we don't need others + this.renderer.geometry.bind(this._packedGeometries[this._flushId]); + } + } + + /** + * Stops and flushes the current batch. + */ + stop() + { + this.flush(); + } + + /** + * Destroys this `AbstractBatchRenderer`. It cannot be used again. + */ + destroy() + { + for (let i = 0; i < this._packedGeometryPoolSize; i++) + { + if (this._packedGeometries[i]) + { + this._packedGeometries[i].destroy(); + } + } + + this.renderer.off('prerender', this.onPrerender, this); + + this._aBuffers = null; + this._iBuffers = null; + this._packedGeometries = null; + this._drawCalls = null; + + if (this._shader) + { + this._shader.destroy(); + this._shader = null; + } + + super.destroy(); + } + + /** + * Fetches an attribute buffer from `this._aBuffers` that + * can hold atleast `size` floats. + * + * @param {number} size - minimum capacity required + * @return {ViewableBuffer} - buffer than can hold atleast `size` floats + * @private + */ + getAttributeBuffer(size) + { + // 8 vertices is enough for 2 quads + const roundedP2 = nextPow2(Math.ceil(size / 8)); + const roundedSizeIndex = log2(roundedP2); + const roundedSize = roundedP2 * 8; + + if (this._aBuffers.length <= roundedSizeIndex) + { + this._iBuffers.length = roundedSizeIndex + 1; + } + + let buffer = this._aBuffers[roundedSize]; + + if (!buffer) + { + this._aBuffers[roundedSize] = buffer = new ViewableBuffer(roundedSize * this.vertexSize * 4); + } + + return buffer; + } + + /** + * Fetches an index buffer from `this._iBuffers` that can + * has atleast `size` capacity. + * + * @param {number} size - minimum required capacity + * @return {Uint16Array} - buffer that can fit `size` + * indices. + * @private + */ + 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; + } + + /** + * Takes the four batching parameters of `element`, interleaves + * and pushes them into the batching attribute/index buffers given. + * + * It uses these properties: `vertexData` `uvs`, `textureId` and + * `indicies`. It also uses the "tint" of the base-texture, if + * present. + * + * @param {PIXI.Sprite} element - element being rendered + * @param {PIXI.ViewableBuffer} attributeBuffer - attribute buffer. + * @param {Uint16Array} indexBuffer - index buffer + * @param {number} aIndex - number of floats already in the attribute buffer + * @param {number} iIndex - number of indices already in `indexBuffer` + */ + packInterleavedGeometry(element, attributeBuffer, indexBuffer, aIndex, iIndex) + { + const { + uint32View, + float32View, + } = attributeBuffer; + + const packedVertices = aIndex / this.vertexSize; + const uvs = element.uvs; + const indicies = element.indices; + const vertexData = element.vertexData; + const textureId = element._texture.baseTexture._id; + + const alpha = Math.min(element.worldAlpha, 1.0); + const argb = (alpha < 1.0 + && element._texture.baseTexture.premultiplyAlpha) + ? premultiplyTint(element._tintRGB, alpha) + : element._tintRGB + (alpha * 255 << 24); + + // lets not worry about tint! for now.. + for (let i = 0; i < vertexData.length; i += 2) + { + float32View[aIndex++] = vertexData[i]; + float32View[aIndex++] = vertexData[i + 1]; + float32View[aIndex++] = uvs[i]; + float32View[aIndex++] = uvs[i + 1]; + uint32View[aIndex++] = argb; + float32View[aIndex++] = textureId; + } + + for (let i = 0; i < indicies.length; i++) + { + indexBuffer[iIndex++] = packedVertices + indicies[i]; + } + } +} diff --git a/packages/core/src/batch/BatchPluginFactory.js b/packages/core/src/batch/BatchPluginFactory.js index f2785ae..63a9696 100644 --- a/packages/core/src/batch/BatchPluginFactory.js +++ b/packages/core/src/batch/BatchPluginFactory.js @@ -1,6 +1,6 @@ import BatchShaderGenerator from './BatchShaderGenerator'; import BatchGeometry from './BatchGeometry'; -import BaseBatchRenderer from './BatchRenderer'; +import AbstractBatchRenderer from './AbstractBatchRenderer'; import defaultVertex from './texture.vert'; import defaultFragment from './texture.frag'; @@ -50,7 +50,7 @@ vertexSize: 6, }, options); - return class BatchPlugin extends BaseBatchRenderer + return class BatchPlugin extends AbstractBatchRenderer { constructor(renderer) { diff --git a/packages/core/src/batch/BatchRenderer.js b/packages/core/src/batch/BatchRenderer.js deleted file mode 100644 index d75a35d..0000000 --- a/packages/core/src/batch/BatchRenderer.js +++ /dev/null @@ -1,632 +0,0 @@ -import BatchDrawCall from './BatchDrawCall'; -import BaseTexture from '../textures/BaseTexture'; -import ObjectRenderer from './ObjectRenderer'; -import State from '../state/State'; -import ViewableBuffer from '../geometry/ViewableBuffer'; - -import checkMaxIfStatementsInShader from '../shader/utils/checkMaxIfStatementsInShader'; - -import { settings } from '@pixi/settings'; -import { premultiplyBlendMode, premultiplyTint, nextPow2, log2 } from '@pixi/utils'; -import { ENV } from '@pixi/constants'; - -/** - * Renderer dedicated to drawing and batching sprites. - * - * This is the default batch renderer. It buffers objects - * with texture-based geometries and renders them in - * batches. It uploads multiple textures to the GPU to - * reduce to the number of draw calls. - * - * @class - * @protected - * @memberof PIXI - * @extends PIXI.ObjectRenderer - */ -export default class BatchRenderer extends ObjectRenderer -{ - /** - * This will hook onto the renderer's `contextChange` - * and `prerender` signals. - * - * @param {PIXI.Renderer} renderer - The renderer this works for. - */ - constructor(renderer) - { - super(renderer); - - /** - * This is used to generate a shader that can - * color each vertex based on a `aTextureId` - * attribute that points to an texture in `uSampler`. - * - * This enables the objects with different textures - * to be drawn in the same draw call. - * - * You can customize your shader by creating your - * custom shader generator. - * - * @member {PIXI.BatchShaderGenerator} - * @protected - */ - this.shaderGenerator = null; - - /** - * The class that represents the geometry of objects - * that are going to be batched with this. - * - * @member {object} - * @default PIXI.BatchGeometry - * @protected - */ - this.geometryClass = null; - - /** - * Size of data being buffered per vertex in the - * attribute buffers (in floats). By default, the - * batch-renderer plugin uses 6: - * - * | aVertexPosition | 2 | - * |-----------------|---| - * | aTextureCoords | 2 | - * | aColor | 1 | - * | aTextureId | 1 | - * - * @member {number} vertexSize - * @readonly - */ - this.vertexSize = null; - - /** - * The WebGL state in which this renderer will work. - * - * @member {PIXI.State} - * @readonly - */ - this.state = State.for2d(); - - /** - * The number of bufferable objects before a flush - * occurs automatically. - * - * @member {number} - * @default settings.SPRITE_MAX_TEXTURES - */ - this.size = 2000 * 4;// settings.SPRITE_BATCH_SIZE, 2000 is a nice balance between mobile/desktop - - /** - * Total count of all vertices used by the currently - * buffered objects. - * - * @member {number} - * @private - */ - this._vertexCount = 0; - - /** - * Total count of all indices used by the currently - * buffered objects. - * - * @member {number} - * @private - */ - this._indexCount = 0; - - /** - * Buffer of objects that are yet to be rendered. - * - * @member {PIXI.DisplayObject[]} - * @private - */ - this._bufferedElements = []; - - /** - * Number of elements that are buffered and are - * waiting to be flushed. - * - * @member {number} - * @private - */ - this._bufferSize = 0; - - /** - * This shader is generated by `this.shaderGenerator`. - * - * It is generated specifically to handle the required - * number of textures being batched together. - * - * @member {PIXI.Shader} - * @protected - */ - this._shader = null; - - /** - * Pool of `this.geometryClass` geometry objects - * that store buffers. They are used to pass data - * to the shader on each draw call. - * - * These are never re-allocated again, unless a - * context change occurs; however, the pool may - * be expanded if required. - * - * @member {PIXI.Geometry[]} - * @private - * @see PIXI.BatchRenderer.contextChange - */ - this._packedGeometries = []; - - /** - * Size of `this._packedGeometries`. It can be expanded - * if more than `this._packedGeometryPoolSize` flushes - * occur in a single frame. - * - * @member {number} - * @private - */ - this._packedGeometryPoolSize = 2; - - /** - * A flush may occur multiple times in a single - * frame. On iOS devices or when - * `settings.CAN_UPLOAD_SAME_BUFFER` is false, the - * batch renderer does not upload data to the same - * `WebGLBuffer` for performance reasons. - * - * This is the index into `packedGeometries` that points to - * geometry holding the most recent buffers. - * - * @member {number} - * @private - */ - this._flushId = 0; - - /** - * Pool of `BatchDrawCall` objects that `flush` used - * to create "batches" of the objects being rendered. - * - * These are never re-allocated again. - * - * @member BatchDrawCall[] - * @private - */ - this._drawCalls = []; - - for (let k = 0; k < this.size / 4; k++) - { // initialize the draw-calls pool to max size. - this._drawCalls[k] = new BatchDrawCall(); - } - - /** - * Pool of `ViewableBuffer` objects that are sorted in - * order of increasing size. The flush method uses - * the buffer with the least size above the amount - * it requires. These are used for passing attributes. - * - * The first buffer has a size of 8; each subsequent - * buffer has double capacity of its previous. - * - * @member {PIXI.ViewableBuffer} - * @private - * @see PIXI.BatchRenderer#getAttributeBuffer - */ - this._aBuffers = {}; - - /** - * Pool of `Uint16Array` objects that are sorted in - * order of increasing size. The flush method uses - * the buffer with the least size above the amount - * it requires. These are used for passing indices. - * - * The first buffer has a size of 12; each subsequent - * buffer has double capacity of its previous. - * - * @member {Uint16Array[]} - * @private - * @see PIXI.BatchRenderer#getIndexBuffer - */ - this._iBuffers = {}; - - /** - * Maximum number of textures that can be uploaded to - * the GPU under the current context. It is initialized - * properly in `this.contextChange`. - * - * @member {number} - * @see PIXI.BatchRenderer#contextChange - * @readonly - */ - this.MAX_TEXTURES = 1; - - this.renderer.on('prerender', this.onPrerender, this); - renderer.runners.contextChange.add(this); - } - - /** - * Handles the `contextChange` signal. - * - * It calculates `this.MAX_TEXTURES` and allocating the - * packed-geometry object pool. - */ - contextChange() - { - const gl = this.renderer.gl; - - if (settings.PREFER_ENV === ENV.WEBGL_LEGACY) - { - this.MAX_TEXTURES = 1; - } - else - { - // step 1: first check max textures the GPU can handle. - this.MAX_TEXTURES = Math.min( - gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), - settings.SPRITE_MAX_TEXTURES); - - // step 2: check the maximum number of if statements the shader can have too.. - this.MAX_TEXTURES = checkMaxIfStatementsInShader( - this.MAX_TEXTURES, gl); - } - - this._shader = this.shaderGenerator.generateShader(this.MAX_TEXTURES); - - // we use the second shader as the first one depending on your browser - // may omit aTextureId as it is not used by the shader so is optimized out. - for (let i = 0; i < this._packedGeometryPoolSize; i++) - { - /* eslint-disable max-len */ - this._packedGeometries[i] = new (this.geometryClass)(); - } - } - - /** - * Handles the `prerender` signal. - * - * It ensures that flushes start from the first geometry - * object again. - */ - onPrerender() - { - this._flushId = 0; - } - - /** - * Buffers the "batchable" object. It need not be rendered - * immediately. - * - * @param {PIXI.Sprite} sprite - the sprite to render when - * using this spritebatch - */ - render(element) - { - if (!element._texture.valid) - { - return; - } - - if (this._vertexCount + (element.vertexData.length / 2) > this.size) - { - this.flush(); - } - - this._vertexCount += element.vertexData.length / 2; - this._indexCount += element.indices.length; - this._bufferedElements[this._bufferSize++] = element; - } - - /** - * Renders the content _now_ and empties the current batch. - */ - flush() - { - if (this._vertexCount === 0) - { - return; - } - - const attributeBuffer = this.getAttributeBuffer(this._vertexCount); - const indexBuffer = this.getIndexBuffer(this._indexCount); - const gl = this.renderer.gl; - - const { - _bufferedElements: elements, - _drawCalls: drawCalls, - MAX_TEXTURES, - _packedGeometries: packedGeometries, - vertexSize, - } = this; - - const touch = this.renderer.textureGC.count; - - let index = 0; - let _indexCount = 0; - - let nextTexture; - let currentTexture; - let textureCount = 0; - - let currentGroup = drawCalls[0]; - let groupCount = 0; - - let blendMode = -1;// blend-mode of previous element/sprite/object! - - currentGroup.textureCount = 0; - currentGroup.start = 0; - currentGroup.blend = blendMode; - - let TICK = ++BaseTexture._globalBatch; - let i; - - for (i = 0; i < this._bufferSize; ++i) - { - const sprite = elements[i]; - - elements[i] = null; - nextTexture = sprite._texture.baseTexture; - - const spriteBlendMode = premultiplyBlendMode[ - nextTexture.premultiplyAlpha ? 1 : 0][sprite.blendMode]; - - if (blendMode !== spriteBlendMode) - { - blendMode = spriteBlendMode; - - // force the batch to break! - currentTexture = null; - textureCount = MAX_TEXTURES; - TICK++; - } - - if (currentTexture !== nextTexture) - { - currentTexture = nextTexture; - - if (nextTexture._batchEnabled !== TICK) - { - if (textureCount === MAX_TEXTURES) - { - TICK++; - - textureCount = 0; - - currentGroup.size = _indexCount - currentGroup.start; - - currentGroup = drawCalls[groupCount++]; - currentGroup.textureCount = 0; - currentGroup.blend = blendMode; - currentGroup.start = _indexCount; - } - - nextTexture.touched = touch; - nextTexture._batchEnabled = TICK; - nextTexture._id = textureCount; - - currentGroup.textures[currentGroup.textureCount++] = nextTexture; - textureCount++; - } - } - - this.packInterleavedGeometry(sprite, attributeBuffer, - indexBuffer, index, _indexCount); - - // push a graphics.. - index += (sprite.vertexData.length / 2) * vertexSize; - _indexCount += sprite.indices.length; - } - - BaseTexture._globalBatch = TICK; - currentGroup.size = _indexCount - currentGroup.start; - - if (!settings.CAN_UPLOAD_SAME_BUFFER) - { /* Usually on iOS devices, where the browser doesn't - like uploads to the same buffer in a single frame. */ - if (this._packedGeometryPoolSize <= this._flushId) - { - this._packedGeometryPoolSize++; - packedGeometries[this._flushId] = new (this.geometryClass)(); - } - - packedGeometries[this._flushId]._buffer.update(attributeBuffer.rawBinaryData, 0); - packedGeometries[this._flushId]._indexBuffer.update(indexBuffer, 0); - - this.renderer.geometry.bind(packedGeometries[this._flushId]); - this.renderer.geometry.updateBuffers(); - this._flushId++; - } - else - { - // lets use the faster option, always use buffer number 0 - packedGeometries[this._flushId]._buffer.update(attributeBuffer.rawBinaryData, 0); - packedGeometries[this._flushId]._indexBuffer.update(indexBuffer, 0); - - this.renderer.geometry.updateBuffers(); - } - - const textureSystem = this.renderer.texture; - const stateSystem = this.renderer.state; - - // Upload textures and do the draw calls - for (i = 0; i < groupCount; i++) - { - const group = drawCalls[i]; - const groupTextureCount = group.textureCount; - - for (let j = 0; j < groupTextureCount; j++) - { - textureSystem.bind(group.textures[j], j); - group.textures[j] = null; - } - - stateSystem.setBlendMode(group.blend); - gl.drawElements(group.type, group.size, gl.UNSIGNED_SHORT, group.start * 2); - } - - // reset elements for the next flush - this._bufferSize = 0; - this._vertexCount = 0; - this._indexCount = 0; - } - - /** - * Starts a new sprite batch. - */ - start() - { - this.renderer.state.set(this.state); - - this.renderer.shader.bind(this._shader); - - if (settings.CAN_UPLOAD_SAME_BUFFER) - { - // bind buffer #0, we don't need others - this.renderer.geometry.bind(this._packedGeometries[this._flushId]); - } - } - - /** - * Stops and flushes the current batch. - */ - stop() - { - this.flush(); - } - - /** - * Destroys this `BatchRenderer`. It cannot be used again. - */ - destroy() - { - for (let i = 0; i < this._packedGeometryPoolSize; i++) - { - if (this._packedGeometries[i]) - { - this._packedGeometries[i].destroy(); - } - } - - this.renderer.off('prerender', this.onPrerender, this); - - this._aBuffers = null; - this._iBuffers = null; - this._packedGeometries = null; - this._drawCalls = null; - - if (this._shader) - { - this._shader.destroy(); - this._shader = null; - } - - super.destroy(); - } - - /** - * Fetches an attribute buffer from `this._aBuffers` that - * can hold atleast `size` floats. - * - * @param {number} size - minimum capacity required - * @return {ViewableBuffer} - buffer than can hold atleast `size` floats - * @private - */ - getAttributeBuffer(size) - { - // 8 vertices is enough for 2 quads - const roundedP2 = nextPow2(Math.ceil(size / 8)); - const roundedSizeIndex = log2(roundedP2); - const roundedSize = roundedP2 * 8; - - if (this._aBuffers.length <= roundedSizeIndex) - { - this._iBuffers.length = roundedSizeIndex + 1; - } - - let buffer = this._aBuffers[roundedSize]; - - if (!buffer) - { - this._aBuffers[roundedSize] = buffer = new ViewableBuffer(roundedSize * this.vertexSize * 4); - } - - return buffer; - } - - /** - * Fetches an index buffer from `this._iBuffers` that can - * has atleast `size` capacity. - * - * @param {number} size - minimum required capacity - * @return {Uint16Array} - buffer that can fit `size` - * indices. - * @private - */ - 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; - } - - /** - * Takes the four batching parameters of `element`, interleaves - * and pushes them into the batching attribute/index buffers given. - * - * It uses these properties: `vertexData` `uvs`, `textureId` and - * `indicies`. It also uses the "tint" of the base-texture, if - * present. - * - * @param {PIXI.Sprite} element - element being rendered - * @param {PIXI.ViewableBuffer} attributeBuffer - attribute buffer. - * @param {Uint16Array} indexBuffer - index buffer - * @param {number} aIndex - number of floats already in the attribute buffer - * @param {number} iIndex - number of indices already in `indexBuffer` - */ - packInterleavedGeometry(element, attributeBuffer, indexBuffer, aIndex, iIndex) - { - const { - uint32View, - float32View, - } = attributeBuffer; - - const packedVertices = aIndex / this.vertexSize; - const uvs = element.uvs; - const indicies = element.indices; - const vertexData = element.vertexData; - const textureId = element._texture.baseTexture._id; - - const alpha = Math.min(element.worldAlpha, 1.0); - const argb = (alpha < 1.0 - && element._texture.baseTexture.premultiplyAlpha) - ? premultiplyTint(element._tintRGB, alpha) - : element._tintRGB + (alpha * 255 << 24); - - // lets not worry about tint! for now.. - for (let i = 0; i < vertexData.length; i += 2) - { - float32View[aIndex++] = vertexData[i]; - float32View[aIndex++] = vertexData[i + 1]; - float32View[aIndex++] = uvs[i]; - float32View[aIndex++] = uvs[i + 1]; - uint32View[aIndex++] = argb; - float32View[aIndex++] = textureId; - } - - for (let i = 0; i < indicies.length; i++) - { - indexBuffer[iIndex++] = packedVertices + indicies[i]; - } - } -} diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 08ba1c1..df353a4 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -22,6 +22,7 @@ export { default as TextureUvs } from './textures/TextureUvs'; export { default as State } from './state/State'; export { default as ObjectRenderer } from './batch/ObjectRenderer'; +export { default as AbstractBatchRenderer } from './batch/AbstractBatchRenderer'; export { default as BatchPluginFactory, BatchRenderer } from './batch/BatchPluginFactory'; export { default as BatchShaderGenerator } from './batch/BatchShaderGenerator'; export { default as BatchGeometry } from './batch/BatchGeometry';