/**
* The core kami module provides basic 2D sprite batching and
* asset management.
*
* @module kami
*/
var Class = require('klasse');
var Mesh = require('./glutils/Mesh');
var colorToFloat = require('number-util').colorToFloat;
/**
* A batcher mixin composed of quads (two tris, indexed).
*
* This is used internally; users should look at
* {{#crossLink "SpriteBatch"}}{{/crossLink}} instead, which inherits from this
* class.
*
* The batcher itself is not managed by WebGLContext; however, it makes
* use of Mesh and Texture which will be managed. For this reason, the batcher
* does not hold a direct reference to the GL state.
*
* Subclasses must implement the following:
* {{#crossLink "BaseBatch/_createShader:method"}}{{/crossLink}}
* {{#crossLink "BaseBatch/_createVertexAttributes:method"}}{{/crossLink}}
* {{#crossLink "BaseBatch/getVertexSize:method"}}{{/crossLink}}
*
* @class BaseBatch
* @constructor
* @param {WebGLContext} context the context this batcher belongs to
* @param {Number} size the optional size of this batch, i.e. max number of quads
* @default 500
*/
var BaseBatch = new Class({
//Constructor
initialize: function BaseBatch(context, size) {
if (typeof context !== "object")
throw "GL context not specified to SpriteBatch";
this.context = context;
this.size = size || 500;
// 65535 is max index, so 65535 / 6 = 10922.
if (this.size > 10922) //(you'd have to be insane to try and batch this much with WebGL)
throw "Can't have more than 10922 sprites per batch: " + this.size;
this._blendSrc = this.context.gl.ONE;
this._blendDst = this.context.gl.ONE_MINUS_SRC_ALPHA
this._blendingEnabled = true;
this._shader = this._createShader();
/**
* This shader will be used whenever "null" is passed
* as the batch's shader.
*
* @property {ShaderProgram} shader
*/
this.defaultShader = this._shader;
/**
* By default, a SpriteBatch is created with its own ShaderProgram,
* stored in `defaultShader`. If this flag is true, on deleting the SpriteBatch, its
* `defaultShader` will also be deleted. If this flag is false, no shaders
* will be deleted on destroy.
*
* Note that if you re-assign `defaultShader`, you will need to dispose the previous
* default shader yoursel.
*
* @property ownsShader
* @type {Boolean}
*/
this.ownsShader = true;
this.idx = 0;
/**
* Whether we are currently drawing to the batch. Do not modify.
*
* @property {Boolean} drawing
*/
this.drawing = false;
this.mesh = this._createMesh(this.size);
/**
* The ABGR packed color, as a single float. The default
* value is the color white (255, 255, 255, 255).
*
* @property {Number} color
* @readOnly
*/
this.color = colorToFloat(255, 255, 255, 255);
/**
* Whether to premultiply alpha on calls to setColor.
* This is true by default, so that we can conveniently write:
*
* batch.setColor(1, 0, 0, 0.25); //tints red with 25% opacity
*
* If false, you must premultiply the colors yourself to achieve
* the same tint, like so:
*
* batch.setColor(0.25, 0, 0, 0.25);
*
* @property premultiplied
* @type {Boolean}
* @default true
*/
this.premultiplied = true;
},
/**
* A property to enable or disable blending for this sprite batch. If
* we are currently drawing, this will first flush the batch, and then
* update GL_BLEND state (enabled or disabled) with our new value.
*
* @property {Boolean} blendingEnabled
*/
blendingEnabled: {
set: function(val) {
var old = this._blendingEnabled;
if (this.drawing)
this.flush();
this._blendingEnabled = val;
//if we have a new value, update it.
//this is because blend is done in begin() / end()
if (this.drawing && old != val) {
var gl = this.context.gl;
if (val)
gl.enable(gl.BLEND);
else
gl.disable(gl.BLEND);
}
},
get: function() {
return this._blendingEnabled;
}
},
/**
* Sets the blend source parameters.
* If we are currently drawing, this will flush the batch.
*
* Setting either src or dst to `null` or a falsy value tells the SpriteBatch
* to ignore gl.blendFunc. This is useful if you wish to use your
* own blendFunc or blendFuncSeparate.
*
* @property {GLenum} blendDst
*/
blendSrc: {
set: function(val) {
if (this.drawing)
this.flush();
this._blendSrc = val;
},
get: function() {
return this._blendSrc;
}
},
/**
* Sets the blend destination parameters.
* If we are currently drawing, this will flush the batch.
*
* Setting either src or dst to `null` or a falsy value tells the SpriteBatch
* to ignore gl.blendFunc. This is useful if you wish to use your
* own blendFunc or blendFuncSeparate.
*
* @property {GLenum} blendSrc
*/
blendDst: {
set: function(val) {
if (this.drawing)
this.flush();
this._blendDst = val;
},
get: function() {
return this._blendDst;
}
},
/**
* Sets the blend source and destination parameters. This is
* a convenience function for the blendSrc and blendDst setters.
* If we are currently drawing, this will flush the batch.
*
* Setting either to `null` or a falsy value tells the SpriteBatch
* to ignore gl.blendFunc. This is useful if you wish to use your
* own blendFunc or blendFuncSeparate.
*
* @method setBlendFunction
* @param {GLenum} blendSrc the source blend parameter
* @param {GLenum} blendDst the destination blend parameter
*/
setBlendFunction: function(blendSrc, blendDst) {
this.blendSrc = blendSrc;
this.blendDst = blendDst;
},
/**
* This is a setter/getter for this batch's current ShaderProgram.
* If this is set when the batch is drawing, the state will be flushed
* to the GPU and the new shader will then be bound.
*
* If `null` or a falsy value is specified, the batch's `defaultShader` will be used.
*
* Note that shaders are bound on batch.begin().
*
* @property shader
* @type {ShaderProgram}
*/
shader: {
set: function(val) {
var wasDrawing = this.drawing;
if (wasDrawing) {
this.end(); //unbinds the shader from the mesh
}
this._shader = val ? val : this.defaultShader;
if (wasDrawing) {
this.begin();
}
},
get: function() {
return this._shader;
}
},
/**
* Sets the color of this sprite batcher, which is used in subsequent draw
* calls. This does not flush the batch.
*
* If r, g, b, are all numbers, this method assumes that RGB
* or RGBA float values (0.0 to 1.0) are being passed. Alpha defaults to one
* if undefined.
*
* If the first three arguments are not numbers, we only consider the first argument
* and assign it to all four components -- this is useful for setting transparency
* in a premultiplied alpha stage.
*
* If the first argument is invalid or not a number,
* the color defaults to (1, 1, 1, 1).
*
* @method setColor
* @param {Number} r the red component, normalized
* @param {Number} g the green component, normalized
* @param {Number} b the blue component, normalized
* @param {Number} a the alpha component, normalized
*/
setColor: function(r, g, b, a) {
var rnum = typeof r === "number";
if (rnum
&& typeof g === "number"
&& typeof b === "number") {
//default alpha to one
a = (a || a === 0) ? a : 1.0;
} else {
r = g = b = a = rnum ? r : 1.0;
}
if (this.premultiplied) {
r *= a;
g *= a;
b *= a;
}
this.color = colorToFloat(
~~(r * 255),
~~(g * 255),
~~(b * 255),
~~(a * 255)
);
},
/**
* Called from the constructor to create a new Mesh
* based on the expected batch size. Should set up
* verts & indices properly.
*
* Users should not call this directly; instead, it
* should only be implemented by subclasses.
*
* @method _createMesh
* @param {Number} size the size passed through the constructor
*/
_createMesh: function(size) {
//the total number of floats in our batch
var numVerts = size * 4 * this.getVertexSize();
//the total number of indices in our batch
var numIndices = size * 6;
var gl = this.context.gl;
//vertex data
this.vertices = new Float32Array(numVerts);
//index data
this.indices = new Uint16Array(numIndices);
for (var i=0, j=0; i < numIndices; i += 6, j += 4)
{
this.indices[i + 0] = j + 0;
this.indices[i + 1] = j + 1;
this.indices[i + 2] = j + 2;
this.indices[i + 3] = j + 0;
this.indices[i + 4] = j + 2;
this.indices[i + 5] = j + 3;
}
var mesh = new Mesh(this.context, false,
numVerts, numIndices, this._createVertexAttributes());
mesh.vertices = this.vertices;
mesh.indices = this.indices;
mesh.vertexUsage = gl.DYNAMIC_DRAW;
mesh.indexUsage = gl.STATIC_DRAW;
mesh.dirty = true;
return mesh;
},
/**
* Returns a shader for this batch. If you plan to support
* multiple instances of your batch, it may or may not be wise
* to use a shared shader to save resources.
*
* This method initially throws an error; so it must be overridden by
* subclasses of BaseBatch.
*
* @method _createShader
* @return {Number} the size of a vertex, in # of floats
*/
_createShader: function() {
throw "_createShader not implemented"
},
/**
* Returns an array of vertex attributes for this mesh;
* subclasses should implement this with the attributes
* expected for their batch.
*
* This method initially throws an error; so it must be overridden by
* subclasses of BaseBatch.
*
* @method _createVertexAttributes
* @return {Array} an array of Mesh.VertexAttrib objects
*/
_createVertexAttributes: function() {
throw "_createVertexAttributes not implemented";
},
/**
* Returns the number of floats per vertex for this batcher.
*
* This method initially throws an error; so it must be overridden by
* subclasses of BaseBatch.
*
* @method getVertexSize
* @return {Number} the size of a vertex, in # of floats
*/
getVertexSize: function() {
throw "getVertexSize not implemented";
},
/**
* Begins the sprite batch. This will bind the shader
* and mesh. Subclasses may want to disable depth or
* set up blending.
*
* @method begin
*/
begin: function() {
if (this.drawing)
throw "batch.end() must be called before begin";
this.drawing = true;
this.shader.bind();
//bind the attributes now to avoid redundant calls
this.mesh.bind(this.shader);
if (this._blendingEnabled) {
var gl = this.context.gl;
gl.enable(gl.BLEND);
}
},
/**
* Ends the sprite batch. This will flush any remaining
* data and set GL state back to normal.
*
* @method end
*/
end: function() {
if (!this.drawing)
throw "batch.begin() must be called before end";
if (this.idx > 0)
this.flush();
this.drawing = false;
this.mesh.unbind(this.shader);
if (this._blendingEnabled) {
var gl = this.context.gl;
gl.disable(gl.BLEND);
}
},
/**
* Called before rendering to bind new textures.
* This method does nothing by default.
*
* @method _preRender
*/
_preRender: function() {
},
/**
* Flushes the batch by pushing the current data
* to GL.
*
* @method flush
*/
flush: function() {
if (this.idx===0)
return;
var gl = this.context.gl;
//premultiplied alpha
if (this._blendingEnabled) {
//set either to null if you want to call your own
//blendFunc or blendFuncSeparate
if (this._blendSrc && this._blendDst)
gl.blendFunc(this._blendSrc, this._blendDst);
}
this._preRender();
//number of sprites in batch
var numComponents = this.getVertexSize();
var spriteCount = (this.idx / (numComponents * 4));
//draw the sprites
this.mesh.verticesDirty = true;
this.mesh.draw(gl.TRIANGLES, spriteCount * 6, 0, this.idx);
this.idx = 0;
},
/**
* Adds a sprite to this batch.
* The specifics depend on the sprite batch implementation.
*
* @method draw
* @param {Texture} texture the texture for this sprite
* @param {Number} x the x position, defaults to zero
* @param {Number} y the y position, defaults to zero
* @param {Number} width the width, defaults to the texture width
* @param {Number} height the height, defaults to the texture height
* @param {Number} u1 the first U coordinate, default zero
* @param {Number} v1 the first V coordinate, default zero
* @param {Number} u2 the second U coordinate, default one
* @param {Number} v2 the second V coordinate, default one
*/
draw: function(texture, x, y, width, height, u1, v1, u2, v2) {
},
/**
* Adds a single quad mesh to this sprite batch from the given
* array of vertices.
* The specifics depend on the sprite batch implementation.
*
* @method drawVertices
* @param {Texture} texture the texture we are drawing for this sprite
* @param {Float32Array} verts an array of vertices
* @param {Number} off the offset into the vertices array to read from
*/
drawVertices: function(texture, verts, off) {
},
drawRegion: function(region, x, y, width, height) {
this.draw(region.texture, x, y, width, height, region.u, region.v, region.u2, region.v2);
},
/**
* Destroys the batch, deleting its buffers and removing it from the
* WebGLContext management. Trying to use this
* batch after destroying it can lead to unpredictable behaviour.
*
* If `ownsShader` is true, this will also delete the `defaultShader` object.
*
* @method destroy
*/
destroy: function() {
this.vertices = null;
this.indices = null;
this.size = this.maxVertices = 0;
if (this.ownsShader && this.defaultShader)
this.defaultShader.destroy();
this.defaultShader = null;
this._shader = null; // remove reference to whatever shader is currently being used
if (this.mesh)
this.mesh.destroy();
this.mesh = null;
}
});
module.exports = BaseBatch;