import ObjectRenderer from '../../renderers/webgl/utils/ObjectRenderer'; import WebGLRenderer from '../../renderers/webgl/WebGLRenderer'; import createIndicesForQuads from '../../utils/createIndicesForQuads'; import generateMultiTextureShader from './generateMultiTextureShader'; import checkMaxIfStatmentsInShader from '../../renderers/webgl/utils/checkMaxIfStatmentsInShader'; import Buffer from './BatchBuffer'; import { SPRITE_BATCH_SIZE, SPRITE_MAX_TEXTURES } from '../../const'; import glCore from 'pixi-gl-core'; import bitTwiddle from 'bit-twiddle'; let TICK = 0; /** * Renderer dedicated to drawing and batching sprites. * * @class * @private * @memberof PIXI * @extends PIXI.ObjectRenderer */ export default class SpriteRenderer extends ObjectRenderer { /** * @param {PIXI.WebGLRenderer} renderer - The renderer this sprite batch works for. */ constructor(renderer) { super(renderer); /** * Number of values sent in the vertex buffer. * positionX, positionY, colorR, colorG, colorB = 5 * * @member {number} */ this.vertSize = 5; /** * The size of the vertex information in bytes. * * @member {number} */ this.vertByteSize = this.vertSize * 4; /** * The number of images in the SpriteBatch before it flushes. * * @member {number} */ this.size = SPRITE_BATCH_SIZE; // 2000 is a nice balance between mobile / desktop // the total number of bytes in our batch // let numVerts = this.size * 4 * this.vertByteSize; this.buffers = []; for (let i = 1; i <= bitTwiddle.nextPow2(this.size); i *= 2) { this.buffers.push(new Buffer(i * 4 * this.vertByteSize)); } /** * Holds the indices of the geometry (quads) to draw * * @member {Uint16Array} */ this.indices = createIndicesForQuads(this.size); /** * The default shaders that is used if a sprite doesn't have a more specific one. * there is a shader for each number of textures that can be rendererd. * These shaders will also be generated on the fly as required. * @member {PIXI.Shader[]} */ this.shaders = null; this.currentIndex = 0; TICK = 0; this.groups = []; for (let k = 0; k < this.size; k++) { this.groups[k] = { textures: [], textureCount: 0, ids: [], size: 0, start: 0, blend: 0 }; } this.sprites = []; this.vertexBuffers = []; this.vaos = []; this.vaoMax = 2; this.vertexCount = 0; this.renderer.on('prerender', this.onPrerender, this); } /** * Sets up the renderer context and necessary buffers. * * @private */ onContextChange() { const gl = this.renderer.gl; // step 1: first check max textures the GPU can handle. this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), SPRITE_MAX_TEXTURES); // step 2: check the maximum number of if statements the shader can have too.. this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); this.shaders = new Array(this.MAX_TEXTURES); this.shaders[0] = generateMultiTextureShader(gl, 1); this.shaders[1] = generateMultiTextureShader(gl, 2); // create a couple of buffers this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); // 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. const shader = this.shaders[1]; for (let i = 0; i < this.vaoMax; i++) { this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); /* eslint-disable max-len */ // build the vao object that will render.. this.vaos[i] = this.renderer.createVao() .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[i], shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); /* eslint-disable max-len */ } this.vao = this.vaos[0]; this.currentBlendMode = 99999; } /** * Called before the renderer starts rendering. * */ onPrerender() { this.vertexCount = 0; } /** * Renders the sprite object. * * @param {PIXI.Sprite} sprite - the sprite to render when using this spritebatch */ render(sprite) { // TODO set blend modes.. // check texture.. if (this.currentIndex >= this.size) { this.flush(); } // get the uvs for the texture // if the uvs have not updated then no point rendering just yet! if (!sprite.texture._uvs) { return; } // push a texture. // increment the batchsize this.sprites[this.currentIndex++] = sprite; } /** * Renders the content and empties the current batch. * */ flush() { if (this.currentIndex === 0) { return; } const gl = this.renderer.gl; const np2 = bitTwiddle.nextPow2(this.currentIndex); const log2 = bitTwiddle.log2(np2); const buffer = this.buffers[log2]; const sprites = this.sprites; const groups = this.groups; const float32View = buffer.float32View; const uint32View = buffer.uint32View; let index = 0; let nextTexture; let currentTexture; let groupCount = 1; let textureCount = 0; let currentGroup = groups[0]; let vertexData; let tint; let uvs; let textureId; let blendMode = sprites[0].blendMode; let shader; currentGroup.textureCount = 0; currentGroup.start = 0; currentGroup.blend = blendMode; TICK++; let i; for (i = 0; i < this.currentIndex; i++) { // upload the sprite elemetns... // they have all ready been calculated so we just need to push them into the buffer. const sprite = sprites[i]; nextTexture = sprite._texture.baseTexture; if (blendMode !== sprite.blendMode) { blendMode = sprite.blendMode; // force the batch to break! currentTexture = null; textureCount = this.MAX_TEXTURES; TICK++; } if (currentTexture !== nextTexture) { currentTexture = nextTexture; if (nextTexture._enabled !== TICK) { if (textureCount === this.MAX_TEXTURES) { TICK++; textureCount = 0; currentGroup.size = i - currentGroup.start; currentGroup = groups[groupCount++]; currentGroup.textureCount = 0; currentGroup.blend = blendMode; currentGroup.start = i; } nextTexture._enabled = TICK; nextTexture._id = textureCount; currentGroup.textures[currentGroup.textureCount++] = nextTexture; textureCount++; } } vertexData = sprite.vertexData; // TODO this sum does not need to be set each frame.. tint = sprite._tintRGB + (sprite.worldAlpha * 255 << 24); uvs = sprite._texture._uvs.uvsUint32; textureId = nextTexture._id; if (this.renderer.roundPixels) { const resolution = this.renderer.resolution; // xy float32View[index] = ((vertexData[0] * resolution) | 0) / resolution; float32View[index + 1] = ((vertexData[1] * resolution) | 0) / resolution; // xy float32View[index + 5] = ((vertexData[2] * resolution) | 0) / resolution; float32View[index + 6] = ((vertexData[3] * resolution) | 0) / resolution; // xy float32View[index + 10] = ((vertexData[4] * resolution) | 0) / resolution; float32View[index + 11] = ((vertexData[5] * resolution) | 0) / resolution; // xy float32View[index + 15] = ((vertexData[6] * resolution) | 0) / resolution; float32View[index + 16] = ((vertexData[7] * resolution) | 0) / resolution; } else { // xy float32View[index] = vertexData[0]; float32View[index + 1] = vertexData[1]; // xy float32View[index + 5] = vertexData[2]; float32View[index + 6] = vertexData[3]; // xy float32View[index + 10] = vertexData[4]; float32View[index + 11] = vertexData[5]; // xy float32View[index + 15] = vertexData[6]; float32View[index + 16] = vertexData[7]; } uint32View[index + 2] = uvs[0]; uint32View[index + 7] = uvs[1]; uint32View[index + 12] = uvs[2]; uint32View[index + 17] = uvs[3]; uint32View[index + 3] = uint32View[index + 8] = uint32View[index + 13] = uint32View[index + 18] = tint; float32View[index + 4] = float32View[index + 9] = float32View[index + 14] = float32View[index + 19] = textureId; index += 20; } currentGroup.size = i - currentGroup.start; this.vertexCount++; if (this.vaoMax <= this.vertexCount) { this.vaoMax++; shader = this.shaders[1]; this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); // build the vao object that will render.. this.vaos[this.vertexCount] = this.renderer.createVao() .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[this.vertexCount], shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[this.vertexCount], shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) .addAttribute(this.vertexBuffers[this.vertexCount], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) .addAttribute(this.vertexBuffers[this.vertexCount], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } this.vertexBuffers[this.vertexCount].upload(buffer.vertices, 0); this.vao = this.vaos[this.vertexCount].bind(); // / render the groups.. for (i = 0; i < groupCount; i++) { const group = groups[i]; const groupTextureCount = group.textureCount; shader = this.shaders[groupTextureCount - 1]; if (!shader) { shader = this.shaders[groupTextureCount - 1] = generateMultiTextureShader(gl, groupTextureCount); // console.log("SHADER generated for " + textureCount + " textures") } this.renderer.bindShader(shader); for (let j = 0; j < groupTextureCount; j++) { this.renderer.bindTexture(group.textures[j], j); } // set the blend mode.. this.renderer.state.setBlendMode(group.blend); gl.drawElements(gl.TRIANGLES, group.size * 6, gl.UNSIGNED_SHORT, group.start * 6 * 2); } // reset elements for the next flush this.currentIndex = 0; } /** * Starts a new sprite batch. * */ start() { // this.renderer.bindShader(this.shader); // TICK %= 1000; } /** * Stops and flushes the current batch. * */ stop() { this.flush(); this.vao.unbind(); } /** * Destroys the SpriteBatch. * */ destroy() { for (let i = 0; i < this.vaoMax; i++) { this.vertexBuffers[i].destroy(); this.vaos[i].destroy(); } this.indexBuffer.destroy(); this.renderer.off('prerender', this.onPrerender, this); super.destroy(); for (let i = 0; i < this.shaders.length; i++) { if (this.shaders[i]) { this.shaders[i].destroy(); } } this.vertexBuffers = null; this.vaos = null; this.indexBuffer = null; this.indices = null; this.sprites = null; for (let i = 0; i < this.buffers.length; i++) { this.buffers[i].destroy(); } } } WebGLRenderer.registerPlugin('sprite', SpriteRenderer);