org.terasology.logic.manager.PostProcessingRenderer.java Source code

Java tutorial

Introduction

Here is the source code for org.terasology.logic.manager.PostProcessingRenderer.java

Source

/*
 * Copyright 2012 Benjamin Glatzel <benjamin.glatzel@me.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.terasology.logic.manager;

import java.nio.ByteBuffer;
import java.nio.IntBuffer;
import java.util.HashMap;

import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.terasology.game.CoreRegistry;
import org.terasology.math.TeraMath;
import org.terasology.rendering.shader.ShaderProgram;
import org.terasology.rendering.world.WorldRenderer;

import javax.swing.*;

import static org.lwjgl.opengl.GL11.*;

/**
 * Responsible for applying and rendering various shader based
 * post processing effects.
 *
 * @author Benjamin Glatzel <benjamin.glatzel@me.com>
 */
public class PostProcessingRenderer {

    public static final float DEFAULT_EXPOSURE = 2.5f;
    public static final float MAX_EXPOSURE = 6.0f;
    public static final float MAX_EXPOSURE_NIGHT = 1.0f;
    public static final float MIN_EXPOSURE = 0.5f;
    public static final float TARGET_LUMINANCE = 1.0f;
    public static final float ADJUSTMENT_SPEED = 0.025f;

    private static final Logger logger = LoggerFactory.getLogger(PostProcessingRenderer.class);

    private static PostProcessingRenderer _instance = null;
    private float _exposure = 2.0f;
    private float _sceneLuminance = 1.0f;
    private int _displayListQuad = -1;

    public class FBO {
        public int _fboId = 0;
        public int _textureId = 0;
        public int _depthTextureId = 0;
        public int _depthRboId = 0;
        public int _normalsTextureId = 0;

        public int _width = 0;
        public int _height = 0;

        public void bind() {
            EXTFramebufferObject.glBindFramebufferEXT(EXTFramebufferObject.GL_FRAMEBUFFER_EXT, _fboId);
        }

        public void unbind() {
            EXTFramebufferObject.glBindFramebufferEXT(EXTFramebufferObject.GL_FRAMEBUFFER_EXT, 0);
        }

        public void bindDepthTexture() {
            GL11.glBindTexture(GL11.GL_TEXTURE_2D, _depthTextureId);
        }

        public void bindTexture() {
            GL11.glBindTexture(GL11.GL_TEXTURE_2D, _textureId);
        }

        public void bindNormalsTexture() {
            GL11.glBindTexture(GL11.GL_TEXTURE_2D, _normalsTextureId);
        }

        public void unbindTexture() {
            GL11.glBindTexture(GL11.GL_TEXTURE_2D, 0);
        }
    }

    private HashMap<String, FBO> _FBOs = new HashMap<String, FBO>();

    /**
     * Returns (and creates  if necessary) the static instance
     * of this helper class.
     *
     * @return The instance
     */
    public static PostProcessingRenderer getInstance() {
        if (_instance == null) {
            _instance = new PostProcessingRenderer();
        }

        return _instance;
    }

    public PostProcessingRenderer() {
        initialize();
    }

    public void initialize() {
        createOrUpdateFullscreenFbos();

        createFBO("scene16", 16, 16, false, false, false);
        createFBO("scene8", 8, 8, false, false, false);
        createFBO("scene4", 4, 4, false, false, false);
        createFBO("scene2", 2, 2, false, false, false);
        createFBO("scene1", 1, 1, false, false, false);
    }

    public void deleteFBO(String title) {
        if (_FBOs.containsKey(title)) {
            FBO fbo = _FBOs.get(title);

            EXTFramebufferObject.glDeleteFramebuffersEXT(fbo._fboId);
            EXTFramebufferObject.glDeleteRenderbuffersEXT(fbo._depthRboId);
            GL11.glDeleteTextures(fbo._depthTextureId);
            GL11.glDeleteTextures(fbo._textureId);
        }
    }

