shaku
Version:
A simple and effective JavaScript game development framework that knows its place!
604 lines (530 loc) • 20.6 kB
JavaScript
/**
* Implement the gfx lines batch renderer.
*
* |-- copyright and license --|
* @module Shaku
* @file shaku\src\gfx\draw_batches\lines_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 lines renderer.
* Responsible to drawing a batch of line segments or strips.
*/
class LinesBatch extends DrawBatch
{
/**
* Create the sprites batch.
* @param {Number=} lineSegmentsCount Internal buffers size, in line segments count (line segment = 3 vertices). Bigger value = faster rendering but more RAM.
*/
constructor(lineSegmentsCount)
{
// init draw batch
super();
// create buffers for drawing shapes
this.#_createBuffers(lineSegmentsCount || 500);
/**
* How many line segments this batch can hold.
* @private
*/
this.__maxLinesCount = Math.floor((this._buffers.positionArray.length / 6));
/**
* How many line segments we currently have.
* @private
*/
this.__linesCount = 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;
/**
* If true, will draw lines as a single lines strip.
* @type {Boolean}
* @name ShapesBatch#linesStrip
*/
this.linesStrip = 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 * 2 * batchPolygonsCount),
colorsBuffer: gl.createBuffer(),
colorsArray: new Float32Array(4 * 2 * 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.__linesCount = 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;
}
/**
* 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 (!this.linesStrip && ((vertices.length % 2) !== 0)) {
_logger.warn("Tried to push vertices that are not multiplication of 2!");
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 2 vertices..
if ((i++ === 1) || this.linesStrip)
{
// update quads count
this.__linesCount++;
// check if full
if (this.__linesCount >= this.__maxLinesCount) {
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);
}
/**
* 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 {Rectangle=} sourceRect Source rectangle, or undefined to use the entire texture.
* @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, sourceRect, color, rotation, origin)
{
if ((destRect.isVector2) || (destRect.isVector3)) {
destRect = new Rectangle(0, 0, destRect.x, destRect.y);
}
this.drawQuad(destRect.getCenter(), destRect.getSize(), sourceRect, 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 {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, ratio, rotation)
{
// sanity
this.__validateDrawing(true);
// defaults segments count
if (segmentsCount === undefined) {
segmentsCount = 24;
}
else if (segmentsCount < 2) {
return;
}
// 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++)
{
// calculate new point
let newPoint = new Vector2(
(circle.radius * Math.cos(i * segmentStep)) * ratio.x,
(circle.radius * Math.sin(i * segmentStep)) * ratio.y
);
if (rotateVec) { rotateVec(newPoint); }
// add for line strip
if (this.linesStrip) {
if (i === 1) {
this.drawVertices([
new Vertex(prevPoint.add(circle.center), null, color)
]);
}
this.drawVertices([
new Vertex(newPoint.add(circle.center), null, color)
]);
}
// add for line segments
else {
this.drawVertices([
new Vertex(prevPoint.add(circle.center), null, color),
new Vertex(newPoint.add(circle.center), null, color),
]);
}
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
if (this.linesStrip) {
this.drawVertices([
new Vertex(topLeft, null, sprite.color),
new Vertex(topRight, null, sprite.color),
new Vertex(bottomRight, null, sprite.color),
new Vertex(bottomLeft, null, sprite.color),
new Vertex(topLeft, null, sprite.color),
]);
}
else {
this.drawVertices([
new Vertex(topLeft, null, sprite.color),
new Vertex(topRight, null, sprite.color),
new Vertex(topRight, null, sprite.color),
new Vertex(bottomRight, null, sprite.color),
new Vertex(bottomRight, null, sprite.color),
new Vertex(bottomLeft, null, sprite.color),
new Vertex(bottomLeft, null, sprite.color),
new Vertex(topLeft, null, sprite.color),
]);
}
}
}
/**
* Get how many line segments are currently in batch.
* @returns {Number} Line segments in batch count.
*/
get linesInBatch()
{
return this.__linesCount;
}
/**
* Get how many line segments this batch can contain.
* @returns {Number} Max line segments count.
*/
get maxLinesCount()
{
return this.__maxLinesCount;
}
/**
* Check if this batch is full.
* @returns {Boolean} True if batch is full.
*/
get isFull()
{
return this.__linesCount >= this.__maxLinesCount;
}
/**
* 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;
// nothing to draw? skip
if (positionArray._index <= 1) {
return;
}
// get default effect
effect = effect || this.defaultEffect;
// call base method to set effect and draw params
super._drawBatch();
// vertices count
const verticesCount = positionArray._index / 3;
// copy position buffer
effect.setPositionsAttribute(positionBuffer, true);
if (needBuffersCopy) {
gl.bufferData(gl.ARRAY_BUFFER,
positionArray,
this.__buffersUsage, 0, positionArray._index);
}
// copy color buffer
if (this.__currDrawingParams.hasVertexColor && colorsBuffer) {
effect.setColorsAttribute(colorsBuffer, true);
if (needBuffersCopy && colorsArray) {
gl.bufferData(gl.ARRAY_BUFFER,
colorsArray,
this.__buffersUsage, 0, colorsArray._index);
}
}
// set indices
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
// draw elements
gl.drawElements((this.linesStrip ? gl.LINE_STRIP : gl.LINES), verticesCount, this.__indicesType, 0);
gfx._internal.drawCallsCount++;
gfx._internal.drawShapePolygonsCount += verticesCount / 2;
// 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 = LinesBatch;