org.terasology.rendering.renderingProcesses.DefaultRenderingProcess.java Source code

Java tutorial

Introduction

Here is the source code for org.terasology.rendering.renderingProcesses.DefaultRenderingProcess.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.rendering.renderingProcesses;

import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.*;
import org.lwjgl.opengl.Display;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.terasology.config.Config;
import org.terasology.editor.properties.IPropertyProvider;
import org.terasology.editor.properties.Property;
import org.terasology.game.CoreRegistry;
import org.terasology.game.GameEngine;
import org.terasology.game.paths.PathManager;
import org.terasology.logic.manager.ShaderManager;
import org.terasology.math.TeraMath;
import org.terasology.rendering.oculusVr.OculusVrHelper;
import org.terasology.rendering.assets.GLSLShaderProgramInstance;
import org.terasology.rendering.world.WorldRenderer;

import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.IntBuffer;
import java.text.SimpleDateFormat;
import java.util.*;

import static org.lwjgl.opengl.GL11.*;
import static org.lwjgl.opengl.GL15.GL_READ_ONLY;
import static org.lwjgl.opengl.GL20.glStencilOpSeparate;

/**
 * The Default Rendering Process class.
 *
 * @author Benjamin Glatzel <benjamin.glatzel@me.com>
 */
public class DefaultRenderingProcess implements IPropertyProvider {

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

    private static DefaultRenderingProcess instance = null;

    /* PROPERTIES */
    private Property hdrExposureDefault = new Property("hdrExposureDefault", 2.5f, 0.0f, 10.0f);
    private Property hdrMaxExposure = new Property("hdrMaxExposure", 8.0f, 0.0f, 10.0f);
    private Property hdrMaxExposureNight = new Property("hdrMaxExposureNight", 8.0f, 0.0f, 10.0f);
    private Property hdrMinExposure = new Property("hdrMinExposure", 1.0f, 0.0f, 10.0f);
    private Property hdrTargetLuminance = new Property("hdrTargetLuminance", 1.0f, 0.0f, 4.0f);
    private Property hdrExposureAdjustmentSpeed = new Property("hdrExposureAdjustmentSpeed", 0.05f, 0.0f, 0.5f);

    private Property bloomHighPassThreshold = new Property("bloomHighPassThreshold", 0.5f, 0.0f, 5.0f);
    private Property bloomBlurRadius = new Property("bloomBlurRadius", 12.0f, 0.0f, 32.0f);

    private Property overallBlurRadiusFactor = new Property("overallBlurRadiusFactor", 0.8f, 0.0f, 16.0f);

    /* HDR */
    private float currentExposure = 2.0f;
    private float currentSceneLuminance = 1.0f;
    private PBO readBackPBOFront, readBackPBOBack, readBackPBOCurrent;

    /* RTs */
    private int rtFullWidth;
    private int rtFullHeight;
    private int rtWidth2, rtHeight2;
    private int rtWidth4, rtHeight4;
    private int rtWidth8, rtHeight8;
    private int rtWidth16, rtHeight16;
    private int rtWidth32, rtHeight32;

    private int overwriteRtWidth = 0;
    private int overwriteRtHeight = 0;

    private String currentlyBoundFboName = "";
    private FBO currentlyBoundFbo = null;
    //private int currentlyBoundTextureId = -1;

    public enum FBOType {
        DEFAULT, HDR, NO_COLOR
    }

    /* VARIOUS */
    private boolean takeScreenshot = false;
    private int displayListQuad = -1;
    private Config config = CoreRegistry.get(Config.class);

    public enum StereoRenderState {
        MONO, OCULUS_LEFT_EYE, OCULUS_RIGHT_EYE
    }

    public class PBO {
        public int pboId = 0;
        public int width, height;
        ByteBuffer cachedBuffer = null;

        public PBO() {
            pboId = EXTPixelBufferObject.glGenBuffersARB();
        }

        public void bind() {
            EXTPixelBufferObject.glBindBufferARB(EXTPixelBufferObject.GL_PIXEL_PACK_BUFFER_EXT, pboId);
        }

        public void unbind() {
            EXTPixelBufferObject.glBindBufferARB(EXTPixelBufferObject.GL_PIXEL_PACK_BUFFER_EXT, 0);
        }

        public void init(int width, int height) {
            this.width = width;
            this.height = height;

            int byteSize = width * height * 4;
            cachedBuffer = BufferUtils.createByteBuffer(byteSize);

            bind();
            EXTPixelBufferObject.glBufferDataARB(EXTPixelBufferObject.GL_PIXEL_PACK_BUFFER_EXT, byteSize,
                    EXTPixelBufferObject.GL_STREAM_READ_ARB);
            unbind();
        }

        public void copyFromFBO(int fboId, int width, int height, int format, int type) {
            bind();
            EXTFramebufferObject.glBindFramebufferEXT(EXTFramebufferObject.GL_FRAMEBUFFER_EXT, fboId);
            glReadPixels(0, 0, width, height, format, type, 0);
            unbind();
            EXTFramebufferObject.glBindFramebufferEXT(EXTFramebufferObject.GL_FRAMEBUFFER_EXT, 0);
        }

        public ByteBuffer readBackPixels() {
            bind();

            cachedBuffer = EXTPixelBufferObject.glMapBufferARB(EXTPixelBufferObject.GL_PIXEL_PACK_BUFFER_EXT,
                    GL_READ_ONLY, cachedBuffer);

            // Maybe fix for the issues appearing on some platforms where accessing the "cachedBuffer" causes a JVM exception and therefore a crash...
            ByteBuffer resultBuffer = BufferUtils.createByteBuffer(cachedBuffer.capacity());
            resultBuffer.put(cachedBuffer);
            cachedBuffer.rewind();
            resultBuffer.flip();

            EXTPixelBufferObject.glUnmapBufferARB(EXTPixelBufferObject.GL_PIXEL_PACK_BUFFER_EXT);
            unbind();

            return resultBuffer;
        }
    }