    public FBO createFBO(String title, int width, int height, boolean hdr, boolean depth, boolean normals) {
        // Make sure to delete the existing FBO before creating a new one
        deleteFBO(title);

        // Create a new FBO object
        FBO fbo = new FBO();
        fbo._width = width;
        fbo._height = height;

        // Create the color target texture
        fbo._textureId = GL11.glGenTextures();
        GL11.glBindTexture(GL11.GL_TEXTURE_2D, fbo._textureId);

        GL11.glTexParameterf(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_LINEAR);
        GL11.glTexParameterf(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR);
        GL11.glTexParameterf(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_S, GL12.GL_CLAMP_TO_EDGE);
        GL11.glTexParameterf(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_T, GL12.GL_CLAMP_TO_EDGE);

        if (hdr) {
            GL11.glTexImage2D(GL11.GL_TEXTURE_2D, 0, ARBTextureFloat.GL_RGBA16F_ARB, width, height, 0, GL11.GL_RGBA,
                    ARBHalfFloatPixel.GL_HALF_FLOAT_ARB, (java.nio.ByteBuffer) null);
        } else {
            GL11.glTexImage2D(GL11.GL_TEXTURE_2D, 0, GL11.GL_RGBA, width, height, 0, GL11.GL_RGBA,
                    GL11.GL_UNSIGNED_BYTE, (java.nio.ByteBuffer) null);
        }

        if (depth) {
            // Generate the depth texture
            fbo._depthTextureId = GL11.glGenTextures();
            GL11.glBindTexture(GL11.GL_TEXTURE_2D, fbo._depthTextureId);

            GL11.glTexParameterf(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_LINEAR);
            GL11.glTexParameterf(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR);
            GL11.glTexParameterf(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_S, GL12.GL_CLAMP_TO_EDGE);
            GL11.glTexParameterf(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_T, GL12.GL_CLAMP_TO_EDGE);

            GL11.glTexImage2D(GL11.GL_TEXTURE_2D, 0, GL14.GL_DEPTH_COMPONENT24, width, height, 0,
                    GL11.GL_DEPTH_COMPONENT, ARBHalfFloatPixel.GL_HALF_FLOAT_ARB, (java.nio.ByteBuffer) null);

            // Create depth render buffer object
            fbo._depthRboId = EXTFramebufferObject.glGenRenderbuffersEXT();
            EXTFramebufferObject.glBindRenderbufferEXT(EXTFramebufferObject.GL_RENDERBUFFER_EXT, fbo._depthRboId);
            EXTFramebufferObject.glRenderbufferStorageEXT(EXTFramebufferObject.GL_RENDERBUFFER_EXT,
                    GL14.GL_DEPTH_COMPONENT16, width, height);
            EXTFramebufferObject.glBindRenderbufferEXT(EXTFramebufferObject.GL_RENDERBUFFER_EXT, 0);
        }

        if (normals) {
            // Generate the normals texture
            fbo._normalsTextureId = GL11.glGenTextures();
            GL11.glBindTexture(GL11.GL_TEXTURE_2D, fbo._normalsTextureId);

            GL11.glTexParameterf(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_LINEAR);
            GL11.glTexParameterf(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR);
            GL11.glTexParameterf(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_S, GL12.GL_CLAMP_TO_EDGE);
            GL11.glTexParameterf(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_T, GL12.GL_CLAMP_TO_EDGE);

            GL11.glTexImage2D(GL11.GL_TEXTURE_2D, 0, GL11.GL_RGBA, width, height, 0, GL11.GL_RGBA,
                    GL11.GL_UNSIGNED_BYTE, (java.nio.ByteBuffer) null);
        }

        GL11.glBindTexture(GL11.GL_TEXTURE_2D, 0);

        // Create the FBO
        fbo._fboId = EXTFramebufferObject.glGenFramebuffersEXT();
        EXTFramebufferObject.glBindFramebufferEXT(EXTFramebufferObject.GL_FRAMEBUFFER_EXT, fbo._fboId);

        EXTFramebufferObject.glFramebufferTexture2DEXT(EXTFramebufferObject.GL_FRAMEBUFFER_EXT,
                EXTFramebufferObject.GL_COLOR_ATTACHMENT0_EXT, GL11.GL_TEXTURE_2D, fbo._textureId, 0);

        if (depth) {
            // Generate the depth render buffer and depth map texture
            EXTFramebufferObject.glFramebufferRenderbufferEXT(EXTFramebufferObject.GL_FRAMEBUFFER_EXT,
                    EXTFramebufferObject.GL_DEPTH_ATTACHMENT_EXT, EXTFramebufferObject.GL_RENDERBUFFER_EXT,
                    fbo._depthRboId);
            EXTFramebufferObject.glFramebufferTexture2DEXT(EXTFramebufferObject.GL_FRAMEBUFFER_EXT,
                    EXTFramebufferObject.GL_DEPTH_ATTACHMENT_EXT, GL11.GL_TEXTURE_2D, fbo._depthTextureId, 0);
        }

        if (normals) {
            EXTFramebufferObject.glFramebufferTexture2DEXT(EXTFramebufferObject.GL_FRAMEBUFFER_EXT,
                    EXTFramebufferObject.GL_COLOR_ATTACHMENT1_EXT, GL11.GL_TEXTURE_2D, fbo._normalsTextureId, 0);
        }

        IntBuffer bufferIds = BufferUtils.createIntBuffer(3);
        bufferIds.put(EXTFramebufferObject.GL_COLOR_ATTACHMENT0_EXT);
        if (normals) {
            bufferIds.put(EXTFramebufferObject.GL_COLOR_ATTACHMENT1_EXT);
        }
        bufferIds.flip();
        GL20.glDrawBuffers(bufferIds);

        int framebuffer = EXTFramebufferObject.glCheckFramebufferStatusEXT(EXTFramebufferObject.GL_FRAMEBUFFER_EXT);
        switch (framebuffer) {
        case EXTFramebufferObject.GL_FRAMEBUFFER_COMPLETE_EXT:
            break;
        case EXTFramebufferObject.GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT_EXT:
            logger.error(
                    "FrameBuffer: " + title + ", has caused a GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT_EXT exception");
        case EXTFramebufferObject.GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT_EXT:
            logger.error("FrameBuffer: " + title
                    + ", has caused a GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT_EXT exception");
        case EXTFramebufferObject.GL_FRAMEBUFFER_INCOMPLETE_DIMENSIONS_EXT:
            logger.error(
                    "FrameBuffer: " + title + ", has caused a GL_FRAMEBUFFER_INCOMPLETE_DIMENSIONS_EXT exception");
        case EXTFramebufferObject.GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER_EXT:
            logger.error(
                    "FrameBuffer: " + title + ", has caused a GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER_EXT exception");
        case EXTFramebufferObject.GL_FRAMEBUFFER_INCOMPLETE_FORMATS_EXT:
            logger.error(
                    "FrameBuffer: " + title + ", has caused a GL_FRAMEBUFFER_INCOMPLETE_FORMATS_EXT exception");
        case EXTFramebufferObject.GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER_EXT:
            logger.error(
                    "FrameBuffer: " + title + ", has caused a GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER_EXT exception");
        default:
            throw new RuntimeException("Unexpected reply from glCheckFramebufferStatusEXT: " + framebuffer);
        }

        EXTFramebufferObject.glBindFramebufferEXT(EXTFramebufferObject.GL_FRAMEBUFFER_EXT, 0);

        _FBOs.put(title, fbo);
        return fbo;
    }

