/**
* @module kami
*/
var Class = require('klasse');
var Signal = require('signals');
var nextPowerOfTwo = require('number-util').nextPowerOfTwo;
var isPowerOfTwo = require('number-util').isPowerOfTwo;
var Texture = new Class({
/**
* Creates a new texture with the optional width, height, and data.
*
* If the constructor is passed no parameters other than WebGLContext, then
* it will not be initialized and will be non-renderable. You will need to manually
* uploadData or uploadImage yourself.
*
* If you pass a width and height after context, the texture will be initialized with that size
* and null data (e.g. transparent black). If you also pass the format and data,
* it will be uploaded to the texture.
*
* If you pass a String or Data URI as the second parameter,
* this Texture will load an Image object asynchronously. The optional third
* and fourth parameters are callback functions for success and failure, respectively.
* The optional fifrth parameter for this version of the constructor is genMipmaps, which defaults to false.
*
* The arguments are kept in memory for future context restoration events. If
* this is undesirable (e.g. huge buffers which need to be GC'd), you should not
* pass the data in the constructor, but instead upload it after creating an uninitialized
* texture. You will need to manage it yourself, either by extending the create() method,
* or listening to restored events in WebGLContext.
*
* Most users will want to use the AssetManager to create and manage their textures
* with asynchronous loading and context loss.
*
* @example
* new Texture(context, 256, 256); //empty 256x256 texture
* new Texture(context, 1, 1, Texture.Format.RGBA, Texture.DataType.UNSIGNED_BYTE,
* new Uint8Array([255,0,0,255])); //1x1 red texture
* new Texture(context, "test.png"); //loads image asynchronously
* new Texture(context, "test.png", successFunc, failFunc, useMipmaps); //extra params for image laoder
*
* @class Texture
* @constructor
* @param {WebGLContext} context the WebGL context
* @param {Number} width the width of this texture
* @param {Number} height the height of this texture
* @param {GLenum} format e.g. Texture.Format.RGBA
* @param {GLenum} dataType e.g. Texture.DataType.UNSIGNED_BYTE (Uint8Array)
* @param {GLenum} data the array buffer, e.g. a Uint8Array view
* @param {Boolean} genMipmaps whether to generate mipmaps after uploading the data
*/
initialize: function Texture(context, width, height, format, dataType, data, genMipmaps) {
if (typeof context !== "object")
throw "GL context not specified to Texture";
this.context = context;
/**
* The WebGLTexture which backs this Texture object. This
* can be used for low-level GL calls.
*
* @type {WebGLTexture}
*/
this.id = null; //initialized in create()
/**
* The target for this texture unit, i.e. TEXTURE_2D. Subclasses
* should override the create() method to change this, for correct
* usage with context restore.
*
* @property target
* @type {GLenum}
* @default gl.TEXTURE_2D
*/
this.target = context.gl.TEXTURE_2D;
/**
* The width of this texture, in pixels.
*
* @property width
* @readOnly
* @type {Number} the width
*/
this.width = 0; //initialized on texture upload
/**
* The height of this texture, in pixels.
*
* @property height
* @readOnly
* @type {Number} the height
*/
this.height = 0; //initialized on texture upload
// e.g. --> new Texture(gl, 256, 256, gl.RGB, gl.UNSIGNED_BYTE, data);
// creates a new empty texture, 256x256
// --> new Texture(gl);
// creates a new texture but WITHOUT uploading any data.
/**
* The S wrap parameter.
* @property {GLenum} wrapS
*/
this.wrapS = Texture.DEFAULT_WRAP;
/**
* The T wrap parameter.
* @property {GLenum} wrapT
*/
this.wrapT = Texture.DEFAULT_WRAP;
/**
* The minifcation filter.
* @property {GLenum} minFilter
*/
this.minFilter = Texture.DEFAULT_FILTER;
/**
* The magnification filter.
* @property {GLenum} magFilter
*/
this.magFilter = Texture.DEFAULT_FILTER;
/**
* When a texture is created, we keep track of the arguments provided to
* its constructor. On context loss and restore, these arguments are re-supplied
* to the Texture, so as to re-create it in its correct form.
*
* This is mainly useful if you are procedurally creating textures and passing
* their data directly (e.g. for generic lookup tables in a shader). For image
* or media based textures, it would be better to use an AssetManager to manage
* the asynchronous texture upload.
*
* Upon destroying a texture, a reference to this is also lost.
*
* @property managedArgs
* @type {Array} the array of arguments, shifted to exclude the WebGLContext parameter
*/
this.managedArgs = Array.prototype.slice.call(arguments, 1);
//This is maanged by WebGLContext
this.context.addManagedObject(this);
this.create();
},
/**
* This can be called after creating a Texture to load an Image object asynchronously,
* or upload image data directly. It takes the same parameters as the constructor, except
* for the context which has already been established.
*
* Users will generally not need to call this directly.
*
* @protected
* @method setup
*/
setup: function(width, height, format, dataType, data, genMipmaps) {
var gl = this.gl;
//If the first argument is a string, assume it's an Image loader
//second argument will then be genMipmaps, third and fourth the success/fail callbacks
if (typeof width === "string") {
var img = new Image();
var path = arguments[0]; //first argument, the path
var successCB = typeof arguments[1] === "function" ? arguments[1] : null;
var failCB = typeof arguments[2] === "function" ? arguments[2] : null;
genMipmaps = !!arguments[3];
var self = this;
//If you try to render a texture that is not yet "renderable" (i.e. the
//async load hasn't completed yet, which is always the case in Chrome since requestAnimationFrame
//fires before img.onload), WebGL will throw us errors. So instead we will just upload some
//dummy data until the texture load is complete. Users can disable this with the global flag.
if (Texture.USE_DUMMY_1x1_DATA) {
self.uploadData(1, 1);
this.width = this.height = 0;
}
img.onload = function() {
self.uploadImage(img, undefined, undefined, genMipmaps);
if (successCB)
successCB();
}
img.onerror = function() {
// console.warn("Error loading image: "+path);
if (genMipmaps) //we still need to gen mipmaps on the 1x1 dummy
gl.generateMipmap(gl.TEXTURE_2D);
if (failCB)
failCB();
}
img.onabort = function() {
// console.warn("Image load aborted: "+path);
if (genMipmaps) //we still need to gen mipmaps on the 1x1 dummy
gl.generateMipmap(gl.TEXTURE_2D);
if (failCB)
failCB();
}
img.src = path;
}
//otherwise assume our regular list of width/height arguments are passed
else {
this.uploadData(width, height, format, dataType, data, genMipmaps);
}
},
/**
* Called in the Texture constructor, and after the GL context has been re-initialized.
* Subclasses can override this to provide a custom data upload, e.g. cubemaps or compressed
* textures.
*
* @method create
*/
create: function() {
this.gl = this.context.gl;
var gl = this.gl;
this.id = gl.createTexture(); //texture ID is recreated
this.width = this.height = 0; //size is reset to zero until loaded
this.target = gl.TEXTURE_2D; //the provider can change this if necessary (e.g. cube maps)
this.bind();
//TODO: clean these up a little.
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, Texture.UNPACK_PREMULTIPLY_ALPHA);
gl.pixelStorei(gl.UNPACK_ALIGNMENT, Texture.UNPACK_ALIGNMENT);
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, Texture.UNPACK_FLIP_Y);
var colorspace = Texture.UNPACK_COLORSPACE_CONVERSION || gl.BROWSER_DEFAULT_WEBGL;
gl.pixelStorei(gl.UNPACK_COLORSPACE_CONVERSION_WEBGL, colorspace);
//setup wrap modes without binding redundantly
this.setWrap(this.wrapS, this.wrapT, false);
this.setFilter(this.minFilter, this.magFilter, false);
if (this.managedArgs.length !== 0) {
this.setup.apply(this, this.managedArgs);
}
},
/**
* Destroys this texture by deleting the GL resource,
* removing it from the WebGLContext management stack,
* setting its size to zero, and id and managed arguments to null.
*
* Trying to use this texture after may lead to undefined behaviour.
*
* @method destroy
*/
destroy: function() {
if (this.id && this.gl)
this.gl.deleteTexture(this.id);
if (this.context)
this.context.removeManagedObject(this);
this.width = this.height = 0;
this.id = null;
this.managedArgs = null;
this.context = null;
this.gl = null;
},
/**
* Sets the wrap mode for this texture; if the second argument
* is undefined or falsy, then both S and T wrap will use the first
* argument.
*
* You can use Texture.Wrap constants for convenience, to avoid needing
* a GL reference.
*
* @method setWrap
* @param {GLenum} s the S wrap mode
* @param {GLenum} t the T wrap mode
* @param {Boolean} ignoreBind (optional) if true, the bind will be ignored.
*/
setWrap: function(s, t, ignoreBind) { //TODO: support R wrap mode
if (s && t) {
this.wrapS = s;
this.wrapT = t;
} else
this.wrapS = this.wrapT = s;
//enforce POT rules..
this._checkPOT();
if (!ignoreBind)
this.bind();
var gl = this.gl;
gl.texParameteri(this.target, gl.TEXTURE_WRAP_S, this.wrapS);
gl.texParameteri(this.target, gl.TEXTURE_WRAP_T, this.wrapT);
},
/**
* Sets the min and mag filter for this texture;
* if mag is undefined or falsy, then both min and mag will use the
* filter specified for min.
*
* You can use Texture.Filter constants for convenience, to avoid needing
* a GL reference.
*
* @method setFilter
* @param {GLenum} min the minification filter
* @param {GLenum} mag the magnification filter
* @param {Boolean} ignoreBind if true, the bind will be ignored.
*/
setFilter: function(min, mag, ignoreBind) {
if (min && mag) {
this.minFilter = min;
this.magFilter = mag;
} else
this.minFilter = this.magFilter = min;
//enforce POT rules..
this._checkPOT();
if (!ignoreBind)
this.bind();
var gl = this.gl;
gl.texParameteri(this.target, gl.TEXTURE_MIN_FILTER, this.minFilter);
gl.texParameteri(this.target, gl.TEXTURE_MAG_FILTER, this.magFilter);
},
/**
* A low-level method to upload the specified ArrayBufferView
* to this texture. This will cause the width and height of this
* texture to change.
*
* @method uploadData
* @param {Number} width the new width of this texture,
* defaults to the last used width (or zero)
* @param {Number} height the new height of this texture
* defaults to the last used height (or zero)
* @param {GLenum} format the data format, default RGBA
* @param {GLenum} type the data type, default UNSIGNED_BYTE (Uint8Array)
* @param {ArrayBufferView} data the raw data for this texture, or null for an empty image
* @param {Boolean} genMipmaps whether to generate mipmaps after uploading the data, default false
*/
uploadData: function(width, height, format, type, data, genMipmaps) {
var gl = this.gl;
format = format || gl.RGBA;
type = type || gl.UNSIGNED_BYTE;
data = data || null; //make sure falsey value is null for texImage2D
this.width = (width || width==0) ? width : this.width;
this.height = (height || height==0) ? height : this.height;
this._checkPOT();
this.bind();
gl.texImage2D(this.target, 0, format,
this.width, this.height, 0, format,
type, data);
if (genMipmaps)
gl.generateMipmap(this.target);
},
/**
* Uploads ImageData, HTMLImageElement, HTMLCanvasElement or
* HTMLVideoElement.
*
* @method uploadImage
* @param {Object} domObject the DOM image container
* @param {GLenum} format the format, default gl.RGBA
* @param {GLenum} type the data type, default gl.UNSIGNED_BYTE
* @param {Boolean} genMipmaps whether to generate mipmaps after uploading the data, default false
*/
uploadImage: function(domObject, format, type, genMipmaps) {
var gl = this.gl;
format = format || gl.RGBA;
type = type || gl.UNSIGNED_BYTE;
this.width = domObject.width;
this.height = domObject.height;
this._checkPOT();
this.bind();
gl.texImage2D(this.target, 0, format, format,
type, domObject);
if (genMipmaps)
gl.generateMipmap(this.target);
},
/**
* If FORCE_POT is false, we verify this texture to see if it is valid,
* as per non-power-of-two rules. If it is non-power-of-two, it must have
* a wrap mode of CLAMP_TO_EDGE, and the minification filter must be LINEAR
* or NEAREST. If we don't satisfy these needs, an error is thrown.
*
* @method _checkPOT
* @private
* @return {[type]} [description]
*/
_checkPOT: function() {
if (!Texture.FORCE_POT) {
//If minFilter is anything but LINEAR or NEAREST
//or if wrapS or wrapT are not CLAMP_TO_EDGE...
var wrongFilter = (this.minFilter !== Texture.Filter.LINEAR && this.minFilter !== Texture.Filter.NEAREST);
var wrongWrap = (this.wrapS !== Texture.Wrap.CLAMP_TO_EDGE || this.wrapT !== Texture.Wrap.CLAMP_TO_EDGE);
if ( wrongFilter || wrongWrap ) {
if (!isPowerOfTwo(this.width) || !isPowerOfTwo(this.height))
throw new Error(wrongFilter
? "Non-power-of-two textures cannot use mipmapping as filter"
: "Non-power-of-two textures must use CLAMP_TO_EDGE as wrap");
}
}
},
/**
* Binds the texture. If unit is specified,
* it will bind the texture at the given slot
* (TEXTURE0, TEXTURE1, etc). If unit is not specified,
* it will simply bind the texture at whichever slot
* is currently active.
*
* @method bind
* @param {Number} unit the texture unit index, starting at 0
*/
bind: function(unit) {
var gl = this.gl;
if (unit || unit === 0)
gl.activeTexture(gl.TEXTURE0 + unit);
gl.bindTexture(this.target, this.id);
},
toString: function() {
return this.id + ":" + this.width + "x" + this.height + "";
}
});
/**
* A set of Filter constants that match their GL counterparts.
* This is for convenience, to avoid the need for a GL rendering context.
*
* @example
* ```
* Texture.Filter.NEAREST
* Texture.Filter.NEAREST_MIPMAP_LINEAR
* Texture.Filter.NEAREST_MIPMAP_NEAREST
* Texture.Filter.LINEAR
* Texture.Filter.LINEAR_MIPMAP_LINEAR
* Texture.Filter.LINEAR_MIPMAP_NEAREST
* ```
* @attribute Filter
* @static
* @type {Object}
*/
Texture.Filter = {
NEAREST: 9728,
NEAREST_MIPMAP_LINEAR: 9986,
NEAREST_MIPMAP_NEAREST: 9984,
LINEAR: 9729,
LINEAR_MIPMAP_LINEAR: 9987,
LINEAR_MIPMAP_NEAREST: 9985
};
/**
* A set of Wrap constants that match their GL counterparts.
* This is for convenience, to avoid the need for a GL rendering context.
*
* @example
* ```
* Texture.Wrap.CLAMP_TO_EDGE
* Texture.Wrap.MIRRORED_REPEAT
* Texture.Wrap.REPEAT
* ```
* @attribute Wrap
* @static
* @type {Object}
*/
Texture.Wrap = {
CLAMP_TO_EDGE: 33071,
MIRRORED_REPEAT: 33648,
REPEAT: 10497
};
/**
* A set of Format constants that match their GL counterparts.
* This is for convenience, to avoid the need for a GL rendering context.
*
* @example
* ```
* Texture.Format.RGB
* Texture.Format.RGBA
* Texture.Format.LUMINANCE_ALPHA
* ```
* @attribute Format
* @static
* @type {Object}
*/
Texture.Format = {
DEPTH_COMPONENT: 6402,
ALPHA: 6406,
RGBA: 6408,
RGB: 6407,
LUMINANCE: 6409,
LUMINANCE_ALPHA: 6410
};
/**
* A set of DataType constants that match their GL counterparts.
* This is for convenience, to avoid the need for a GL rendering context.
*
* @example
* ```
* Texture.DataType.UNSIGNED_BYTE
* Texture.DataType.FLOAT
* ```
* @attribute DataType
* @static
* @type {Object}
*/
Texture.DataType = {
BYTE: 5120,
SHORT: 5122,
INT: 5124,
FLOAT: 5126,
UNSIGNED_BYTE: 5121,
UNSIGNED_INT: 5125,
UNSIGNED_SHORT: 5123,
UNSIGNED_SHORT_4_4_4_4: 32819,
UNSIGNED_SHORT_5_5_5_1: 32820,
UNSIGNED_SHORT_5_6_5: 33635
}
/**
* The default wrap mode when creating new textures. If a custom
* provider was specified, it may choose to override this default mode.
*
* @attribute {GLenum} DEFAULT_WRAP
* @static
* @default Texture.Wrap.CLAMP_TO_EDGE
*/
Texture.DEFAULT_WRAP = Texture.Wrap.CLAMP_TO_EDGE;
/**
* The default filter mode when creating new textures. If a custom
* provider was specified, it may choose to override this default mode.
*
* @attribute {GLenum} DEFAULT_FILTER
* @static
* @default Texture.Filter.LINEAR
*/
Texture.DEFAULT_FILTER = Texture.Filter.NEAREST;
/**
* By default, we do some error checking when creating textures
* to ensure that they will be "renderable" by WebGL. Non-power-of-two
* textures must use CLAMP_TO_EDGE as their wrap mode, and NEAREST or LINEAR
* as their wrap mode. Further, trying to generate mipmaps for a NPOT image
* will lead to errors.
*
* However, you can disable this error checking by setting `FORCE_POT` to true.
* This may be useful if you are running on specific hardware that supports POT
* textures, or in some future case where NPOT textures is added as a WebGL extension.
*
* @attribute {Boolean} FORCE_POT
* @static
* @default false
*/
Texture.FORCE_POT = false;
//default pixel store operations. Used in create()
Texture.UNPACK_FLIP_Y = false;
Texture.UNPACK_ALIGNMENT = 1;
Texture.UNPACK_PREMULTIPLY_ALPHA = true;
Texture.UNPACK_COLORSPACE_CONVERSION = undefined;
//for the Image constructor we need to handle things a bit differently..
Texture.USE_DUMMY_1x1_DATA = true;
/**
* Utility to get the number of components for the given GLenum, e.g. gl.RGBA returns 4.
* Returns null if the specified format is not of type DEPTH_COMPONENT, ALPHA, LUMINANCE,
* LUMINANCE_ALPHA, RGB, or RGBA.
*
* @method getNumComponents
* @static
* @param {GLenum} format a texture format, i.e. Texture.Format.RGBA
* @return {Number} the number of components for this format
*/
Texture.getNumComponents = function(format) {
switch (format) {
case Texture.Format.DEPTH_COMPONENT:
case Texture.Format.ALPHA:
case Texture.Format.LUMINANCE:
return 1;
case Texture.Format.LUMINANCE_ALPHA:
return 2;
case Texture.Format.RGB:
return 3;
case Texture.Format.RGBA:
return 4;
}
return null;
};
module.exports = Texture;