    public class FBO {
        public int fboId = 0;
        public int textureId = 0;
        public int depthStencilTextureId = 0;
        public int depthStencilRboId = 0;
        public int normalsTextureId = 0;
        public int lightBufferTextureId = 0;

        public int width = 0;
        public int height = 0;

        public void bind() {
            if (this != currentlyBoundFbo) {
                EXTFramebufferObject.glBindFramebufferEXT(EXTFramebufferObject.GL_FRAMEBUFFER_EXT, fboId);
                currentlyBoundFbo = this;
            }
        }

        public void unbind() {
            if (currentlyBoundFbo != null) {
                EXTFramebufferObject.glBindFramebufferEXT(EXTFramebufferObject.GL_FRAMEBUFFER_EXT, 0);
                currentlyBoundFbo = null;
            }
        }

        public void bindDepthTexture() {
            //if (currentlyBoundTextureId != depthStencilTextureId) {
            GL11.glBindTexture(GL11.GL_TEXTURE_2D, depthStencilTextureId);
            //currentlyBoundTextureId = depthStencilTextureId;
            //}
        }

        public void bindTexture() {
            //if (currentlyBoundTextureId != textureId) {
            GL11.glBindTexture(GL11.GL_TEXTURE_2D, textureId);
            //currentlyBoundTextureId = textureId;
            //}
        }

        public void bindNormalsTexture() {
            //if (currentlyBoundTextureId != normalsTextureId) {
            GL11.glBindTexture(GL11.GL_TEXTURE_2D, normalsTextureId);
            //currentlyBoundTextureId = normalsTextureId;
            //}
        }

        public void bindLightBufferTexture() {
            //if (currentlyBoundTextureId != lightBufferTextureId) {
            GL11.glBindTexture(GL11.GL_TEXTURE_2D, lightBufferTextureId);
            //currentlyBoundTextureId = lightBufferTextureId;
            //}
        }

        public void unbindTexture() {
            //if (currentlyBoundTextureId != 0) {
            GL11.glBindTexture(GL11.GL_TEXTURE_2D, 0);
            //currentlyBoundTextureId = 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 DefaultRenderingProcess getInstance() {
        if (instance == null) {
            instance = new DefaultRenderingProcess();
        }

        return instance;
    }

    public DefaultRenderingProcess() {
        initialize();
    }

    public void initialize() {
        createOrUpdateFullscreenFbos();

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

        readBackPBOFront = new PBO();
        readBackPBOBack = new PBO();
        readBackPBOFront.init(1, 1);
        readBackPBOBack.init(1, 1);

        readBackPBOCurrent = readBackPBOFront;
    }

    /**
     * Creates the scene FBOs and updates them according to the size of the viewport. The current size
     * provided by the display class is only used if the parameters overwriteRTWidth and overwriteRTHeight are set
     * to zero.
     */
    private void createOrUpdateFullscreenFbos() {
        rtFullWidth = overwriteRtWidth;
        rtFullHeight = overwriteRtHeight;

        if (overwriteRtWidth == 0) {
            rtFullWidth = org.lwjgl.opengl.Display.getWidth();
        }

        if (overwriteRtHeight == 0) {
            rtFullHeight = org.lwjgl.opengl.Display.getHeight();
        }

        if (CoreRegistry.get(Config.class).getRendering().isOculusVrSupport()) {
            if (overwriteRtWidth == 0) {
                rtFullWidth *= OculusVrHelper.getScaleFactor();
            }
            if (overwriteRtHeight == 0) {
                rtFullHeight *= OculusVrHelper.getScaleFactor();
            }
        }

        rtWidth2 = rtFullWidth / 2;
        rtHeight2 = rtFullHeight / 2;
        rtWidth4 = rtWidth2 / 2;
        rtHeight4 = rtHeight2 / 2;
        rtWidth8 = rtWidth4 / 2;
        rtHeight8 = rtHeight4 / 2;
        rtWidth16 = rtHeight8 / 2;
        rtHeight16 = rtWidth8 / 2;
        rtWidth32 = rtHeight16 / 2;
        rtHeight32 = rtWidth16 / 2;

        FBO scene = FBOs.get("sceneOpaque");
        final boolean recreate = scene == null || (scene.width != rtFullWidth || scene.height != rtFullHeight);

        if (!recreate) {
            return;
        }

        createFBO("sceneOpaque", rtFullWidth, rtFullHeight, FBOType.HDR, true, true, true, true);
        createFBO("sceneOpaquePingPong", rtFullWidth, rtFullHeight, FBOType.HDR, true, true, true, true);

        createFBO("sceneTransparent", rtFullWidth, rtFullHeight, FBOType.HDR);
        attachDepthBufferToFbo("sceneOpaque", "sceneTransparent");

        createFBO("sceneReflected", rtWidth2, rtHeight2, FBOType.DEFAULT, true);

        createFBO("sceneShadowMap", config.getRendering().getShadowMapResolution(),
                config.getRendering().getShadowMapResolution(), FBOType.NO_COLOR, true, false);

        createFBO("scenePrePost", rtFullWidth, rtFullHeight, FBOType.HDR);
        createFBO("sceneToneMapped", rtFullWidth, rtFullHeight, FBOType.HDR);
        createFBO("sceneFinal", rtFullWidth, rtFullHeight, FBOType.DEFAULT);

        createFBO("sobel", rtFullWidth, rtFullHeight, FBOType.DEFAULT);

        createFBO("ssao", rtFullWidth, rtFullHeight, FBOType.DEFAULT);
        createFBO("ssaoBlurred", rtFullWidth, rtFullHeight, FBOType.DEFAULT);

        createFBO("lightShafts", rtWidth2, rtHeight2, FBOType.DEFAULT);

        createFBO("sceneHighPass", rtFullWidth, rtFullHeight, FBOType.DEFAULT);
        createFBO("sceneBloom0", rtWidth2, rtHeight2, FBOType.DEFAULT);
        createFBO("sceneBloom1", rtWidth4, rtHeight4, FBOType.DEFAULT);
        createFBO("sceneBloom2", rtWidth8, rtHeight8, FBOType.DEFAULT);

        createFBO("sceneBlur0", rtWidth2, rtHeight2, FBOType.DEFAULT);
        createFBO("sceneBlur1", rtWidth2, rtHeight2, FBOType.DEFAULT);

        createFBO("sceneSkyBand0", rtWidth16, rtHeight16, FBOType.DEFAULT);
        createFBO("sceneSkyBand1", rtWidth32, rtHeight32, FBOType.DEFAULT);
    }

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

