shaku
Version:
A simple and effective JavaScript game development framework that knows its place!
599 lines (527 loc) • 20.7 kB
JavaScript
/**
* Implement the gfx shapes batch renderer.
*
* |-- copyright and license --|
* @module Shaku
* @file shaku\src\gfx\draw_batches\shapes_batch.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 Vector3 = require('../../utils/vector3');
const Matrix = require('../../utils/matrix.js');
const Vertex = require('../vertex');
const DrawBatch = require('./draw_batch');
const _logger = require('../../logger.js').getLogger('gfx-sprite-batch');
/**
* Colored shapes renderer.
* Responsible to drawing a batch of basic geometric shapes with as little draw calls as possible.
*/
class ShapesBatch extends DrawBatch
{
/**
* Create the sprites batch.
* @param {Number=} batchPolygonsCount Internal buffers size, in polygons count (polygon = 3 vertices). Bigger value = faster rendering but more RAM.
*/
constructor(batchPolygonsCount)
{
// init draw batch
super();
// create buffers for drawing shapes
this.#_createBuffers(batchPolygonsCount || 500);
/**
* How many polygons this batch can hold.
* @private
*/
this.__maxPolyCount = Math.floor((this._buffers.positionArray.length / 9));
/**
* How many polygons we currently have.
* @private
*/
this.__polyCount = 0;
/**
* Indicate there were changes in buffers.
* @private
*/
this.__dirty = false;
/**
* Optional method to trigger when shapes batch overflows and can't contain any more polygons.
* @type {Function}
* @name ShapesBatch#onOverflow
*/
this.onOverflow = null;
/**
* If true, will floor vertices positions before pushing them to batch.
* @type {Boolean}
* @name ShapesBatch#snapPixels
*/
this.snapPixels = false;
}
/**
* Get the gfx manager.
* @private
*/
get #_gfx()
{
return DrawBatch._gfx;
}
/**
* Get the web gl instance.
* @private
*/
get #_gl()
{
return DrawBatch._gfx._internal.gl;
}
/**
* Build the dynamic buffers.
* @private
*/
#_createBuffers(batchPolygonsCount)
{
let gl = this.#_gl;
// dynamic buffers, used for batch rendering
this._buffers = {
positionBuffer: gl.createBuffer(),
positionArray: new Float32Array(3 * 3 * batchPolygonsCount),
colorsBuffer: gl.createBuffer(),
colorsArray: new Float32Array(4 * 3 * batchPolygonsCount),
indexBuffer: gl.createBuffer(),
}
// create the indices buffer
let maxIndex = (batchPolygonsCount * 3);
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(batchPolygonsCount * 3); // 3 = number of indices per sprite
for (let i = 0; i < indices.length; i++) {
indices[i] = i;
}
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.colorsArray);
}
/**
* @inheritdoc
*/
clear()
{
super.clear();
this._buffers.positionArray._index = 0;
this._buffers.colorsArray._index = 0;
this.__polyCount = 0;
this.__dirty = false;
}
/**
* @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);
}
this._buffers = null;
}
/**
* @inheritdoc
*/
get isDestroyed()
{
return Boolean(this._buffers) === false;
}
/**
* @inheritdoc
*/
get defaultEffect()
{
return this.#_gfx.builtinEffects.Shapes;
}
/**
* Draw a line between two points.
* This method actually uses a rectangle internally, which is less efficient than using a proper LinesBatch, but have the advantage of supporting width.
* @param {Vector2} fromPoint Starting position.
* @param {Vector2} toPoint Ending position.
* @param {Color} color Line color.
* @param {Number=} width Line width.
*/
drawLine(fromPoint, toPoint, color, width)
{
width = width || 1;
length = fromPoint.distanceTo(toPoint);
let rotation = Vector2.radiansBetween(fromPoint, toPoint);
let position = (width > 1) ? (new Vector2(fromPoint.x, fromPoint.y - width / 2)) : fromPoint;
let size = new Vector2(length, width);
this.drawQuad(position, size, color, rotation, new Vector2(0, 0.5));
}
/**
* Push vertices to drawing batch.
* @param {Array<Vertex>} vertices Vertices to push. Vertices count must be dividable by 3 to keep the batch consistent of polygons.
*/
drawVertices(vertices)
{
// sanity
this.__validateDrawing(true);
// sanity check
if ((vertices.length % 3) !== 0) {
_logger.warn("Tried to push vertices that are not multiplication of 3!");
return;
}
// push vertices
let i = 0;
let colors = this._buffers.colorsArray;
let positions = this._buffers.positionArray;
for (let vertex of vertices)
{
// push color
if (this.__currDrawingParams.hasVertexColor) {
colors[colors._index++] = (vertex.color.r || 0);
colors[colors._index++] = (vertex.color.g || 0);
colors[colors._index++] = (vertex.color.b || 0);
colors[colors._index++] = (vertex.color.a || 0);
}
// push position
positions[positions._index++] = (vertex.position.x || 0);
positions[positions._index++] = (vertex.position.y || 0);
positions[positions._index++] = (vertex.position.z || 0);
// every 3 vertices..
if (i++ === 2)
{
// update quads count
this.__polyCount++;
// check if full
if (this.__polyCount >= this.__maxPolyCount) {
this._handleFullBuffer();
}
// reset count
i = 0;
}
}
// mark as dirty
this.__dirty = true;
}
/**
* Add a rectangle to draw.
* @param {Vector2|Vector3} position Drawing position (at origin). If vector3 is provided, will pass z value to the shader code position attribute.
* @param {Vector2|Vector3|Number} size Drawing size. If vector3 is provided, will pass z value to the shader code position attribute for the bottom vertices, as position.z + size.z.
* @param {Color|Array<Color>|undefined=} color Rectangle color, or undefined to not change color. If array is set, will assign each color to different vertex, starting from top-left.
* @param {Number=} rotation Rotate rectangle.
* @param {Vector2=} origin Drawing origin. This will be the point at 'position' and rotation origin.
* @param {Vector2=} skew Skew the drawing corners on X and Y axis, around the origin point.
*/
drawQuad(position, size, color, rotation, origin, skew)
{
let sprite = this.#_gfx.Sprite.build(null, position, size, undefined, color, rotation, origin, skew);
this.#_addRect(sprite);
}
/**
* Adds a 1x1 point.
* @param {Vector2|Vector3} position Point position.
* @param {Color} color Point color.
*/
addPoint(position, color)
{
this.drawVertices([new Vertex(position, null, color), new Vertex(position.add(2,0), null, color), new Vertex(position.add(0,2), null, color)]);
}
/**
* Add a rectangle that covers a given destination rectangle.
* @param {Rectangle|Vector2} destRect Destination rectangle to draw on. If vector is provided, will draw from 0,0 with vector as size.
* @param {Color|Array<Color>|undefined=} color Rectangle color, or undefined to not change color. If array is set, will assign each color to different vertex, starting from top-left.
* @param {Number=} rotation Rotate rectangle.
* @param {Vector2=} origin Drawing origin. This will be the point at 'position' and rotation origin.
*/
drawRectangle(destRect, color, rotation, origin)
{
if ((destRect.isVector2) || (destRect.isVector3)) {
destRect = new Rectangle(0, 0, destRect.x, destRect.y);
}
let position = origin ? destRect.getPosition().addSelf(size.mul(origin)) : destRect.getCenter();
origin = origin || Vector2.halfReadonly;
let size = destRect.getSize();
this.drawQuad(position, size, color, rotation, origin);
}
/**
* Draw a colored circle.
* @param {Circle} circle Circle to draw.
* @param {Color} color Circle fill color.
* @param {Number=} segmentsCount How many segments to build the circle from (more segments = smoother circle).
* @param {Color=} outsideColor If provided, will create a gradient-colored circle and this value will be the outter side color.
* @param {Number|Vector2=} ratio If procided, will scale the circle on X and Y axis to turn it into an oval. If a number is provided, will use this number to scale Y axis.
* @param {Number=} rotation If provided will rotate the oval / circle.
*/
drawCircle(circle, color, segmentsCount, outsideColor, ratio, rotation)
{
// sanity
this.__validateDrawing(true);
// defaults segments count
if (segmentsCount === undefined) {
segmentsCount = 24;
}
else if (segmentsCount < 2) {
return;
}
// default outside color
if (!outsideColor) {
outsideColor = color;
}
// default ratio
if (!ratio) {
ratio = Vector2.oneReadonly;
}
else if (typeof ratio === 'number') {
ratio = new Vector2(1, ratio);
}
// for rotation
let rotateVec;
if (rotation) {
let cos = Math.cos(rotation);
let sin = Math.sin(rotation);
rotateVec = function(vector)
{
let x = (vector.x * cos - vector.y * sin);
let y = (vector.x * sin + vector.y * cos);
vector.x = x;
vector.y = y;
return vector;
}
}
// build first position that is not center
const segmentStep = (2 * Math.PI) / segmentsCount;
let prevPoint = new Vector2(
(circle.radius * Math.cos(0)) * ratio.x,
(circle.radius * Math.sin(0)) * ratio.y
);
if (rotateVec) { rotateVec(prevPoint); }
// generate list of vertices to draw the circle
for (let i = 1; i <= segmentsCount; i++) {
let newPoint = new Vector2(
(circle.radius * Math.cos(i * segmentStep)) * ratio.x,
(circle.radius * Math.sin(i * segmentStep)) * ratio.y
);
if (rotateVec) { rotateVec(newPoint); }
this.drawVertices([
new Vertex(circle.center, null, color),
new Vertex(prevPoint.add(circle.center), null, outsideColor),
new Vertex(newPoint.add(circle.center), null, outsideColor),
]);
prevPoint = newPoint;
}
}
/**
* Add a rectangle from sprite data.
* @private
*/
#_addRect(sprite, transform)
{
// sanity
this.__validateDrawing(true);
// mark as dirty
this.__dirty = true;
// add rectangle from sprite data
{
// 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));
}
// snap pixels
if (this.snapPixels)
{
topLeft.floorSelf();
topRight.floorSelf();
bottomLeft.floorSelf();
bottomRight.floorSelf();
}
// add rectangle vertices
this.drawVertices([
new Vertex(topLeft, null, sprite.color),
new Vertex(topRight, null, sprite.color),
new Vertex(bottomLeft, null, sprite.color),
new Vertex(topRight, null, sprite.color),
new Vertex(bottomLeft, null, sprite.color),
new Vertex(bottomRight, null, sprite.color),
]);
}
}
/**
* Get how many polygons are currently in batch.
* @returns {Number} Polygons in batch count.
*/
get polygonsInBatch()
{
return this.__polyCount;
}
/**
* Get how many polygons this sprite batch can contain.
* @returns {Number} Max polygons count.
*/
get maxPolygonsCount()
{
return this.__maxPolyCount;
}
/**
* Check if this batch is full.
* @returns {Boolean} True if batch is full.
*/
get isFull()
{
return this.__polyCount >= this.__maxPolyCount;
}
/**
* 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 default effect
let effect = this.__currDrawingParams.effect;
// get some members
let gl = this.#_gl;
let gfx = this.#_gfx;
let positionArray = this._buffers.positionArray;
let colorsArray = this.__currDrawingParams.hasVertexColor ? this._buffers.colorsArray : null;
let positionBuffer = this._buffers.positionBuffer;
let colorsBuffer = this._buffers.colorsBuffer;
let indexBuffer = this._buffers.indexBuffer;
// should copy buffers
let needBuffersCopy = this.__dirty;
// calculate current batch quads count
let _currPolyCount = this.polygonsInBatch;
// nothing to draw? skip
if (_currPolyCount === 0) {
return;
}
// 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, _currPolyCount * 3 * 3);
}
// copy color buffer
if (this.__currDrawingParams.hasVertexColor && colorsBuffer) {
effect.setColorsAttribute(colorsBuffer, true);
if (needBuffersCopy && colorsArray) {
gl.bufferData(gl.ARRAY_BUFFER,
colorsArray,
this.__buffersUsage, 0, _currPolyCount * 3 * 4);
}
}
// set indices
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
// draw elements
gl.drawElements(gl.TRIANGLES, _currPolyCount * 3, this.__indicesType, 0);
gfx._internal.drawCallsCount++;
gfx._internal.drawShapePolygonsCount += _currPolyCount;
// mark as not dirty
this.__dirty = false;
// if static, free arrays we no longer need them
if (this.__staticBuffers) {
this._buffers.positionArray = 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 shapes batch class
module.exports = ShapesBatch;