    private void updateExposure() {
        if (Config.getInstance().isEyeAdaption()) {

            ByteBuffer pixels = BufferUtils.createByteBuffer(4);
            FBO scene = PostProcessingRenderer.getInstance().getFBO("scene1");

            scene.bind();
            glReadPixels(0, 0, 1, 1, GL12.GL_BGRA, GL11.GL_BYTE, pixels);
            scene.unbind();

            _sceneLuminance = 0.2126f * pixels.get(2) / 255.f + 0.7152f * pixels.get(1) / 255.f
                    + 0.0722f * pixels.get(0) / 255.f;

            float targetExposure = MAX_EXPOSURE;

            if (_sceneLuminance > 0) {
                targetExposure = TARGET_LUMINANCE / _sceneLuminance;
            }

            float maxExposure = MAX_EXPOSURE;

            if (CoreRegistry.get(WorldRenderer.class).getSkysphere().getDaylight() == 0.0) {
                maxExposure = MAX_EXPOSURE_NIGHT;
            }

            if (targetExposure > maxExposure) {
                targetExposure = maxExposure;
            } else if (targetExposure < MIN_EXPOSURE) {
                targetExposure = MIN_EXPOSURE;
            }

            _exposure = (float) TeraMath.lerp(_exposure, targetExposure, ADJUSTMENT_SPEED);

        } else {
            if (CoreRegistry.get(WorldRenderer.class).getSkysphere().getDaylight() == 0.0) {
                _exposure = MAX_EXPOSURE_NIGHT;
            } else {
                _exposure = DEFAULT_EXPOSURE;
            }
        }
    }

    public void beginRenderScene() {
        getFBO("scene").bind();
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    }

    public void beginRenderReflectedScene() {
        FBO reflected = getFBO("sceneReflected");
        reflected.bind();

        glViewport(0, 0, reflected._width, reflected._height);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    }

    public void endRenderScene() {
        getFBO("scene").unbind();
    }

    public void endRenderReflectedScene() {
        getFBO("sceneReflected").unbind();
        glViewport(0, 0, Display.getWidth(), Display.getHeight());
    }