            EXTFramebufferObject.glDeleteFramebuffersEXT(fbo.fboId);
            EXTFramebufferObject.glDeleteRenderbuffersEXT(fbo.depthStencilRboId);
            GL11.glDeleteTextures(fbo.normalsTextureId);
            GL11.glDeleteTextures(fbo.depthStencilTextureId);
            GL11.glDeleteTextures(fbo.textureId);
        }
    }

    public boolean attachDepthBufferToFbo(String sourceFboName, String targetFboName) {
        FBO source = getFBO(sourceFboName);
        FBO target = getFBO(targetFboName);

        if (source == null || target == null) {
            return false;
        }

        EXTFramebufferObject.glBindFramebufferEXT(EXTFramebufferObject.GL_FRAMEBUFFER_EXT, target.fboId);

        EXTFramebufferObject.glFramebufferRenderbufferEXT(EXTFramebufferObject.GL_FRAMEBUFFER_EXT,
                EXTFramebufferObject.GL_DEPTH_ATTACHMENT_EXT, EXTFramebufferObject.GL_RENDERBUFFER_EXT,
                source.depthStencilRboId);
        EXTFramebufferObject.glFramebufferTexture2DEXT(EXTFramebufferObject.GL_FRAMEBUFFER_EXT,
                EXTFramebufferObject.GL_DEPTH_ATTACHMENT_EXT, GL11.GL_TEXTURE_2D, source.depthStencilTextureId, 0);

        EXTFramebufferObject.glBindFramebufferEXT(EXTFramebufferObject.GL_FRAMEBUFFER_EXT, 0);

        return true;
    }

    public FBO createFBO(String title, int width, int height, FBOType type) {
        return createFBO(title, width, height, type, false, false, false);
    }

    public FBO createFBO(String title, int width, int height, FBOType type, boolean depth) {
        return createFBO(title, width, height, type, depth, false, false);
    }

    public FBO createFBO(String title, int width, int height, FBOType type, boolean depth, boolean normals) {
        return createFBO(title, width, height, type, depth, normals, false);
    }

    public FBO createFBO(String title, int width, int height, FBOType type, boolean depth, boolean normals,
            boolean lightBuffer) {
        return createFBO(title, width, height, type, depth, normals, lightBuffer, false);
    }

    public FBO createFBO(String title, int width, int height, FBOType type, boolean depthBuffer,
            boolean normalBuffer, boolean lightBuffer, boolean stencilBuffer) {
        // 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 FBO
        fbo.fboId = EXTFramebufferObject.glGenFramebuffersEXT();
        EXTFramebufferObject.glBindFramebufferEXT(EXTFramebufferObject.GL_FRAMEBUFFER_EXT, fbo.fboId);

        if (type != FBOType.NO_COLOR) {
            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 (type == FBOType.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);
            }

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

        if (normalBuffer) {
            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);

            EXTFramebufferObject.glFramebufferTexture2DEXT(EXTFramebufferObject.GL_FRAMEBUFFER_EXT,
                    EXTFramebufferObject.GL_COLOR_ATTACHMENT1_EXT, GL11.GL_TEXTURE_2D, fbo.normalsTextureId, 0);
        }

        if (lightBuffer) {
            fbo.lightBufferTextureId = GL11.glGenTextures();
            GL11.glBindTexture(GL11.GL_TEXTURE_2D, fbo.lightBufferTextureId);

            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 (type == FBOType.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);
            }

            EXTFramebufferObject.glFramebufferTexture2DEXT(EXTFramebufferObject.GL_FRAMEBUFFER_EXT,
                    EXTFramebufferObject.GL_COLOR_ATTACHMENT2_EXT, GL11.GL_TEXTURE_2D, fbo.lightBufferTextureId, 0);
        }

        if (depthBuffer) {
            fbo.depthStencilTextureId = GL11.glGenTextures();
            GL11.glBindTexture(GL11.GL_TEXTURE_2D, fbo.depthStencilTextureId);

            GL11.glTexParameterf(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_NEAREST);
            GL11.glTexParameterf(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_NEAREST);
            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 (!stencilBuffer) {
                GL11.glTexImage2D(GL11.GL_TEXTURE_2D, 0, GL14.GL_DEPTH_COMPONENT24, width, height, 0,
                        GL11.GL_DEPTH_COMPONENT, GL11.GL_UNSIGNED_INT, (java.nio.ByteBuffer) null);
            } else {
                GL11.glTexImage2D(GL11.GL_TEXTURE_2D, 0, EXTPackedDepthStencil.GL_DEPTH24_STENCIL8_EXT, width,
                        height, 0, EXTPackedDepthStencil.GL_DEPTH_STENCIL_EXT,
                        EXTPackedDepthStencil.GL_UNSIGNED_INT_24_8_EXT, (java.nio.ByteBuffer) null);
            }

            fbo.depthStencilRboId = EXTFramebufferObject.glGenRenderbuffersEXT();
            EXTFramebufferObject.glBindRenderbufferEXT(EXTFramebufferObject.GL_RENDERBUFFER_EXT,
                    fbo.depthStencilRboId);

            if (!stencilBuffer) {
                EXTFramebufferObject.glRenderbufferStorageEXT(EXTFramebufferObject.GL_RENDERBUFFER_EXT,
                        GL14.GL_DEPTH_COMPONENT24, width, height);
            } else {
                EXTFramebufferObject.glRenderbufferStorageEXT(EXTFramebufferObject.GL_RENDERBUFFER_EXT,
                        EXTPackedDepthStencil.GL_DEPTH24_STENCIL8_EXT, width, height);
            }

            EXTFramebufferObject.glBindRenderbufferEXT(EXTFramebufferObject.GL_RENDERBUFFER_EXT, 0);

            EXTFramebufferObject.glFramebufferRenderbufferEXT(EXTFramebufferObject.GL_FRAMEBUFFER_EXT,
                    EXTFramebufferObject.GL_DEPTH_ATTACHMENT_EXT, EXTFramebufferObject.GL_RENDERBUFFER_EXT,
                    fbo.depthStencilRboId);
            EXTFramebufferObject.glFramebufferTexture2DEXT(EXTFramebufferObject.GL_FRAMEBUFFER_EXT,
                    EXTFramebufferObject.GL_DEPTH_ATTACHMENT_EXT, GL11.GL_TEXTURE_2D, fbo.depthStencilTextureId, 0);

            if (stencilBuffer) {
                EXTFramebufferObject.glFramebufferTexture2DEXT(EXTFramebufferObject.GL_FRAMEBUFFER_EXT,
                        EXTFramebufferObject.GL_STENCIL_ATTACHMENT_EXT, GL11.GL_TEXTURE_2D,
                        fbo.depthStencilTextureId, 0);
            }
        }

        GL11.glBindTexture(GL11.GL_TEXTURE_2D, 0);

        IntBuffer bufferIds = BufferUtils.createIntBuffer(3);
        if (type != FBOType.NO_COLOR) {
            bufferIds.put(EXTFramebufferObject.GL_COLOR_ATTACHMENT0_EXT);
        }
        if (normalBuffer) {
            bufferIds.put(EXTFramebufferObject.GL_COLOR_ATTACHMENT1_EXT);
        }
        if (lightBuffer) {
            bufferIds.put(EXTFramebufferObject.GL_COLOR_ATTACHMENT2_EXT);
        }
        bufferIds.flip();

        if (bufferIds.limit() == 0) {
            GL11.glReadBuffer(GL11.GL_NONE);
            GL20.glDrawBuffers(GL11.GL_NONE);
        } else {
            GL20.glDrawBuffers(bufferIds);
        }

        int checkFB = EXTFramebufferObject.glCheckFramebufferStatusEXT(EXTFramebufferObject.GL_FRAMEBUFFER_EXT);
        switch (checkFB) {
        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");
            break;
        case EXTFramebufferObject.GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT_EXT:
            logger.error("FrameBuffer: " + title
                    + ", has caused a GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT_EXT exception");
            break;
        case EXTFramebufferObject.GL_FRAMEBUFFER_INCOMPLETE_DIMENSIONS_EXT:
            logger.error(
                    "FrameBuffer: " + title + ", has caused a GL_FRAMEBUFFER_INCOMPLETE_DIMENSIONS_EXT exception");
            break;
        case EXTFramebufferObject.GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER_EXT:
            logger.error(
                    "FrameBuffer: " + title + ", has caused a GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER_EXT exception");
            break;
        case EXTFramebufferObject.GL_FRAMEBUFFER_INCOMPLETE_FORMATS_EXT:
            logger.error(
                    "FrameBuffer: " + title + ", has caused a GL_FRAMEBUFFER_INCOMPLETE_FORMATS_EXT exception");
            break;
        case EXTFramebufferObject.GL_FRAMEBUFFER_UNSUPPORTED_EXT:
            logger.error("FrameBuffer: " + title + ", has caused a GL_FRAMEBUFFER_UNSUPPORTED_EXT exception");
            break;
        case EXTFramebufferObject.GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER_EXT:
            logger.error(
                    "FrameBuffer: " + title + ", has caused a GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER_EXT exception");

            /*
             * On some graphics cards, FBOType.NO_COLOR can cause a GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER_EXT.
             * Attempt to continue without this FBO.
             */
            if (type == FBOType.NO_COLOR) {
                logger.error("FrameBuffer: " + title
                        + ", ...but the FBOType was NO_COLOR, ignoring this error and continuing without this FBO.");

                return null;
            }

            break;
        default:
            logger.error("Unexpected reply from glCheckFramebufferStatusEXT: " + checkFB);
            break;
        }

        EXTFramebufferObject.glBindFramebufferEXT(EXTFramebufferObject.GL_FRAMEBUFFER_EXT, 0);

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

    private void updateExposure() {
        if (config.getRendering().isEyeAdaptation()) {
            FBO scene = getFBO("scene1");

            if (scene == null) {
                return;
            }

            readBackPBOCurrent.copyFromFBO(scene.fboId, 1, 1, GL12.GL_BGRA, GL11.GL_UNSIGNED_BYTE);

            if (readBackPBOCurrent == readBackPBOFront) {
                readBackPBOCurrent = readBackPBOBack;
            } else {
                readBackPBOCurrent = readBackPBOFront;
            }

            ByteBuffer pixels = readBackPBOCurrent.readBackPixels();

            if (pixels.limit() < 3) {
                logger.error("Failed to auto-update the exposure value.");
                return;
            }

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

            float targetExposure = (Float) hdrMaxExposure.getValue();

            if (currentSceneLuminance > 0) {
                targetExposure = (Float) hdrTargetLuminance.getValue() / currentSceneLuminance;
            }

            float maxExposure = (Float) hdrMaxExposure.getValue();

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

            if (targetExposure > maxExposure) {
                targetExposure = maxExposure;
            } else if (targetExposure < (Float) hdrMinExposure.getValue()) {
                targetExposure = (Float) hdrMinExposure.getValue();
            }

            currentExposure = (float) TeraMath.lerp(currentExposure, targetExposure,
                    (Float) hdrExposureAdjustmentSpeed.getValue());

        } else {
            if (CoreRegistry.get(WorldRenderer.class).getSkysphere().getDaylight() == 0.0) {
                currentExposure = (Float) hdrMaxExposureNight.getValue();
            } else {
                currentExposure = (Float) hdrExposureDefault.getValue();
            }
        }
    }

    public void clear() {
        bindFbo("sceneOpaque");
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
        unbindFbo("sceneOpaque");
        bindFbo("sceneTransparent");
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
        unbindFbo("sceneTransparent");
    }

    public void beginRenderSceneOpaque() {
        bindFbo("sceneOpaque");
        setRenderBufferMask(true, true, true);
    }

    public void endRenderSceneOpaque() {
        setRenderBufferMask(true, true, true);
        unbindFbo("sceneOpaque");
    }

    public void beginRenderLightGeometryStencilPass() {
        bindFbo("sceneOpaque");
        setRenderBufferMask(false, false, false);
        glDepthMask(false);

        glClear(GL_STENCIL_BUFFER_BIT);

        glCullFace(GL_FRONT);
        glDisable(GL_CULL_FACE);

        glEnable(GL_STENCIL_TEST);
        glStencilFunc(GL_ALWAYS, 0, 0);

        glStencilOpSeparate(GL_BACK, GL_KEEP, GL_INCR, GL_KEEP);
        glStencilOpSeparate(GL_FRONT, GL_KEEP, GL_DECR, GL_KEEP);
    }

    public void endRenderLightGeometryStencilPass() {
        setRenderBufferMask(true, true, true);
        unbindFbo("sceneOpaque");
    }

    public void beginRenderLightGeometry() {
        bindFbo("sceneOpaque");

        // Only write to the light buffer
        setRenderBufferMask(false, false, true);

        glStencilFunc(GL_NOTEQUAL, 0, 0xFF);

        glDepthMask(true);
        glDisable(GL_DEPTH_TEST);

        glEnable(GL_BLEND);
        glBlendFunc(GL_ONE, GL_ONE);

        glEnable(GL_CULL_FACE);
        glCullFace(GL_FRONT);
    }

    public void endRenderLightGeometry() {
        glDisable(GL_STENCIL_TEST);
        glCullFace(GL_BACK);

        unbindFbo("sceneOpaque");
    }

    public void beginRenderDirectionalLights() {
        bindFbo("sceneOpaque");
    }

    public void endRenderDirectionalLights() {
        glDisable(GL_BLEND);
        glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

        glEnable(GL_DEPTH_TEST);

        setRenderBufferMask(true, true, true);
        unbindFbo("sceneOpaque");

        applyLightBufferPass("sceneOpaque");
    }

    public void setRenderBufferMask(boolean color, boolean normal, boolean lightBuffer) {
        setRenderBufferMask(currentlyBoundFboName, color, normal, lightBuffer);
    }

    public void setRenderBufferMask(String fboTitle, boolean color, boolean normal, boolean lightBuffer) {
        setRenderBufferMask(getFBO(fboTitle), color, normal, lightBuffer);
    }

    public void setRenderBufferMask(FBO fbo, boolean color, boolean normal, boolean lightBuffer) {
        if (fbo == null) {
            return;
        }

        int attachmentId = 0;

        IntBuffer bufferIds = BufferUtils.createIntBuffer(3);

        if (fbo.textureId != 0) {
            if (color) {
                bufferIds.put(EXTFramebufferObject.GL_COLOR_ATTACHMENT0_EXT + attachmentId);
            }

            attachmentId++;
        }
        if (fbo.normalsTextureId != 0) {
            if (normal) {
                bufferIds.put(EXTFramebufferObject.GL_COLOR_ATTACHMENT0_EXT + attachmentId);
            }

            attachmentId++;
        }
        if (fbo.lightBufferTextureId != 0) {
            if (lightBuffer) {
                bufferIds.put(EXTFramebufferObject.GL_COLOR_ATTACHMENT0_EXT + attachmentId);
            }

            attachmentId++;
        }

        bufferIds.flip();

        GL20.glDrawBuffers(bufferIds);
    }

    public void beginRenderSceneTransparent() {
        bindFbo("sceneTransparent");
    }

    public void endRenderSceneTransparent() {
        unbindFbo("sceneTransparent");
    }

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

        if (reflected == null) {
            return;
        }

        reflected.bind();

        glViewport(0, 0, reflected.width, reflected.height);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    }

    public void endRenderReflectedScene() {
        unbindFbo("sceneReflected");
        glViewport(0, 0, rtFullWidth, rtFullHeight);
    }

    public void beginRenderSceneShadowMap() {
        FBO shadowMap = getFBO("sceneShadowMap");

        if (shadowMap == null) {
            return;
        }

        shadowMap.bind();

        glViewport(0, 0, shadowMap.width, shadowMap.height);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    }

    public void endRenderSceneShadowMap() {
        unbindFbo("sceneShadowMap");
        glViewport(0, 0, rtFullWidth, rtFullHeight);
    }

    public void beginRenderSceneSky() {
        setRenderBufferMask(true, false, false);
    }

    public void endRenderSceneSky() {
        setRenderBufferMask(true, true, true);

        generateSkyBand(0);
        generateSkyBand(1);

        bindFbo("sceneOpaque");
    }

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

        if (config.getRendering().isOutline()) {
            generateSobel();
        }

        if (config.getRendering().isSsao()) {
            generateSSAO();
            generateBlurredSSAO();
        }

        generateCombinedScene();

        if (config.getRendering().isLightShafts()) {
            generateLightShafts();
        }

        generatePrePost();

        if (config.getRendering().isEyeAdaptation()) {
            generateDownsampledScene();
        }

        updateExposure();

        generateToneMappedScene();

        if (config.getRendering().isBloom()) {
            generateHighPass();
        }

        for (int i = 0; i < 3; i++) {
            if (config.getRendering().isBloom()) {
                generateBloom(i);
            }
        }

        for (int i = 0; i < 2; i++) {
            if (config.getRendering().getBlurIntensity() != 0) {
                generateBlur(i);
            }
        }

        if (stereoRenderState == StereoRenderState.OCULUS_LEFT_EYE
                || stereoRenderState == StereoRenderState.OCULUS_RIGHT_EYE
                || (stereoRenderState == StereoRenderState.MONO && takeScreenshot)) {

            renderFinalSceneToRT(stereoRenderState);

            if (takeScreenshot) {
                saveScreenshot();
            }
        }

        if (stereoRenderState == StereoRenderState.MONO
                || stereoRenderState == StereoRenderState.OCULUS_RIGHT_EYE) {
            renderFinalScene();
        }
    }

    private void renderFinalSceneToRT(StereoRenderState stereoRenderState) {
        GLSLShaderProgramInstance shader;

        if (config.getSystem().isDebugRenderingEnabled()) {
            shader = ShaderManager.getInstance().getShaderProgramInstance("debug");
        } else {
            shader = ShaderManager.getInstance().getShaderProgramInstance("post");
        }

        shader.enable();

        bindFbo("sceneFinal");

        if (stereoRenderState == StereoRenderState.MONO || stereoRenderState == StereoRenderState.OCULUS_LEFT_EYE) {
            glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
        }

        switch (stereoRenderState) {
        case MONO:
            renderFullscreenQuad(0, 0, rtFullWidth, rtFullHeight);
            break;
        case OCULUS_LEFT_EYE:
            renderFullscreenQuad(0, 0, rtFullWidth / 2, rtFullHeight);
            break;
        case OCULUS_RIGHT_EYE:
            renderFullscreenQuad(rtFullWidth / 2, 0, rtFullWidth / 2, rtFullHeight);
            break;
        }

        unbindFbo("sceneFinal");
    }

    private void updateOcShaderParametersForVP(GLSLShaderProgramInstance program, int vpX, int vpY, int vpWidth,
            int vpHeight, StereoRenderState stereoRenderState) {
        float w = (float) vpWidth / rtFullWidth;
        float h = (float) vpHeight / rtFullHeight;
        float x = (float) vpX / rtFullWidth;
        float y = (float) vpY / rtFullHeight;

        float as = (float) vpWidth / vpHeight;

        program.setFloat4("ocHmdWarpParam", OculusVrHelper.getDistortionParams()[0],
                OculusVrHelper.getDistortionParams()[1], OculusVrHelper.getDistortionParams()[2],
                OculusVrHelper.getDistortionParams()[3]);

        float ocLensCenter = (stereoRenderState == StereoRenderState.OCULUS_RIGHT_EYE)
                ? -1.0f * OculusVrHelper.getLensViewportShift()
                : OculusVrHelper.getLensViewportShift();

        program.setFloat2("ocLensCenter", x + (w + ocLensCenter * 0.5f) * 0.5f, y + h * 0.5f);
        program.setFloat2("ocScreenCenter", x + w * 0.5f, y + h * 0.5f);

        float scaleFactor = 1.0f / OculusVrHelper.getScaleFactor();

        program.setFloat2("ocScale", (w / 2) * scaleFactor, (h / 2) * scaleFactor * as);
        program.setFloat2("ocScaleIn", (2 / w), (2 / h) / as);
    }

    private void renderFinalScene() {

        GLSLShaderProgramInstance shader;

        if (config.getRendering().isOculusVrSupport()) {
            shader = ShaderManager.getInstance().getShaderProgramInstance("ocDistortion");
            shader.enable();

            updateOcShaderParametersForVP(shader, 0, 0, rtFullWidth / 2, rtFullHeight,
                    StereoRenderState.OCULUS_LEFT_EYE);
        } else {
            if (config.getSystem().isDebugRenderingEnabled()) {
                shader = ShaderManager.getInstance().getShaderProgramInstance("debug");
            } else {
                shader = ShaderManager.getInstance().getShaderProgramInstance("post");
            }

            shader.enable();
        }

        renderFullscreenQuad(0, 0, org.lwjgl.opengl.Display.getWidth(), org.lwjgl.opengl.Display.getHeight());

        if (config.getRendering().isOculusVrSupport()) {
            updateOcShaderParametersForVP(shader, rtFullWidth / 2, 0, rtFullWidth / 2, rtFullHeight,
                    StereoRenderState.OCULUS_RIGHT_EYE);

            renderFullscreenQuad(0, 0, org.lwjgl.opengl.Display.getWidth(), Display.getHeight());
        }
    }

    private void generateCombinedScene() {
        ShaderManager.getInstance().enableShader("combine");

        bindFbo("sceneOpaquePingPong");

        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        renderFullscreenQuad();

        unbindFbo("sceneOpaquePingPong");

        flipPingPongFbo("sceneOpaque");
        attachDepthBufferToFbo("sceneOpaque", "sceneTransparent");
    }

    private void applyLightBufferPass(String target) {
        GLSLShaderProgramInstance program = ShaderManager.getInstance().getShaderProgramInstance("lightBufferPass");
        program.enable();

        DefaultRenderingProcess.FBO targetFbo = getFBO(target);

        int texId = 0;
        if (targetFbo != null) {
            GL13.glActiveTexture(GL13.GL_TEXTURE0 + texId);
            targetFbo.bindTexture();
            program.setInt("texSceneOpaque", texId++);

            GL13.glActiveTexture(GL13.GL_TEXTURE0 + texId);
            targetFbo.bindDepthTexture();
            program.setInt("texSceneOpaqueDepth", texId++);

            GL13.glActiveTexture(GL13.GL_TEXTURE0 + texId);
            targetFbo.bindNormalsTexture();
            program.setInt("texSceneOpaqueNormals", texId++);

            GL13.glActiveTexture(GL13.GL_TEXTURE0 + texId);
            targetFbo.bindLightBufferTexture();
            program.setInt("texSceneOpaqueLightBuffer", texId++);
        }

        bindFbo(target + "PingPong");
        setRenderBufferMask(true, true, true);

        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        renderFullscreenQuad();

        unbindFbo(target + "PingPong");

        flipPingPongFbo(target);

        if (target.equals("sceneOpaque")) {
            attachDepthBufferToFbo("sceneOpaque", "sceneTransparent");
        }
    }

    private void generateSkyBand(int id) {
        FBO skyBand = getFBO("sceneSkyBand" + id);

        if (skyBand == null) {
            return;
        }

        skyBand.bind();
        setRenderBufferMask(true, false, false);

        GLSLShaderProgramInstance shader = ShaderManager.getInstance().getShaderProgramInstance("blur");

        shader.enable();
        shader.setFloat("radius", 8.0f);
        shader.setFloat2("texelSize", 1.0f / skyBand.width, 1.0f / skyBand.height);

        if (id == 0) {
            bindFboTexture("sceneOpaque");
        } else {
            bindFboTexture("sceneSkyBand" + (id - 1));
        }

        glViewport(0, 0, skyBand.width, skyBand.height);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        renderFullscreenQuad();

        skyBand.unbind();
        glViewport(0, 0, rtFullWidth, rtFullHeight);
    }

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

        bindFbo("sceneToneMapped");

        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        renderFullscreenQuad();

        unbindFbo("sceneToneMapped");
    }

    private void generateLightShafts() {
        ShaderManager.getInstance().enableShader("lightshaft");

        FBO lightshaft = getFBO("lightShafts");

        if (lightshaft == null) {
            return;
        }

        lightshaft.bind();

        glViewport(0, 0, lightshaft.width, lightshaft.height);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        renderFullscreenQuad();

        lightshaft.unbind();
        glViewport(0, 0, rtFullWidth, rtFullHeight);
    }

    private void generateSSAO() {
        GLSLShaderProgramInstance ssaoShader = ShaderManager.getInstance().getShaderProgramInstance("ssao");
        ssaoShader.enable();

        FBO ssao = getFBO("ssao");

        if (ssao == null) {
            return;
        }

        ssaoShader.setFloat2("texelSize", 1.0f / ssao.width, 1.0f / ssao.height);
        ssaoShader.setFloat2("noiseTexelSize", 1.0f / 4.0f, 1.0f / 4.0f);

        ssao.bind();

        glViewport(0, 0, ssao.width, ssao.height);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        renderFullscreenQuad();

        ssao.unbind();
        glViewport(0, 0, rtFullWidth, rtFullHeight);
    }

    private void generateSobel() {
        ShaderManager.getInstance().enableShader("sobel");

        FBO sobel = getFBO("sobel");

        if (sobel == null) {
            return;
        }

        sobel.bind();

        glViewport(0, 0, sobel.width, sobel.height);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        renderFullscreenQuad();

        sobel.unbind();
        glViewport(0, 0, rtFullWidth, rtFullHeight);
    }

    private void generateBlurredSSAO() {
        GLSLShaderProgramInstance shader = ShaderManager.getInstance().getShaderProgramInstance("ssaoBlur");
        shader.enable();

        FBO ssao = getFBO("ssaoBlurred");

        if (ssao == null) {
            return;
        }

        shader.setFloat2("texelSize", 1.0f / ssao.width, 1.0f / ssao.height);

        ssao.bind();

        glViewport(0, 0, ssao.width, ssao.height);

        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        bindFboTexture("ssao");

        renderFullscreenQuad();

        ssao.unbind();

        glViewport(0, 0, rtFullWidth, rtFullHeight);
    }

    private void generatePrePost() {
        ShaderManager.getInstance().enableShader("prePost");

        bindFbo("scenePrePost");

        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        renderFullscreenQuad();

        unbindFbo("scenePrePost");
    }

    private void generateHighPass() {
        GLSLShaderProgramInstance program = ShaderManager.getInstance().getShaderProgramInstance("highp");
        program.setFloat("highPassThreshold", (Float) bloomHighPassThreshold.getValue());
        program.enable();

        FBO highPass = getFBO("sceneHighPass");

        if (highPass == null) {
            return;
        }

        highPass.bind();

        FBO sceneOpaque = getFBO("sceneOpaque");

        int texId = 0;
        GL13.glActiveTexture(GL13.GL_TEXTURE0 + texId);
        sceneOpaque.bindTexture();
        program.setInt("tex", texId++);

        GL13.glActiveTexture(GL13.GL_TEXTURE0 + texId);
        sceneOpaque.bindDepthTexture();
        program.setInt("texDepth", texId++);

        glViewport(0, 0, highPass.width, highPass.height);

        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        renderFullscreenQuad();

        highPass.unbind();

        glViewport(0, 0, rtFullWidth, rtFullHeight);
    }

    private void generateBlur(int id) {
        GLSLShaderProgramInstance shader = ShaderManager.getInstance().getShaderProgramInstance("blur");
        shader.enable();

        shader.setFloat("radius",
                (Float) overallBlurRadiusFactor.getValue() * config.getRendering().getBlurRadius());

        FBO blur = getFBO("sceneBlur" + id);

        if (blur == null) {
            return;
        }

        shader.setFloat2("texelSize", 1.0f / blur.width, 1.0f / blur.height);

        blur.bind();

        glViewport(0, 0, blur.width, blur.height);

        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        if (id == 0) {
            bindFboTexture("sceneToneMapped");
        } else {
            bindFboTexture("sceneBlur" + (id - 1));
        }

        renderFullscreenQuad();

        blur.unbind();

        glViewport(0, 0, rtFullWidth, rtFullHeight);
    }

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

        shader.enable();
        shader.setFloat("radius", (Float) bloomBlurRadius.getValue());

        FBO bloom = getFBO("sceneBloom" + id);

        if (bloom == null) {
            return;
        }

        shader.setFloat2("texelSize", 1.0f / bloom.width, 1.0f / bloom.height);

        bloom.bind();

        glViewport(0, 0, bloom.width, bloom.height);

        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

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

        renderFullscreenQuad();

        bloom.unbind();

        glViewport(0, 0, rtFullWidth, rtFullHeight);
    }

    private void generateDownsampledScene() {
        GLSLShaderProgramInstance shader = ShaderManager.getInstance().getShaderProgramInstance("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);

            bindFbo("scene" + size);
            glViewport(0, 0, size, size);

            glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

            if (i == 4) {
                bindFboTexture("scenePrePost");
            } else {
                bindFboTexture("scene" + sizePrev);
            }

            renderFullscreenQuad();

            unbindFbo("scene" + size);
        }

        glViewport(0, 0, rtFullWidth, rtFullHeight);
    }

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

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

        renderQuad();

        glPopMatrix();

        glMatrixMode(GL_MODELVIEW);
        glPopMatrix();
    }

    public void renderFullscreenQuad(int x, int y, int viewportWidth, int viewportHeight) {
        glViewport(x, y, viewportWidth, viewportHeight);

        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 void takeScreenshot() {
        takeScreenshot = true;

        overwriteRtWidth = 1920 * 2;
        overwriteRtHeight = 1080 * 2;
        createOrUpdateFullscreenFbos();
    }

    public void saveScreenshot() {
        if (!takeScreenshot) {
            return;
        }

        final FBO fboSceneFinal = getFBO("sceneFinal");

        if (fboSceneFinal == null) {
            return;
        }

        final ByteBuffer buffer = BufferUtils.createByteBuffer(fboSceneFinal.width * fboSceneFinal.height * 4);

        fboSceneFinal.bindTexture();
        GL11.glGetTexImage(GL11.GL_TEXTURE_2D, 0, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, buffer);
        fboSceneFinal.unbindTexture();

        Runnable r = new Runnable() {
            @Override
            public void run() {
                Calendar cal = Calendar.getInstance();
                SimpleDateFormat sdf = new SimpleDateFormat("yyMMddHHmmss");

                final String fileName = "Terasology-" + sdf.format(cal.getTime()) + "-" + fboSceneFinal.width + "x"
                        + fboSceneFinal.height + ".png";
                File file = new File(PathManager.getInstance().getScreenshotPath(), fileName);
                BufferedImage image = new BufferedImage(fboSceneFinal.width, fboSceneFinal.height,
                        BufferedImage.TYPE_INT_RGB);

                for (int x = 0; x < fboSceneFinal.width; x++)
                    for (int y = 0; y < fboSceneFinal.height; y++) {
                        int i = (x + fboSceneFinal.width * y) * 4;
                        int r = buffer.get(i) & 0xFF;
                        int g = buffer.get(i + 1) & 0xFF;
                        int b = buffer.get(i + 2) & 0xFF;
                        image.setRGB(x, fboSceneFinal.height - (y + 1), (0xFF << 24) | (r << 16) | (g << 8) | b);
                    }

                try {
                    ImageIO.write(image, "png", file);
                    logger.info("Screenshot '" + fileName + "' saved! ");
                } catch (IOException e) {
                    logger.warn("Could not save screenshot!", e);
                }
            }
        };

        CoreRegistry.get(GameEngine.class).submitTask("Write screenshot", r);

        takeScreenshot = false;
        overwriteRtWidth = 0;
        overwriteRtWidth = 0;

        createOrUpdateFullscreenFbos();
    }

    public float getExposure() {
        return currentExposure;
    }

    public FBO getFBO(String title) {
        FBO fbo = FBOs.get(title);

        if (fbo == null) {
            logger.error("Failed to retrieve FBO '" + title + "'!");
        }

        return fbo;
    }

    public boolean bindFbo(String title) {
        FBO fbo;

        if ((fbo = FBOs.get(title)) != null) {
            fbo.bind();
            currentlyBoundFboName = title;
            return true;
        }

        logger.error("Failed to bind FBO since the requested FBO could not be found!");
        return false;
    }

    public boolean unbindFbo(String title) {
        FBO fbo;

        if ((fbo = FBOs.get(title)) != null) {
            fbo.unbind();
            currentlyBoundFboName = "";
            return true;
        }

        logger.error("Failed to unbind FBO since the requested FBO could not be found!");
        return false;
    }

    public boolean bindFboTexture(String title) {
        FBO fbo;

        if ((fbo = FBOs.get(title)) != null) {
            fbo.bindTexture();
            return true;
        }

        logger.error("Failed to bind FBO texture since the requested FBO could not be found!");
        return false;
    }

    public boolean bindFboDepthTexture(String title) {
        FBO fbo;

        if ((fbo = FBOs.get(title)) != null) {
            fbo.bindDepthTexture();
            return true;
        }

        logger.error("Failed to bind FBO depth texture since the requested FBO could not be found!");
        return false;
    }

    public boolean bindFboNormalsTexture(String title) {
        FBO fbo;

        if ((fbo = FBOs.get(title)) != null) {
            fbo.bindNormalsTexture();
            return true;
        }

        logger.error("Failed to bind FBO normals texture since the requested FBO could not be found!");
        return false;
    }

    public boolean bindFboLightBufferTexture(String title) {
        FBO fbo;

        if ((fbo = FBOs.get(title)) != null) {
            fbo.bindLightBufferTexture();
            return true;
        }

        logger.error("Failed to bind FBO texture since the requested FBO could not be found!");
        return false;
    }

    public void flipPingPongFbo(String title) {
        FBO fbo1 = getFBO(title);
        FBO fbo2 = getFBO(title + "PingPong");

        if (fbo1 == null || fbo2 == null) {
            return;
        }

        FBOs.put(title, fbo2);
        FBOs.put(title + "PingPong", fbo1);
    }

    @Override
    public void addPropertiesToList(List<Property> properties) {
        properties.add(hdrMaxExposure);
        properties.add(hdrExposureAdjustmentSpeed);
        properties.add(hdrExposureDefault);
        properties.add(hdrMaxExposureNight);
        properties.add(hdrMinExposure);
        properties.add(hdrTargetLuminance);
        properties.add(overallBlurRadiusFactor);
        properties.add(bloomHighPassThreshold);
        properties.add(bloomBlurRadius);
    }
}