import Base from './core/Base';
import Texture from './Texture';
import TextureCube from './TextureCube';
import glenum from './core/glenum';
import Cache from './core/Cache';
var KEY_FRAMEBUFFER = 'framebuffer';
var KEY_RENDERBUFFER = 'renderbuffer';
var KEY_RENDERBUFFER_WIDTH = KEY_RENDERBUFFER + '_width';
var KEY_RENDERBUFFER_HEIGHT = KEY_RENDERBUFFER + '_height';
var KEY_RENDERBUFFER_ATTACHED = KEY_RENDERBUFFER + '_attached';
var KEY_DEPTHTEXTURE_ATTACHED = 'depthtexture_attached';
var GL_FRAMEBUFFER = glenum.FRAMEBUFFER;
var GL_RENDERBUFFER = glenum.RENDERBUFFER;
var GL_DEPTH_ATTACHMENT = glenum.DEPTH_ATTACHMENT;
var GL_COLOR_ATTACHMENT0 = glenum.COLOR_ATTACHMENT0;
/**
* @constructor clay.FrameBuffer
* @extends clay.core.Base
*/
var FrameBuffer = Base.extend(
/** @lends clay.FrameBuffer# */
{
/**
* If use depth buffer
* @type {boolean}
*/
depthBuffer: true,
/**
* @type {Object}
*/
viewport: null,
_width: 0,
_height: 0,
_textures: null,
_boundRenderer: null,
}, function () {
// Use cache
this._cache = new Cache();
this._textures = {};
},
/**@lends clay.FrameBuffer.prototype. */
{
/**
* Get attached texture width
* {number}
*/
// FIXME Can't use before #bind
getTextureWidth: function () {
return this._width;
},
/**
* Get attached texture height
* {number}
*/
getTextureHeight: function () {
return this._height;
},
/**
* Bind the framebuffer to given renderer before rendering
* @param {clay.Renderer} renderer
*/
bind: function (renderer) {
if (renderer.__currentFrameBuffer) {
// Already bound
if (renderer.__currentFrameBuffer === this) {
return;
}
console.warn('Renderer already bound with another framebuffer. Unbind it first');
}
renderer.__currentFrameBuffer = this;
var _gl = renderer.gl;
_gl.bindFramebuffer(GL_FRAMEBUFFER, this._getFrameBufferGL(renderer));
this._boundRenderer = renderer;
var cache = this._cache;
cache.put('viewport', renderer.viewport);
var hasTextureAttached = false;
var width;
var height;
for (var attachment in this._textures) {
hasTextureAttached = true;
var obj = this._textures[attachment];
if (obj) {
// TODO Do width, height checking, make sure size are same
width = obj.texture.width;
height = obj.texture.height;
// Attach textures
this._doAttach(renderer, obj.texture, attachment, obj.target);
}
}
this._width = width;
this._height = height;
if (!hasTextureAttached && this.depthBuffer) {
console.error('Must attach texture before bind, or renderbuffer may have incorrect width and height.')
}
if (this.viewport) {
renderer.setViewport(this.viewport);
}
else {
renderer.setViewport(0, 0, width, height, 1);
}
var attachedTextures = cache.get('attached_textures');
if (attachedTextures) {
for (var attachment in attachedTextures) {
if (!this._textures[attachment]) {
var target = attachedTextures[attachment];
this._doDetach(_gl, attachment, target);
}
}
}
if (!cache.get(KEY_DEPTHTEXTURE_ATTACHED) && this.depthBuffer) {
// Create a new render buffer
if (cache.miss(KEY_RENDERBUFFER)) {
cache.put(KEY_RENDERBUFFER, _gl.createRenderbuffer());
}
var renderbuffer = cache.get(KEY_RENDERBUFFER);
if (width !== cache.get(KEY_RENDERBUFFER_WIDTH)
|| height !== cache.get(KEY_RENDERBUFFER_HEIGHT)) {
_gl.bindRenderbuffer(GL_RENDERBUFFER, renderbuffer);
_gl.renderbufferStorage(GL_RENDERBUFFER, _gl.DEPTH_COMPONENT16, width, height);
cache.put(KEY_RENDERBUFFER_WIDTH, width);
cache.put(KEY_RENDERBUFFER_HEIGHT, height);
_gl.bindRenderbuffer(GL_RENDERBUFFER, null);
}
if (!cache.get(KEY_RENDERBUFFER_ATTACHED)) {
_gl.framebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, renderbuffer);
cache.put(KEY_RENDERBUFFER_ATTACHED, true);
}
}
},
/**
* Unbind the frame buffer after rendering
* @param {clay.Renderer} renderer
*/
unbind: function (renderer) {
// Remove status record on renderer
renderer.__currentFrameBuffer = null;
var _gl = renderer.gl;
_gl.bindFramebuffer(GL_FRAMEBUFFER, null);
this._boundRenderer = null;
this._cache.use(renderer.__uid__);
var viewport = this._cache.get('viewport');
// Reset viewport;
if (viewport) {
renderer.setViewport(viewport);
}
this.updateMipmap(renderer);
},
// Because the data of texture is changed over time,
// Here update the mipmaps of texture each time after rendered;
updateMipmap: function (renderer) {
var _gl = renderer.gl;
for (var attachment in this._textures) {
var obj = this._textures[attachment];
if (obj) {
var texture = obj.texture;
// FIXME some texture format can't generate mipmap
if (!texture.NPOT && texture.useMipmap
&& texture.minFilter === Texture.LINEAR_MIPMAP_LINEAR) {
var target = texture.textureType === 'textureCube' ? glenum.TEXTURE_CUBE_MAP : glenum.TEXTURE_2D;
_gl.bindTexture(target, texture.getWebGLTexture(renderer));
_gl.generateMipmap(target);
_gl.bindTexture(target, null);
}
}
}
},
// 0x8CD5, 36053, FRAMEBUFFER_COMPLETE
// 0x8CD6, 36054, FRAMEBUFFER_INCOMPLETE_ATTACHMENT
// 0x8CD7, 36055, FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT
// 0x8CD9, 36057, FRAMEBUFFER_INCOMPLETE_DIMENSIONS
// 0x8CDD, 36061, FRAMEBUFFER_UNSUPPORTED
checkStatus: function (_gl) {
return _gl.checkFramebufferStatus(GL_FRAMEBUFFER);
},
_getFrameBufferGL: function (renderer) {
var cache = this._cache;
cache.use(renderer.__uid__);
if (cache.miss(KEY_FRAMEBUFFER)) {
cache.put(KEY_FRAMEBUFFER, renderer.gl.createFramebuffer());
}
return cache.get(KEY_FRAMEBUFFER);
},
/**
* Attach a texture(RTT) to the framebuffer
* @param {clay.Texture} texture
* @param {number} [attachment=gl.COLOR_ATTACHMENT0]
* @param {number} [target=gl.TEXTURE_2D]
*/
attach: function (texture, attachment, target) {
if (!texture.width) {
throw new Error('The texture attached to color buffer is not a valid.');
}
// TODO width and height check
// If the depth_texture extension is enabled, developers
// Can attach a depth texture to the depth buffer
// http://blog.tojicode.com/2012/07/using-webgldepthtexture.html
attachment = attachment || GL_COLOR_ATTACHMENT0;
target = target || glenum.TEXTURE_2D;
var boundRenderer = this._boundRenderer;
var _gl = boundRenderer && boundRenderer.gl;
var attachedTextures;
if (_gl) {
var cache = this._cache;
cache.use(boundRenderer.__uid__);
attachedTextures = cache.get('attached_textures');
}
// Check if texture attached
var previous = this._textures[attachment];
if (previous && previous.target === target
&& previous.texture === texture
&& (attachedTextures && attachedTextures[attachment] != null)
) {
return;
}
var canAttach = true;
if (boundRenderer) {
canAttach = this._doAttach(boundRenderer, texture, attachment, target);
// Set viewport again incase attached to different size textures.
if (!this.viewport) {
boundRenderer.setViewport(0, 0, texture.width, texture.height, 1);
}
}
if (canAttach) {
this._textures[attachment] = this._textures[attachment] || {};
this._textures[attachment].texture = texture;
this._textures[attachment].target = target;
}
},
_doAttach: function (renderer, texture, attachment, target) {
var _gl = renderer.gl;
// Make sure texture is always updated
// Because texture width or height may be changed and in this we can't be notified
// FIXME awkward;
var webglTexture = texture.getWebGLTexture(renderer);
// Assume cache has been used.
var attachedTextures = this._cache.get('attached_textures');
if (attachedTextures && attachedTextures[attachment]) {
var obj = attachedTextures[attachment];
// Check if texture and target not changed
if (obj.texture === texture && obj.target === target) {
return;
}
}
attachment = +attachment;
var canAttach = true;
if (attachment === GL_DEPTH_ATTACHMENT || attachment === glenum.DEPTH_STENCIL_ATTACHMENT) {
var extension = renderer.getGLExtension('WEBGL_depth_texture');
if (!extension) {
console.error('Depth texture is not supported by the browser');
canAttach = false;
}
if (texture.format !== glenum.DEPTH_COMPONENT
&& texture.format !== glenum.DEPTH_STENCIL
) {
console.error('The texture attached to depth buffer is not a valid.');
canAttach = false;
}
// Dispose render buffer created previous
if (canAttach) {
var renderbuffer = this._cache.get(KEY_RENDERBUFFER);
if (renderbuffer) {
_gl.framebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, null);
_gl.deleteRenderbuffer(renderbuffer);
this._cache.put(KEY_RENDERBUFFER, false);
}
this._cache.put(KEY_RENDERBUFFER_ATTACHED, false);
this._cache.put(KEY_DEPTHTEXTURE_ATTACHED, true);
}
}
// Mipmap level can only be 0
_gl.framebufferTexture2D(GL_FRAMEBUFFER, attachment, target, webglTexture, 0);
if (!attachedTextures) {
attachedTextures = {};
this._cache.put('attached_textures', attachedTextures);
}
attachedTextures[attachment] = attachedTextures[attachment] || {};
attachedTextures[attachment].texture = texture;
attachedTextures[attachment].target = target;
return canAttach;
},
_doDetach: function (_gl, attachment, target) {
// Detach a texture from framebuffer
// https://github.com/KhronosGroup/WebGL/blob/master/conformance-suites/1.0.0/conformance/framebuffer-test.html#L145
_gl.framebufferTexture2D(GL_FRAMEBUFFER, attachment, target, null, 0);
// Assume cache has been used.
var attachedTextures = this._cache.get('attached_textures');
if (attachedTextures && attachedTextures[attachment]) {
attachedTextures[attachment] = null;
}
if (attachment === GL_DEPTH_ATTACHMENT || attachment === glenum.DEPTH_STENCIL_ATTACHMENT) {
this._cache.put(KEY_DEPTHTEXTURE_ATTACHED, false);
}
},
/**
* Detach a texture
* @param {number} [attachment=gl.COLOR_ATTACHMENT0]
* @param {number} [target=gl.TEXTURE_2D]
*/
detach: function (attachment, target) {
// TODO depth extension check ?
this._textures[attachment] = null;
if (this._boundRenderer) {
var cache = this._cache;
cache.use(this._boundRenderer.__uid__);
this._doDetach(this._boundRenderer.gl, attachment, target);
}
},
/**
* Dispose
* @param {WebGLRenderingContext} _gl
*/
dispose: function (renderer) {
var _gl = renderer.gl;
var cache = this._cache;
cache.use(renderer.__uid__);
var renderBuffer = cache.get(KEY_RENDERBUFFER);
if (renderBuffer) {
_gl.deleteRenderbuffer(renderBuffer);
}
var frameBuffer = cache.get(KEY_FRAMEBUFFER);
if (frameBuffer) {
_gl.deleteFramebuffer(frameBuffer);
}
cache.deleteContext(renderer.__uid__);
// Clear cache for reusing
this._textures = {};
}
});
FrameBuffer.DEPTH_ATTACHMENT = GL_DEPTH_ATTACHMENT;
FrameBuffer.COLOR_ATTACHMENT0 = GL_COLOR_ATTACHMENT0;
FrameBuffer.STENCIL_ATTACHMENT = glenum.STENCIL_ATTACHMENT;
FrameBuffer.DEPTH_STENCIL_ATTACHMENT = glenum.DEPTH_STENCIL_ATTACHMENT;
export default FrameBuffer;