    /**
     * Renders the final scene to a quad and displays it. The FBO gets automatically rescaled if the size
     * of the viewport changes.
     */
    public void renderScene() {
        createOrUpdateFullscreenFbos();

        generateDownsampledScene();
        updateExposure();

        if (Config.getInstance().isSSAO()) {
            generateSSAO();
            for (int i = 0; i < 2; i++) {
                generateBlurredSSAO(i);
            }
        }

        screenSpaceCombine();
        generateTonemappedScene();
        generateHighPass();

        for (int i = 0; i < 2; i++) {
            if (Config.getInstance().isBloom()) {
                generateBloom(i);
            }
            if (Config.getInstance().getBlurIntensity() != 0) {
                generateBlur(i);
            }
        }

        renderFinalScene();
    }

    private void renderFinalScene() {
        ShaderProgram shaderPost = ShaderManager.getInstance().getShaderProgram("post");
        shaderPost.enable();

        renderFullQuad();
    }

    private void generateTonemappedScene() {
        ShaderManager.getInstance().enableShader("hdr");

        PostProcessingRenderer.getInstance().getFBO("sceneTonemapped").bind();
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        renderFullQuad();

        PostProcessingRenderer.getInstance().getFBO("sceneTonemapped").unbind();
    }

    public void generateSSAO() {
        ShaderManager.getInstance().enableShader("ssao");

        FBO ssao = PostProcessingRenderer.getInstance().getFBO("ssao");
        ssao.bind();

        glViewport(0, 0, ssao._width, ssao._height);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        renderFullQuad();

        PostProcessingRenderer.getInstance().getFBO("ssao").unbind();
        glViewport(0, 0, Display.getWidth(), Display.getHeight());
    }

    public void generateBlurredSSAO(int id) {
        ShaderProgram shader = ShaderManager.getInstance().getShaderProgram("blur");

        shader.enable();
        shader.setFloat("radius", 8.0f);

        FBO bloom = PostProcessingRenderer.getInstance().getFBO("ssaoBlurred" + id);
        bloom.bind();

        glViewport(0, 0, bloom._width, bloom._height);

        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        if (id == 0) {
            PostProcessingRenderer.getInstance().getFBO("ssao").bindTexture();
        } else {
            PostProcessingRenderer.getInstance().getFBO("ssaoBlurred" + (id - 1)).bindTexture();
        }

        renderFullQuad();

        PostProcessingRenderer.getInstance().getFBO("ssaoBlurred" + id).unbind();

        glViewport(0, 0, Display.getWidth(), Display.getHeight());
    }

    public void screenSpaceCombine() {
        ShaderManager.getInstance().enableShader("screenCombine");

        PostProcessingRenderer.getInstance().getFBO("sceneCombined").bind();

        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        renderFullQuad();

        PostProcessingRenderer.getInstance().getFBO("sceneCombined").unbind();
    }

    /**
     * Initially creates the scene FBO and updates it according to the size of the viewport.
     */
    private void createOrUpdateFullscreenFbos() {
        FBO scene = getFBO("scene");
        boolean recreate = scene == null
                || (scene._width != Display.getWidth() || scene._height != Display.getHeight());

        if (!recreate)
            return;

        createFBO("scene", Display.getWidth(), Display.getHeight(), true, true, true);

        createFBO("sceneCombined", Display.getWidth(), Display.getHeight(), true, false, false);
        createFBO("sceneTonemapped", Display.getWidth(), Display.getHeight(), true, false, false);

        final int halfWidth = Display.getWidth() / 2;
        final int halfHeight = Display.getHeight() / 2;
        final int quarterWidth = halfWidth / 2;
        final int quarterHeight = halfHeight / 2;
        final int halfQuarterWidth = quarterWidth / 2;
        final int halfQuarterHeight = quarterHeight / 2;

        createFBO("ssao", halfWidth, halfHeight, false, false, false);
        createFBO("ssaoBlurred0", halfWidth, halfHeight, false, false, false);
        createFBO("ssaoBlurred1", halfWidth, halfHeight, false, false, false);

        createFBO("sceneReflected", halfWidth, halfHeight, true, true, false);

        createFBO("sceneHighPass", halfQuarterWidth, halfQuarterHeight, false, false, false);
        createFBO("sceneBloom0", halfQuarterWidth, halfQuarterHeight, false, false, false);
        createFBO("sceneBloom1", halfQuarterWidth, halfQuarterHeight, false, false, false);

        createFBO("sceneBlur0", quarterWidth, quarterHeight, false, false, false);
        createFBO("sceneBlur1", quarterWidth, quarterHeight, false, false, false);
    }

