/***********************************************************************
* mt4j Copyright (c) 2008 - 2009 C.Ruff, Fraunhofer-Gesellschaft All rights reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
***********************************************************************/
package org.mt4j.util.opengl;
import java.nio.IntBuffer;
import java.util.ArrayList;
import java.util.List;
import javax.media.opengl.GL;
import org.apache.log4j.ConsoleAppender;
import org.apache.log4j.Level;
import org.apache.log4j.Logger;
import org.apache.log4j.SimpleLayout;
import org.mt4j.MTApplication;
import org.mt4j.util.math.Tools3D;
import org.mt4j.util.math.ToolsBuffers;
import org.mt4j.util.math.ToolsMath;
import org.mt4j.util.opengl.GLTexture.EXPANSION_FILTER;
import org.mt4j.util.opengl.GLTexture.SHRINKAGE_FILTER;
import org.mt4j.util.opengl.GLTexture.TEXTURE_TARGET;
import org.mt4j.util.opengl.GLTexture.WRAP_MODE;
import processing.core.PApplet;
import processing.opengl.PGraphicsOpenGL;
/**
* This class abstracts a opengl frame buffer object for easier usage.
* This can mainly be used to draw to an offscreen buffer and use that buffer
* as a texture later.
*
* @author Christopher Ruff
*/
public class GLFBO {
/** The Constant logger. */
private static final Logger logger = Logger.getLogger(GLFBO.class.getName());
static{
// logger.setLevel(Level.ERROR);
// logger.setLevel(Level.WARN);
// logger.setLevel(Level.DEBUG);
logger.setLevel(Level.INFO);
SimpleLayout l = new SimpleLayout();
ConsoleAppender ca = new ConsoleAppender(l);
logger.addAppender(ca);
}
private GL gl;
// private int[] fboID;
private int fboID;
private int depthRBID;
private int width;
private int height;
private PApplet pa;
private List<GLTexture> textures;
private int viewportX;
private int viewportY;
private int viewportWidth;
private int viewportHeight;
private boolean stencilBufferAttached;
private GLFboStack fboStack;
/**
* Instantiates a new GL FBO.
*
* @param pa the pa
* @param width the width
* @param height the height
*/
public GLFBO(PApplet pa, int width, int height) {
this(pa, width, height, true);
}
/**
* Instantiates a new GL FBO.
*
* @param pa the pa
* @param width the width
* @param height the height
* @param attachStencilBuffer the attach stencil buffer
*/
public GLFBO(PApplet pa, int width, int height, boolean attachStencilBuffer) {
super();
this.pa = pa;
this.gl = ((PGraphicsOpenGL)pa.g).gl;
this.stencilBufferAttached = attachStencilBuffer;
this.fboID = 0;
this.depthRBID = 0;
this.viewportX = 0;
this.viewportY = 0;
this.viewportWidth = width;
this.viewportHeight = height;
this.width = width;
this.height = height;
this.textures = new ArrayList<GLTexture>();
//FIXME FBO STACK TEST!!
this.fboStack = GLFboStack.getInstance();
this.initFBO();
}
private void initFBO(){
IntBuffer buffer = ToolsBuffers.createIntBuffer(1);
gl.glGenFramebuffersEXT(1, buffer);
this.fboID = buffer.get(0);
gl.glBindFramebufferEXT(GL.GL_FRAMEBUFFER_EXT, fboID);
//Create depth buffer
IntBuffer buffer2 = ToolsBuffers.createIntBuffer(1);
gl.glGenRenderbuffersEXT(1, buffer2);
this.depthRBID = buffer2.get(0);
gl.glBindRenderbufferEXT(GL.GL_RENDERBUFFER_EXT, depthRBID);
if (this.isStencilBufferAttached()){
//THIS CREATES A FBO WITH A STENCIL BUFFER! HAS TO BE SUPPORTED ON THE PLATFORM!
gl.glRenderbufferStorageEXT(GL.GL_RENDERBUFFER_EXT, GL.GL_DEPTH24_STENCIL8_EXT, this.width, this.height);
}else{
//Creates a fbo with a depth but without a stencil buffer
gl.glRenderbufferStorageEXT(GL.GL_RENDERBUFFER_EXT, GL.GL_DEPTH_COMPONENT, this.width, this.height); //orginal
}
//Attach depth buffer to FBO
gl.glFramebufferRenderbufferEXT(GL.GL_FRAMEBUFFER_EXT, GL.GL_DEPTH_ATTACHMENT_EXT, GL.GL_RENDERBUFFER_EXT, depthRBID);
if (this.isStencilBufferAttached()){
//Attach stencil buffer to FBO - HAS TO BE SUPPORTED ON THE PLATFORM!
gl.glFramebufferRenderbufferEXT(GL.GL_FRAMEBUFFER_EXT, GL.GL_STENCIL_ATTACHMENT_EXT, GL.GL_RENDERBUFFER_EXT, depthRBID);
}
gl.glBindFramebufferEXT(GL.GL_FRAMEBUFFER_EXT, 0);
}
/**
* Attaches a gl texture object to the frame buffer object.
* The texture will contain everything drawn when the fbo is bound.
*
* @return the gL texture
*/
public GLTexture addNewTexture(){
return this.addNewTexture(false);
}
/**
* This creates a GLTexture object with the FBO dimensions and
* attaches it to this frame buffer object.
* The texture can later be used to texture a component.
*
* @return the gL texture
*/
public GLTexture addNewTexture(boolean useMipMap){
this.bind();
boolean isPowerOfTwoDimension = ToolsMath.isPowerOfTwo(this.width) && ToolsMath.isPowerOfTwo(this.height);
GLTextureSettings texSettings = new GLTextureSettings();
texSettings.wrappingHorizontal = WRAP_MODE.CLAMP_TO_EDGE;
texSettings.wrappingVertical = WRAP_MODE.CLAMP_TO_EDGE;
texSettings.shrinkFilter = SHRINKAGE_FILTER.BilinearNoMipMaps;
texSettings.expansionFilter = EXPANSION_FILTER.Bilinear;
// GLTextureParameters tp = new GLTextureParameters();
// //Set texture FILTER MODE
//// tp.minFilter = GLTextureParameters.LINEAR_MIPMAP_LINEAR;
//// tp.minFilter = GLTextureParameters.LINEAR_MIPMAP_NEAREST;
// tp.minFilter = GLTextureParameters.LINEAR;
//// tp.minFilter = GLTextureParameters.NEAREST;
//// if (useMipMap)
//// tp.minFilter = GLTextureParameters.LINEAR_MIPMAP_NEAREST; //Seems to not display the fbo texture when POT
//// else{
//// tp.minFilter = GLTextureParameters.LINEAR;
//// }
// tp.magFilter = GLTextureParameters.LINEAR;
//
// //Set texture WRAP MODE
// tp.wrap_s = GL.GL_CLAMP_TO_EDGE;
// tp.wrap_t = GL.GL_CLAMP_TO_EDGE;
//// tp.wrap_s = GL.GL_CLAMP;
//// tp.wrap_t = GL.GL_CLAMP;
//Set texture TARGET
if (isPowerOfTwoDimension){
// tp.target = GLTextureParameters.NORMAL;
texSettings.target = TEXTURE_TARGET.TEXTURE_2D;
logger.debug("Power of 2 FBO texture created");
}else{
// tp.target = GLTextureParameters.RECTANGULAR;
texSettings.target = TEXTURE_TARGET.RECTANGULAR;
logger.debug("Rectangular FBO texture created");
}
// GLTexture tex = new GLTexture(pa, this.width, this.height, tp, true, 0);
GLTexture tex = new GLTexture(this.pa, texSettings);
tex.setupGLTexture(this.width, this.height);
tex.width = this.width;
tex.height = this.height; //To prevent init() call in loadGLTexture().. not even neccessary with fbo usage?
gl.glBindTexture(tex.getTextureTarget(), tex.getTextureID());
//Use extension to automatically generate mipmaps for the fbo textures
//TODO Is this always needed? supported? only working in power of two dimensions!?
// if (useMipMap && isPowerOfTwoDimension) //only for target GL_TEXTURE_2D allowed
// gl.glGenerateMipmapEXT(tex.getTextureTarget()); //FIXME seems to crash JVM after app close! //FIXME do this only after drawing finished to fbo texture
//Attach texture to FBO
gl.glFramebufferTexture2DEXT(
GL.GL_FRAMEBUFFER_EXT,
GL.GL_COLOR_ATTACHMENT0_EXT,
tex.getTextureTarget(), tex.getTextureID(), 0);
gl.glBindTexture(tex.getTextureTarget(), 0);
this.checkFBOComplete(gl, fboID);
this.unBind();
//Add to list
this.textures.add(tex);
return tex;
}
//TEXTURE HAS TO HAVE FBO DIMENSIONS TO WORK! THIS REPLACED THE OLD ATTACHED TEXTURE!
public boolean add(GLTexture tex) {
this.bind();
gl.glBindTexture(tex.getTextureTarget(), tex.getTextureID());
/*
//F�r OHNE mipmapping
gl.glTexParameteri(tex.getTextureTarget(),GL.GL_TEXTURE_MAG_FILTER, GL.GL_NEAREST);
gl.glTexParameteri(tex.getTextureTarget(),GL.GL_TEXTURE_MIN_FILTER, GL.GL_NEAREST);
*/
// /*
//F�r MIIPMAPPING
// gl.glTexParameteri(tex.getTextureTarget(), GL.GL_GENERATE_MIPMAP, GL.GL_TRUE); // automatic mipmap
gl.glTexParameterf(tex.getTextureTarget(), GL.GL_TEXTURE_WRAP_S, GL.GL_CLAMP_TO_EDGE);
gl.glTexParameterf(tex.getTextureTarget(), GL.GL_TEXTURE_WRAP_T, GL.GL_CLAMP_TO_EDGE);
gl.glTexParameteri(tex.getTextureTarget(), GL.GL_TEXTURE_MAG_FILTER, GL.GL_LINEAR);
gl.glTexParameteri(tex.getTextureTarget(), GL.GL_TEXTURE_MIN_FILTER, GL.GL_LINEAR);
gl.glGenerateMipmapEXT(tex.getTextureTarget());
// */
//Attach texture to FBO
gl.glFramebufferTexture2DEXT(
GL.GL_FRAMEBUFFER_EXT,
GL.GL_COLOR_ATTACHMENT0_EXT,
tex.getTextureTarget(), tex.getTextureID(), 0);
gl.glBindTexture(tex.getTextureTarget(), 0);
this.checkFBOComplete(gl, fboID);
this.unBind();
return textures.add(tex);
}
public boolean contains(GLTexture arg0) {
return textures.contains(arg0);
}
public boolean remove(GLTexture arg0) {
return textures.remove(arg0);
}
/*
In this instance we are creating a normal RGBA image of the same width and height as the renderbuffer we created earlier;
this is important as ALL attachments to a FBO have to be the same width and height.
Note that we don�t upload any data, the space is just reserved by OpenGL so we can use it later.
*/
/**
* If clearColorBuffer is set to true this clears the colorbuffer with the specified color values.
* <br>If clearDepthBuffer is set to true this clears the depth buffer of the framebuffer object.
* <br>NOTE: It seems that a texture has to be attached to the FBO first, for this to work as expected.
* <br>NOTE: This method will bind and unbind the FBO! So use this method only outside of startRenderToTexture() or bind()
* @param clearColorBuffer the color to clear the color buffer with
* @param r the r
* @param g the g
* @param b the b
* @param a the a
* @param clearDepthBuffer clear the depth buffer
*/
public void clear(boolean clearColorBuffer, float r, float g, float b, float a, boolean clearDepthBuffer){
//FIXME make it so we can specify 0..255 colors, and not openGL 0..1 !
//GL gl = Tools3D.getGL(app);
this.bind();
if (clearColorBuffer){
gl.glClearColor(r, g, b, a);
gl.glClear(GL.GL_COLOR_BUFFER_BIT);
}
if (clearDepthBuffer){
gl.glClear(GL.GL_DEPTH_BUFFER_BIT);
}
this.unBind();
}
protected void bind(){
gl.glBindFramebufferEXT(GL.GL_FRAMEBUFFER_EXT, fboID);
//gl.glBindRenderbufferEXT(GL.GL_RENDERBUFFER_EXT, depthRBID);
}
/**
* Sets up this frame buffer object to render all following rendering commands
* into the offscreen textures which are attached to the frame buffer object.
*/
public void startRenderToTexture(){
//FIXME FBO STACK TEST
// this.bind();
this.fboStack.pushFBO();
this.fboStack.useFBO(this);
/*
//TODO f�r mehrere texturen,
- check wieviele buffers (color attachments) m�glich
- f�r jede texture ein color attachment binden
=> gl.glFramebufferTexture2DEXT(
GL.GL_FRAMEBUFFER_EXT,
GL.GL_COLOR_ATTACHMENT0_EXT, //GL.GL_COLOR_ATTACHMENT1_EXT, ..
tex.getTextureTarget(), tex.getTextureID(), 0);
- glDrawBuffers aufrufen //und glReadBuffers
setDrawBuffer(GL.GL_NONE); //wenn kein mutliple texture draw m�glich
setReadBuffer(GL.GL_NONE);
*/
gl.glPushAttrib(GL.GL_VIEWPORT_BIT);
// gl.glDrawBuffer(GL.GL_NONE);
// gl.glViewport(0, 0, width, height);
// gl.glViewport(-50,-50, pa.width+100, pa.height+100);
gl.glViewport(this.viewportX,this.viewportY, this.viewportWidth, this.viewportHeight);
}
/**
* Stops this FBO from rendering in to the attached texture(s).
* <p>NOTE: To use the texture we rendered into, it might me necessary to bind the
* texture and then call gl.glGenerateMipmapEXT(texture.getTextureTarget());
* for mipmap generation. Else we might get a black texture! (not yet confirmed)
*
*/
public void stopRenderToTexture(){
gl.glPopAttrib();
//FIXME FBO STACK TEST
// this.unBind();
this.fboStack.popFBO();
}
protected void unBind() {
//gl.glBindRenderbufferEXT(GL.GL_RENDERBUFFER_EXT, 0);
gl.glBindFramebufferEXT(GL.GL_FRAMEBUFFER_EXT, 0);
}
private void setReadBuffer(int attachVal) {
gl.glReadBuffer(attachVal);
}
private void setDrawBuffer(int attachVal) {
gl.glDrawBuffer(attachVal);
}
/**
* Destroys and deallocates this FBO.
*/
public void destroy() {
if (fboID > 0) {
final IntBuffer id = ToolsBuffers.createIntBuffer(1);
id.put(fboID);
id.rewind();
gl.glDeleteFramebuffersEXT(id.limit(), id);
fboID = 0;
}
if (depthRBID > 0) {
final IntBuffer id = ToolsBuffers.createIntBuffer(1);
id.put(depthRBID);
id.rewind();
gl.glDeleteRenderbuffersEXT(id.limit(), id);
depthRBID = 0;
}
this.textures.clear();
}
@Override
protected void finalize() throws Throwable {
logger.debug("Finalizing - " + this);
if (this.pa instanceof MTApplication) {
MTApplication mtApp = (MTApplication) this.pa;
mtApp.invokeLater(new Runnable() {
public void run() {
destroy();
}
});
}else{
//TODO use registerPre()?
//is the object even valid after finalize() is called??
}
super.finalize();
}
public boolean isStencilBufferAttached() {
return stencilBufferAttached;
}
public int getWidth() {
return width;
}
public int getHeight() {
return height;
}
public int getName(){
return this.fboID;
}
//TODO for several render targets IMPLEMENT!
/*
public void setDrawBuffers(GLTexture[] drawTextures, int n){
numDrawBuffersInUse = PApplet.min(n, drawTextures.length);
colorDrawBuffers = new int[numDrawBuffersInUse];
textureIDs = new int[numDrawBuffersInUse];
textureTargets = new int[numDrawBuffersInUse];
for (int i = 0; i < numDrawBuffersInUse; i++)
{
colorDrawBuffers[i] = GL.GL_COLOR_ATTACHMENT0_EXT + i;
textureTargets[i] = drawTextures[i].getTextureTarget();
textureIDs[i] = drawTextures[i].getTextureID();
gl.glFramebufferTexture2DEXT(GL.GL_FRAMEBUFFER_EXT, colorDrawBuffers[i], textureTargets[i], textureIDs[i], 0);
}
checkFBO();
gl.glDrawBuffers(numDrawBuffersInUse, IntBuffer.wrap(colorDrawBuffers));
}
*/
//TODO for stencil enabled fbo! IMPLEMENT!
/*
// Allocating space for multisampled depth buffer
gl.glRenderbufferStorageMultisampleEXT(GL.GL_RENDERBUFFER_EXT, multisampleLevel, GL_DEPTH24_STENCIL8, width, height);
// Creating handle for multisampled FBO
glstate.pushFramebuffer();
glstate.setFramebuffer(multisampleFBO);
gl.glFramebufferRenderbufferEXT(GL.GL_FRAMEBUFFER_EXT, GL.GL_COLOR_ATTACHMENT0_EXT, GL.GL_RENDERBUFFER_EXT, colorBufferMulti[0]);
gl.glFramebufferRenderbufferEXT(GL.GL_FRAMEBUFFER_EXT, GL.GL_DEPTH_ATTACHMENT_EXT, GL.GL_RENDERBUFFER_EXT, depthStencilBuffer[0]);
gl.glFramebufferRenderbufferEXT(GL.GL_FRAMEBUFFER_EXT, GL.GL_STENCIL_ATTACHMENT_EXT, GL.GL_RENDERBUFFER_EXT, depthStencilBuffer[0]);
*/
public void checkFBOComplete(GL gl, int fboID) {
final int framebuffer = gl.glCheckFramebufferStatusEXT(GL.GL_FRAMEBUFFER_EXT);
switch (framebuffer) {
case GL.GL_FRAMEBUFFER_COMPLETE_EXT:
logger.debug("FRAMEBUFFER STATUS COMPLETE!");
break;
case GL.GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT_EXT:
doError(", has caused a GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT_EXT exception", fboID);
break;
case GL.GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT_EXT:
doError(", has caused a GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT_EXT exception", fboID);
break;
case GL.GL_FRAMEBUFFER_INCOMPLETE_DIMENSIONS_EXT:
doError(", has caused a GL_FRAMEBUFFER_INCOMPLETE_DIMENSIONS_EXT exception", fboID);
break;
case GL.GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER_EXT:
doError(", has caused a GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER_EXT exception", fboID);
break;
case GL.GL_FRAMEBUFFER_INCOMPLETE_FORMATS_EXT:
doError(", has caused a GL_FRAMEBUFFER_INCOMPLETE_FORMATS_EXT exception", fboID);
break;
case GL.GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER_EXT:
doError(", has caused a GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER_EXT exception", fboID);
break;
case GL.GL_FRAMEBUFFER_UNSUPPORTED_EXT:
doError(", has caused a GL_FRAMEBUFFER_UNSUPPORTED_EXT exception", fboID);
break;
default:
doError(", Unexpected reply from glCheckFramebufferStatusEXT: ", fboID);
break;
}
}
private void doError(String msg, int fboID){
// throw new RuntimeException("FrameBuffer: " + fboID + msg);
logger.error("FrameBuffer: " + fboID + msg);
}
public void setViewportX(int viewportX,int viewportY, int viewportWidth, int viewportHeight) {
this.viewportX = viewportX;
this.viewportY = viewportY;
this.viewportWidth = viewportWidth;
this.viewportHeight = viewportHeight;
}
public int[] getViewport(){
return new int[]{
this.viewportX,
this.viewportY,
this.viewportWidth,
this.viewportHeight};
}
/**
* Checks if the FrameBufferObject is supported on this platform.
*
* @param app the PApplet
*
* @return true, if is supported
*/
public static boolean isSupported(PApplet app){
return Tools3D.isGLExtensionSupported(app, "GL_EXT_framebuffer_object");
}
}