import BatchDrawCall from './BatchDrawCall'; import BaseTexture from '../textures/BaseTexture'; import State from '../state/State'; import ObjectRenderer from './ObjectRenderer'; import checkMaxIfStatementsInShader from '../shader/utils/checkMaxIfStatementsInShader'; import { settings } from '@pixi/settings'; import { premultiplyBlendMode, premultiplyTint, nextPow2, log2 } from '@pixi/utils'; import BatchBuffer from './BatchBuffer'; 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} * @readonly */ this.shaderGenerator = null; /** * The class that represents the geometry of objects * that are going to be batched with this. * * @member {object} * @default PIXI.BatchGeometry * @readonly */ 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} * @private */ 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 `BatchBuffer` 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.BatchBuffer} * @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 and empties the current batch. * */ flush() { if (this._vertexCount === 0) { return; } const gl = this.renderer.gl; const MAX_TEXTURES = this.MAX_TEXTURES; const vertSize = this.vertexSize; const buffer = this.getAttributeBuffer(this._vertexCount); const indexBuffer = this.getIndexBuffer(this._indexCount); const elements = this._bufferedElements; const _drawCalls = this._drawCalls; const float32View = buffer.float32View; const uint32View = buffer.uint32View; const touch = this.renderer.textureGC.count; let index = 0; let _indexCount = 0; let nextTexture; let currentTexture; let groupCount = 0; let textureCount = 0; let currentGroup = _drawCalls[0]; let blendMode = -1;// premultiplyBlendMode[elements[0]._texture.baseTexture.premultiplyAlpha ? 0 : ][elements[0].blendMode]; currentGroup.textureCount = 0; currentGroup.start = 0; currentGroup.blend = blendMode; let TICK = ++BaseTexture._globalBatch; let i; for (i = 0; i < this._bufferSize; ++i) { // upload the sprite elements... // they have all ready been calculated so we just need to push them into the buffer. 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, float32View, uint32View, indexBuffer, index, _indexCount);// argb, nextTexture._id, float32View, uint32View, indexBuffer, index, _indexCount); // push a graphics.. index += (sprite.vertexData.length / 2) * vertSize; _indexCount += sprite.indices.length; } BaseTexture._globalBatch = TICK; currentGroup.size = _indexCount - currentGroup.start; if (!settings.CAN_UPLOAD_SAME_BUFFER) { // this is still needed for IOS performance.. // it really does not like uploading to the same buffer in a single frame! if (this._packedGeometryPoolSize <= this._flushId) { this._packedGeometryPoolSize++; /* eslint-disable max-len */ this._packedGeometries[this._flushId] = new (this.geometryClass)(); } this._packedGeometries[this._flushId]._buffer.update(buffer.vertices, 0); this._packedGeometries[this._flushId]._indexBuffer.update(indexBuffer, 0); // this.vertexBuffers[this._flushId].update(buffer.vertices, 0); this.renderer.geometry.bind(this._packedGeometries[this._flushId]); this.renderer.geometry.updateBuffers(); this._flushId++; } else { // lets use the faster option, always use buffer number 0 this._packedGeometries[this._flushId]._buffer.update(buffer.vertices, 0); this._packedGeometries[this._flushId]._indexBuffer.update(indexBuffer, 0); // if (true)// this.spriteOnly) // { // this._packedGeometries[this._flushId].indexBuffer = this.defualtSpriteIndexBuffer; // this._packedGeometries[this._flushId].buffers[1] = this.defualtSpriteIndexBuffer; // } this.renderer.geometry.updateBuffers(); } // this.renderer.state.set(this.state); const textureSystem = this.renderer.texture; const stateSystem = this.renderer.state; // e.log(groupCount); // / render the _drawCalls.. 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; } // this.state.blendMode = group.blend; // this.state.blend = true; // this.renderer.state.setState(this.state); // set the blend mode.. 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 {BatchBuffer} - 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 BatchBuffer(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 {FLoat32Array} float32View - float32-view of the attribute buffer * @param {Uint32Array} uint32View - uint32-view of the 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, float32View, uint32View, indexBuffer, aIndex, iIndex) { const p = 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++] = p + indicies[i]; } } }