    private void generateHighPass() {
        ShaderProgram program = ShaderManager.getInstance().getShaderProgram("highp");
        program.setFloat("highPassThreshold", 1.05f);
        program.enable();

        FBO highPass = PostProcessingRenderer.getInstance().getFBO("sceneHighPass");
        highPass.bind();

        glViewport(0, 0, highPass._width, highPass._height);

        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
        PostProcessingRenderer.getInstance().getFBO("sceneTonemapped").bindTexture();

        renderFullQuad();

        PostProcessingRenderer.getInstance().getFBO("sceneHighPass").unbind();

        glViewport(0, 0, Display.getWidth(), Display.getHeight());
    }

    private void generateBlur(int id) {
        ShaderProgram shader = ShaderManager.getInstance().getShaderProgram("blur");

        shader.enable();

        shader.setFloat("radius", 1.5f * Config.getInstance().getBlurIntensity());

        FBO blur = PostProcessingRenderer.getInstance().getFBO("sceneBlur" + id);
        blur.bind();

        glViewport(0, 0, blur._width, blur._height);

        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        if (id == 0) {
            PostProcessingRenderer.getInstance().getFBO("sceneTonemapped").bindTexture();
        } else {
            PostProcessingRenderer.getInstance().getFBO("sceneBlur" + (id - 1)).bindTexture();
        }

        renderFullQuad();

        PostProcessingRenderer.getInstance().getFBO("sceneBlur" + id).unbind();

        glViewport(0, 0, Display.getWidth(), Display.getHeight());
    }

    private void generateBloom(int id) {
        ShaderProgram shader = ShaderManager.getInstance().getShaderProgram("blur");

        shader.enable();
        shader.setFloat("radius", 32.0f);

        FBO bloom = PostProcessingRenderer.getInstance().getFBO("sceneBloom" + id);
        bloom.bind();

        glViewport(0, 0, bloom._width, bloom._height);

        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        if (id == 0) {
            PostProcessingRenderer.getInstance().getFBO("sceneHighPass").bindTexture();
        } else {
            PostProcessingRenderer.getInstance().getFBO("sceneBloom" + (id - 1)).bindTexture();
        }

        renderFullQuad();

        PostProcessingRenderer.getInstance().getFBO("sceneBloom" + id).unbind();

        glViewport(0, 0, Display.getWidth(), Display.getHeight());
    }

    private void generateDownsampledScene() {
        ShaderProgram shader = ShaderManager.getInstance().getShaderProgram("down");
        shader.enable();

        for (int i = 4; i >= 0; i--) {
            int sizePrev = (int) java.lang.Math.pow(2, i + 1);

            int size = (int) java.lang.Math.pow(2, i);
            shader.setFloat("size", size);

            PostProcessingRenderer.getInstance().getFBO("scene" + size).bind();
            glViewport(0, 0, size, size);

            glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

            if (i == 4) {
                PostProcessingRenderer.getInstance().getFBO("scene").bindTexture();
            } else {
                PostProcessingRenderer.getInstance().getFBO("scene" + sizePrev).bindTexture();
            }

            renderFullQuad();

            PostProcessingRenderer.getInstance().getFBO("scene" + size).unbind();

        }

        glViewport(0, 0, Display.getWidth(), Display.getHeight());
    }

    public void renderFullQuad() {
        glMatrixMode(GL_MODELVIEW);
        glPushMatrix();
        glLoadIdentity();

        glMatrixMode(GL_PROJECTION);
        glPushMatrix();
        glLoadIdentity();

        renderQuad();

        glPopMatrix();

        glMatrixMode(GL_MODELVIEW);
        glPopMatrix();
    }

    private void renderQuad() {
        if (_displayListQuad == -1) {
            _displayListQuad = glGenLists(1);

            glNewList(_displayListQuad, GL11.GL_COMPILE);

            glBegin(GL_QUADS);
            glColor4f(1.0f, 1.0f, 1.0f, 1.0f);

            glTexCoord2d(0.0, 0.0);
            glVertex3i(-1, -1, -1);

            glTexCoord2d(1.0, 0.0);
            glVertex3i(1, -1, -1);

            glTexCoord2d(1.0, 1.0);
            glVertex3i(1, 1, -1);

            glTexCoord2d(0.0, 1.0);
            glVertex3i(-1, 1, -1);

            glEnd();

            glEndList();
        }

        glCallList(_displayListQuad);
    }

    public float getExposure() {
        return _exposure;
    }

    public FBO getFBO(String title) {
        return _FBOs.get(title);
    }
}