shaku
Version:
A simple and effective JavaScript game development framework that knows its place!
626 lines (544 loc) • 22.8 kB
JavaScript
/**
* Implement the gfx sprite batch renderer base class.
*
* |-- copyright and license --|
* @module Shaku
* @file shaku\src\gfx\draw_batches\sprite_batch_base.js
* @author Ronen Ness (ronenness@gmail.com | http://ronenness.com)
* @copyright (c) 2021 Ronen Ness
* @license MIT
* |-- end copyright and license --|
*
*/
'use strict';
const { Rectangle } = require('../../utils');
const Vector2 = require('../../utils/vector2');
const Matrix = require('../../utils/matrix.js');
const DrawBatch = require('./draw_batch');
const _logger = require('../../logger.js').getLogger('gfx-sprite-batch');
/**
* Base class for sprite-based rendering, ie vertices with textures.
*/
class SpriteBatchBase extends DrawBatch
{
/**
* Create the sprites batch.
* @param {Number=} batchSpritesCount Internal buffers size, in sprites count (sprite = 4 vertices). Bigger value = faster rendering but more RAM.
* @param {Boolean=} enableVertexColor If true (default) will support vertex color.
* @param {Boolean=} enableNormals If true (not default) will support vertex normals.
*/
constructor(batchSpritesCount, enableVertexColor, enableNormals)
{
// init draw batch
super();
// create buffers for drawing sprites
this.#_createBuffers(batchSpritesCount || 500, enableVertexColor, Boolean(enableNormals));
/**
* How many quads this batch can hold.
* @private
*/
this.__maxQuadsCount = Math.floor((this._buffers.positionArray.length / 12));
/**
* How many quads we currently have.
* @private
*/
this.__quadsCount = 0;
/**
* Indicate there were changes in buffers.
* @private
*/
this.__dirty = false;
/**
* Optional method to trigger when sprite batch overflows and can't contain any more quads.
* @type {Function}
* @name SpriteBatch#onOverflow
*/
this.onOverflow = null;
/**
* Optional method to trigger right before drawing this batch.
* Receive params: effect, texture.
* @type {Function}
* @name SpriteBatch#beforeDraw
*/
this.beforeDraw = null;
/**
* If true, will floor vertices positions before pushing them to batch.
* @type {Boolean}
* @name SpriteBatch#snapPixels
*/
this.snapPixels = false;
/**
* If true, will cull quads that are not visible in screen when adding them by default.
* Note: will cull based on screen region during the time of adding sprite, not the time of actually rendering it.
* @type {Boolean}
* @name SpriteBatch#cullOutOfScreen
*/
this.cullOutOfScreen = false;
}
/**
* Get the gfx manager.
* @private
*/
get #_gfx()
{
return DrawBatch._gfx;
}
/**
* Get the web gl instance.
* @private
*/
get #_gl()
{
return DrawBatch._gfx._internal.gl;
}
/**
* @inheritdoc
*/
destroy()
{
let gl = this.#_gl;
if (this._buffers) {
if (this._buffers.positionBuffer) gl.deleteBuffer(this._buffers.positionBuffer);
if (this._buffers.colorsBuffer) gl.deleteBuffer(this._buffers.colorsBuffer);
if (this._buffers.textureCoordBuffer) gl.deleteBuffer(this._buffers.textureCoordBuffer);
if (this._buffers.normalsBuffer) gl.deleteBuffer(this._buffers.normalsBuffer);
}
this._buffers = null;
}
/**
* @inheritdoc
*/
get isDestroyed()
{
return Boolean(this._buffers) === false;
}
/**
* Build the dynamic buffers.
* @private
*/
#_createBuffers(batchSpritesCount, enableVertexColor, enableNormals)
{
let gl = this.#_gl;
// default enable vertex color
if (enableVertexColor === undefined) { enableVertexColor = true; }
// dynamic buffers, used for batch rendering
this._buffers = {
positionBuffer: gl.createBuffer(),
positionArray: new Float32Array(3 * 4 * batchSpritesCount),
textureCoordBuffer: gl.createBuffer(),
textureArray: new Float32Array(2 * 4 * batchSpritesCount),
colorsBuffer: enableVertexColor ? gl.createBuffer() : null,
colorsArray: enableVertexColor ? (new Float32Array(4 * 4 * batchSpritesCount)) : null,
normalsBuffer: enableNormals ? gl.createBuffer() : null,
normalsArray: enableNormals ? (new Float32Array(3 * 4 * batchSpritesCount)) : null,
indexBuffer: gl.createBuffer(),
}
// create the indices buffer
let maxIndex = (batchSpritesCount * 4);
let indicesArrayType;
if (maxIndex <= 256) {
indicesArrayType = Uint8Array;
this.__indicesType = gl.UNSIGNED_BYTE;
}
if (maxIndex <= 65535) {
indicesArrayType = Uint16Array;
this.__indicesType = gl.UNSIGNED_SHORT;
}
else {
indicesArrayType = Uint32Array;
this.__indicesType = gl.UNSIGNED_INT;
}
let indices = new indicesArrayType(batchSpritesCount * 6); // 6 = number of indices per sprite
let inc = 0;
for (let i = 0; i < indices.length; i += 6) {
indices[i] = inc;
indices[i+1] = inc + 1;
indices[i+2] = inc + 2;
indices[i+3] = inc + 1;
indices[i+4] = inc + 3;
indices[i+5] = inc + 2;
inc += 4;
}
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this._buffers.indexBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW);
// extand buffers functionality
function extendBuffer(buff)
{
if (buff) { buff._index = 0; }
}
extendBuffer(this._buffers.positionArray);
extendBuffer(this._buffers.textureArray);
extendBuffer(this._buffers.colorsArray);
extendBuffer(this._buffers.normalsArray);
}
/**
* @inheritdoc
*/
clear()
{
super.clear();
if (this._buffers.positionArray) { this._buffers.positionArray._index = 0; }
if (this._buffers.textureArray) { this._buffers.textureArray._index = 0; }
if (this._buffers.normalsArray) { this._buffers.normalsArray._index = 0; }
if (this._buffers.colorsArray && this.supportVertexColor) { this._buffers.colorsArray._index = 0; }
this.__quadsCount = 0;
this.__dirty = false;
}
/**
* Set a new active texture and draw batch if needed.
* @private
*/
_updateTexture(texture)
{
// if texture changed, draw current batch first
if (this.__currDrawingParams.texture && (this.__currDrawingParams.texture != texture)) {
this._drawBatch();
this.clear();
this.__dirty = true;
}
// set active texture
this.__currDrawingParams.texture = texture;
}
/**
* Get if this sprite batch support vertex color.
* @returns {Boolean} True if support vertex color.
*/
get supportVertexColor()
{
return Boolean(this._buffers.colorsBuffer);
}
/**
* @inheritdoc
*/
get defaultEffect()
{
return this.supportVertexColor ? this.#_gfx.builtinEffects.Sprites : this.#_gfx.builtinEffects.SpritesNoVertexColor;
}
/**
* Add sprite or sprites to batch.
* @param {Sprite|Array<Sprite>} sprites Sprite or multiple sprites to draw.
* @param {Matrix=} transform Optional transformations to apply on sprite vertices. Won't apply on static sprites.
* @param {Boolean=} cullOutOfScreen If true, will cull sprites that are not visible in currently set rendering region.
*/
drawSprite(sprites, transform, cullOutOfScreen)
{
// sanity
this.__validateDrawing(true);
// make sure array
if (!Array.isArray(sprites)) {
sprites = [sprites];
}
// mark as dirty
this.__dirty = true;
// get colors and uvs array
let colors = this._buffers.colorsArray;
let uvs = this._buffers.textureArray;
// get screen region for culling
const screenRegion = (cullOutOfScreen || (this.cullOutOfScreen && (cullOutOfScreen === undefined))) ? this.#_gfx._internal.getRenderingRegionInternal() : null;
// add all sprites
for (let sprite of sprites) {
// update texture
this._updateTexture(sprite.texture);
// update quads count
this.__quadsCount++;
// set colors
if (colors && this.__currDrawingParams.hasVertexColor) {
// array of colors
if (Array.isArray(sprite.color)) {
let lastColor = sprite.color[0];
for (let x = 0; x < 4; ++x) {
let curr = (sprite.color[x] || lastColor);
colors[colors._index++] = curr.r;
colors[colors._index++] = curr.g;
colors[colors._index++] = curr.b;
colors[colors._index++] = curr.a;
lastColor = curr;
}
}
// single color
else {
let curr = sprite.color;
for (let x = 0; x < 4; ++x) {
colors[colors._index++] = curr.r;
colors[colors._index++] = curr.g;
colors[colors._index++] = curr.b;
colors[colors._index++] = curr.a;
}
}
}
// get source rectangle
let sourceRect = sprite.sourceRectangle;
let textureSourceRect = sprite.texture.sourceRectangle;
// if got source rectangle, set it
if (sourceRect)
{
textureSourceRect = textureSourceRect || {x:0, y:0, width:0, height:0};
let twidth = sprite.texture.width;
let theight = sprite.texture.height;
let left = (sourceRect.left + textureSourceRect.x) / twidth;
let right = (sourceRect.right + textureSourceRect.x) / twidth;
let top = (sourceRect.top + textureSourceRect.y) / theight;
let bottom = (sourceRect.bottom + textureSourceRect.y) / theight;
uvs[uvs._index++] = (left); uvs[uvs._index++] = (top);
uvs[uvs._index++] = (right); uvs[uvs._index++] = (top);
uvs[uvs._index++] = (left); uvs[uvs._index++] = (bottom);
uvs[uvs._index++] = (right); uvs[uvs._index++] = (bottom);
}
// if got source rectangle from texture (texture atlas without source rect), set it
else if (textureSourceRect)
{
let normalized = sprite.texture.sourceRectangleNormalized;
let twidth = sprite.texture.width;
let theight = sprite.texture.height;
let left = normalized.left || (textureSourceRect.left) / twidth;
let right = normalized.right || (textureSourceRect.right) / twidth;
let top = normalized.top || (textureSourceRect.top) / theight;
let bottom = normalized.bottom || (textureSourceRect.bottom) / theight;
uvs[uvs._index++] = (left); uvs[uvs._index++] = (top);
uvs[uvs._index++] = (right); uvs[uvs._index++] = (top);
uvs[uvs._index++] = (left); uvs[uvs._index++] = (bottom);
uvs[uvs._index++] = (right); uvs[uvs._index++] = (bottom);
}
// if got no source rectangle, take entire texture
else
{
uvs[uvs._index++] = 0; uvs[uvs._index++] = 0;
uvs[uvs._index++] = 1; uvs[uvs._index++] = 0;
uvs[uvs._index++] = 0; uvs[uvs._index++] = 1;
uvs[uvs._index++] = 1; uvs[uvs._index++] = 1;
}
// calculate vertices positions
let sizeX = sprite.size.x;
let sizeY = sprite.size.y;
let left = -sizeX * sprite.origin.x;
let top = -sizeY * sprite.origin.y;
// calculate corners
topLeft.x = left; topLeft.y = top;
topRight.x = left + sizeX; topRight.y = top;
bottomLeft.x = left; bottomLeft.y = top + sizeY;
bottomRight.x = left + sizeX; bottomRight.y = top + sizeY;
// are vertices axis aligned?
let axisAlined = true;
// apply skew
if (sprite.skew)
{
// skew on x axis
if (sprite.skew.x) {
topLeft.x += sprite.skew.x * sprite.origin.y;
topRight.x += sprite.skew.x * sprite.origin.y;
bottomLeft.x -= sprite.skew.x * (1 - sprite.origin.y);
bottomRight.x -= sprite.skew.x * (1 - sprite.origin.y);
axisAlined = false;
}
// skew on y axis
if (sprite.skew.y) {
topLeft.y += sprite.skew.y * sprite.origin.x;
bottomLeft.y += sprite.skew.y * sprite.origin.x;
topRight.y -= sprite.skew.y * (1 - sprite.origin.x);
bottomRight.y -= sprite.skew.y * (1 - sprite.origin.x);
axisAlined = false;
}
}
// apply rotation
if (sprite.rotation) {
let cos = Math.cos(sprite.rotation);
let sin = Math.sin(sprite.rotation);
function rotateVec(vector)
{
let x = (vector.x * cos - vector.y * sin);
let y = (vector.x * sin + vector.y * cos);
vector.x = x;
vector.y = y;
}
rotateVec(topLeft);
rotateVec(topRight);
rotateVec(bottomLeft);
rotateVec(bottomRight);
axisAlined = false;
}
// add sprite position
topLeft.x += sprite.position.x;
topLeft.y += sprite.position.y;
topRight.x += sprite.position.x;
topRight.y += sprite.position.y;
bottomLeft.x += sprite.position.x;
bottomLeft.y += sprite.position.y;
bottomRight.x += sprite.position.x;
bottomRight.y += sprite.position.y;
// apply transform
if (transform && !transform.isIdentity) {
topLeft.copy((topLeft.z !== undefined) ? Matrix.transformVector3(transform, topLeft) : Matrix.transformVector2(transform, topLeft));
topRight.copy((topRight.z !== undefined) ? Matrix.transformVector3(transform, topRight) : Matrix.transformVector2(transform, topRight));
bottomLeft.copy((bottomLeft.z !== undefined) ? Matrix.transformVector3(transform, bottomLeft) : Matrix.transformVector2(transform, bottomLeft));
bottomRight.copy((bottomRight.z !== undefined) ? Matrix.transformVector3(transform, bottomRight) : Matrix.transformVector2(transform, bottomRight));
}
// cull out-of-screen sprites
if (screenRegion)
{
let destRect = axisAlined ?
new Rectangle(topLeft.x, topLeft.y, bottomRight.x - topLeft.x, bottomRight.y - topLeft.y) :
Rectangle.fromPoints([topLeft, topRight, bottomLeft, bottomRight]);
if (!screenRegion.collideRect(destRect)) {
return;
}
}
// snap pixels
if (this.snapPixels)
{
topLeft.floorSelf();
topRight.floorSelf();
bottomLeft.floorSelf();
bottomRight.floorSelf();
}
// optional z position
let z = sprite.position.z || 0;
let zDepth = z + sprite.size.z || 0;
// update positions buffer
let positions = this._buffers.positionArray;
positions[positions._index++] = topLeft.x; positions[positions._index++] = topLeft.y; positions[positions._index++] = z;
positions[positions._index++] = topRight.x; positions[positions._index++] = topRight.y; positions[positions._index++] = z;
positions[positions._index++] = bottomLeft.x; positions[positions._index++] = bottomLeft.y; positions[positions._index++] = zDepth;
positions[positions._index++] = bottomRight.x; positions[positions._index++] = bottomRight.y; positions[positions._index++] = zDepth;
// update normals buffer
let normals = this._buffers.normalsArray;
if (normals) {
normals[normals._index++] = 0;
normals[normals._index++] = 0;
normals[normals._index++] = 1;
}
// check if full
if (this.__quadsCount >= this.__maxQuadsCount) {
this._handleFullBuffer();
}
}
}
/**
* Get how many quads are currently in batch.
* @returns {Number} Quads in batch count.
*/
get quadsInBatch()
{
return this.__quadsCount;
}
/**
* Get how many quads this sprite batch can contain.
* @returns {Number} Max quads count.
*/
get maxQuadsCount()
{
return this.__maxQuadsCount;
}
/**
* Check if this batch is full.
* @returns {Boolean} True if batch is full.
*/
get isFull()
{
return this.__quadsCount >= this.__maxQuadsCount;
}
/**
* Called when the batch becomes full while drawing and there's no handler.
* @private
*/
_handleFullBuffer()
{
// invoke on-overflow callback
if (this.onOverflow) {
this.onOverflow();
}
// draw current batch and clear
this._drawBatch();
this.clear();
}
/**
* @inheritdoc
* @private
*/
_drawBatch()
{
// get texture and effect
let texture = this.__currDrawingParams.texture;
let effect = this.__currDrawingParams.effect;
// texture not loaded yet? skip
if (!texture || !texture.valid) {
return;
}
// should copy buffers
let needBuffersCopy = this.__dirty;
// calculate current batch quads count
let _currBatchCount = this.quadsInBatch;
// nothing to draw? skip
if (_currBatchCount === 0) {
return;
}
// call the before-draw callback
if (this.beforeDraw) {
this.beforeDraw(effect, texture);
}
// get some fields we'll need
let gl = this.#_gl;
let gfx = this.#_gfx;
let positionArray = this._buffers.positionArray;
let textureArray = this._buffers.textureArray;
let colorsArray = this.__currDrawingParams.hasVertexColor ? this._buffers.colorsArray : null;
let normalsArray = this._buffers.normalsArray;
let positionBuffer = this._buffers.positionBuffer;
let textureCoordBuffer = this._buffers.textureCoordBuffer;
let colorsBuffer = this._buffers.colorsBuffer;
let normalsBuffer = this._buffers.normalsBuffer;
let indexBuffer = this._buffers.indexBuffer;
// call base method to set effect and draw params
super._drawBatch();
// copy position buffer
effect.setPositionsAttribute(positionBuffer, true);
if (needBuffersCopy) {
gl.bufferData(gl.ARRAY_BUFFER,
positionArray,
this.__buffersUsage, 0, _currBatchCount * 4 * 3);
}
// copy texture buffer
effect.setTextureCoordsAttribute(textureCoordBuffer, true);
if (needBuffersCopy) {
gl.bufferData(gl.ARRAY_BUFFER,
textureArray,
this.__buffersUsage, 0, _currBatchCount * 4 * 2);
}
// copy color buffer
if (this.__currDrawingParams.hasVertexColor && colorsBuffer) {
effect.setColorsAttribute(colorsBuffer, true);
if (needBuffersCopy && colorsArray) {
gl.bufferData(gl.ARRAY_BUFFER,
colorsArray,
this.__buffersUsage, 0, _currBatchCount * 4 * 4);
}
}
// copy normals buffer
if (normalsBuffer) {
effect.setNormalsAttribute(normalsBuffer, true);
if (needBuffersCopy && normalsArray) {
gl.bufferData(gl.ARRAY_BUFFER,
normalsArray,
this.__buffersUsage, 0, _currBatchCount * 4 * 3);
}
}
// set indices
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
// draw elements
gl.drawElements(gl.TRIANGLES, _currBatchCount * 6, this.__indicesType, 0);
gfx._internal.drawCallsCount++;
gfx._internal.drawQuadsCount += _currBatchCount;
// mark as not dirty
this.__dirty = false;
// if static, free arrays we no longer need them
if (this.__staticBuffers) {
this._buffers.positionArray = this._buffers.textureArray = this._buffers.colorsArray = null;
}
}
}
// used for vertices calculations
const topLeft = new Vector2(0, 0);
const topRight = new Vector2(0, 0);
const bottomLeft = new Vector2(0, 0);
const bottomRight = new Vector2(0, 0);
// export the sprite batch base class
module.exports = SpriteBatchBase;