package demos.renderToTexture;

import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;

import javax.media.opengl.DebugGL;
import javax.media.opengl.GL;
import javax.media.opengl.GLAutoDrawable;
import javax.media.opengl.GLCanvas;
import javax.media.opengl.GLEventListener;
import javax.media.opengl.GLException;
import javax.swing.JFrame;

import com.sun.opengl.utils.GLUT;

public class RenderToTextureFBO implements GLEventListener
{
    private static GLUT glut = new GLUT();

    public static void main(String[] args)
    {
        RenderToTextureFBO renderer = new RenderToTextureFBO();

        GLCanvas canvas = new GLCanvas();

        canvas.addGLEventListener(renderer);

        JFrame frame = new JFrame("Render To Texture Demo");

        frame.addWindowListener(new WindowAdapter()
        {
            public void windowClosing(WindowEvent evt)
            {
                System.exit(0);
            }
        });

        frame.getContentPane().add(canvas);
        canvas.setSize(300, 300);

        frame.pack();
        frame.setVisible(true);
    }

    private static float[] lightPosition = new float[]
    {
        1, 0, 10, 1
    };

    private FramebufferObjectRenderer framebufferRenderer;

    private boolean isFramebufferRendererCreated = false;

    public void init(GLAutoDrawable drawable)
    {
        drawable.setGL(new DebugGL(drawable.getGL()));

        GL gl = drawable.getGL();

        gl.glEnable(GL.GL_DEPTH_TEST);
        gl.glEnable(GL.GL_NORMALIZE);
        gl.glEnable(GL.GL_LIGHTING);

        gl.glClearColor(0.5f, 0.5f, 0.5f, 0);

        // Place the camera
        gl.glMatrixMode(GL.GL_PROJECTION);
        gl.glLoadIdentity();
        gl.glFrustum(-2.0 / 10, 2.0 / 10, -2.0 / 10, 2.0 / 10, 1, 11);

        gl.glMatrixMode(GL.GL_MODELVIEW);
        gl.glLoadIdentity();
        gl.glTranslated(0, 0, -10);

        // Configure the light
        gl.glEnable(GL.GL_LIGHT0);
        gl.glLightfv(GL.GL_LIGHT0, GL.GL_POSITION, lightPosition, 0);

        if (!isFramebufferRendererCreated)
        {
            framebufferRenderer = new FramebufferObjectRenderer(256);
            framebufferRenderer.init(drawable);
            isFramebufferRendererCreated = true;
        }
    }

    public void display(GLAutoDrawable drawable)
    {
        GL gl = drawable.getGL();

        gl.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT);

        framebufferRenderer.renderToFrameBuffer(drawable);

        // Draw the teapot casting a shadow on a repeated picture of the
        // offscreen colour buffer.
        // We'll use multi-texture unit 1 for the shadowing...
        framebufferRenderer.prepareForShadowedRendering(drawable,
                                                        GL.GL_TEXTURE1);
        // ... and multi-texture unit 0 for the picture
        framebufferRenderer.prepareForColouredRendering(drawable,
                                                        GL.GL_TEXTURE0);
        drawSquare(drawable);
        framebufferRenderer.stopColouredRendering(drawable);
        drawFloatingTeapot(drawable);
        framebufferRenderer.stopShadowedRendering(drawable, GL.GL_TEXTURE1);
    }

    private static void drawFloatingTeapot(GLAutoDrawable drawable)
    {
        GL gl = drawable.getGL();

        gl.glPushAttrib(GL.GL_TRANSFORM_BIT);
        {
            gl.glMatrixMode(GL.GL_MODELVIEW);
            gl.glPushMatrix();
            {
                gl.glTranslated(0.5, 0, 3);
                glut.glutSolidTeapot(0.25, true);
            }
            gl.glPopMatrix();
        }
        gl.glPopAttrib();
    }

    private static void drawSquare(GLAutoDrawable drawable)
    {
        GL gl = drawable.getGL();

        gl.glBegin(GL.GL_QUADS);
        {
            gl.glNormal3d(0, 0, 1);
            gl.glMultiTexCoord2d(GL.GL_TEXTURE0, 0, 0);
            gl.glVertex3d(-1, -1, 0);
            gl.glMultiTexCoord2d(GL.GL_TEXTURE0, 2, 0);
            gl.glVertex3d(1, -1, 0);
            gl.glMultiTexCoord2d(GL.GL_TEXTURE0, 2, 2);
            gl.glVertex3d(1, 1, 0);
            gl.glMultiTexCoord2d(GL.GL_TEXTURE0, 0, 2);
            gl.glVertex3d(-1, 1, 0);
        }
        gl.glEnd();
    }

    public void displayChanged(GLAutoDrawable drawable,
                               boolean modeChanged,
                               boolean deviceChanged)
    {
        // Nothing to do
    }

    public void reshape(GLAutoDrawable drawable,
                        int x,
                        int y,
                        int width,
                        int height)
    {
        // Nothing to do
    }

    private static class FramebufferObjectRenderer
    {
        private int textureDimension;

        private int frameBufferID;
        private int colourTextureID;
        private int depthTextureID;

        public FramebufferObjectRenderer(int textureDimension)
        {
            super();

            this.textureDimension = textureDimension;
        }

        public void init(GLAutoDrawable drawable)
        {
            GL gl = drawable.getGL();

            // Allocate the framebuffer object
            int[] result = new int[1];
            gl.glGenFramebuffersEXT(1, result, 0);
            frameBufferID = result[0];
            gl.glBindFramebufferEXT(GL.GL_FRAMEBUFFER_EXT, frameBufferID);

            // Allocate the colour texture
            gl.glGenTextures(1, result, 0);
            colourTextureID = result[0];
            gl.glBindTexture(GL.GL_TEXTURE_2D, colourTextureID);
            gl.glTexParameteri(GL.GL_TEXTURE_2D,
                               GL.GL_TEXTURE_MIN_FILTER,
                               GL.GL_NEAREST);
            gl.glTexParameteri(GL.GL_TEXTURE_2D,
                               GL.GL_TEXTURE_MAG_FILTER,
                               GL.GL_NEAREST);
            gl.glTexParameteri(GL.GL_TEXTURE_2D,
                               GL.GL_TEXTURE_WRAP_S,
                               GL.GL_CLAMP_TO_EDGE);
            gl.glTexParameteri(GL.GL_TEXTURE_2D,
                               GL.GL_TEXTURE_WRAP_T,
                               GL.GL_CLAMP_TO_EDGE);
            gl.glTexImage2D(GL.GL_TEXTURE_2D,
                            0,
                            GL.GL_RGBA8,
                            textureDimension,
                            textureDimension,
                            0,
                            GL.GL_RGBA,
                            GL.GL_UNSIGNED_BYTE,
                            null);

            // Allocate the depth texture
            gl.glGenTextures(1, result, 0);
            depthTextureID = result[0];
            gl.glBindTexture(GL.GL_TEXTURE_2D, depthTextureID);
            gl.glTexParameteri(GL.GL_TEXTURE_2D,
                               GL.GL_TEXTURE_MIN_FILTER,
                               GL.GL_NEAREST);
            gl.glTexParameteri(GL.GL_TEXTURE_2D,
                               GL.GL_TEXTURE_MAG_FILTER,
                               GL.GL_NEAREST);
            gl.glTexParameteri(GL.GL_TEXTURE_2D,
                               GL.GL_TEXTURE_WRAP_S,
                               GL.GL_CLAMP_TO_EDGE);
            gl.glTexParameteri(GL.GL_TEXTURE_2D,
                               GL.GL_TEXTURE_WRAP_T,
                               GL.GL_CLAMP_TO_EDGE);
            gl.glGetIntegerv(GL.GL_DEPTH_BITS, result, 0);
            int depthFormat;
            switch (result[0])
            {
                case 16:
                    depthFormat = GL.GL_DEPTH_COMPONENT16;
                    break;
                case 24:
                    depthFormat = GL.GL_DEPTH_COMPONENT24;
                    break;
                case 32:
                    depthFormat = GL.GL_DEPTH_COMPONENT32;
                    break;
                default:
                    throw new GLException("Unexpected number of depth bits: "
                                          + result[0]);
            }
            gl.glTexImage2D(GL.GL_TEXTURE_2D,
                            0,
                            depthFormat,
                            textureDimension,
                            textureDimension,
                            0,
                            GL.GL_DEPTH_COMPONENT,
                            GL.GL_UNSIGNED_INT,
                            null);

            // Attach the textures to the framebuffer
            gl.glFramebufferTexture2DEXT(GL.GL_FRAMEBUFFER_EXT,
                                         GL.GL_COLOR_ATTACHMENT0_EXT,
                                         GL.GL_TEXTURE_2D,
                                         colourTextureID,
                                         0);
            gl.glFramebufferTexture2DEXT(GL.GL_FRAMEBUFFER_EXT,
                                         GL.GL_DEPTH_ATTACHMENT_EXT,
                                         GL.GL_TEXTURE_2D,
                                         depthTextureID,
                                         0);
            
            gl.glBindFramebufferEXT(GL.GL_FRAMEBUFFER_EXT, 0);
        }

        private int counter = 0;

        public void renderToFrameBuffer(GLAutoDrawable drawable)
        {
            GL gl = drawable.getGL();

            gl.glPushAttrib(GL.GL_TRANSFORM_BIT | GL.GL_ENABLE_BIT
                            | GL.GL_COLOR_BUFFER_BIT);

            gl.glBindFramebufferEXT(GL.GL_FRAMEBUFFER_EXT, frameBufferID);

            gl.glEnable(GL.GL_DEPTH_TEST);
            gl.glEnable(GL.GL_NORMALIZE);
            gl.glDisable(GL.GL_LIGHTING);

            gl.glClearColor(0, 0, 1, 0);

            // Place the offscreen camera at the light position
            gl.glMatrixMode(GL.GL_PROJECTION);
            gl.glPushMatrix();
            gl.glLoadIdentity();
            applyLightFrustum(gl);

            gl.glMatrixMode(GL.GL_MODELVIEW);
            gl.glPushMatrix();
            gl.glLoadIdentity();
            applyLightTransformation(gl);

            gl.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT);

            // Draw the teapot as a red silhouette
            if (counter++ % 2 == 0)
            {
                gl.glColor3d(1, 0, 0);
            }
            else
            {
                gl.glColor3d(0, 1, 0);
            }
            drawFloatingTeapot(drawable);
            // Don't draw the square (because we don't want it to cast a shadow)

            // Unbind the framebuffer
            gl.glBindFramebufferEXT(GL.GL_FRAMEBUFFER_EXT, 0);

            gl.glMatrixMode(GL.GL_PROJECTION);
            gl.glPopMatrix();
            gl.glMatrixMode(GL.GL_MODELVIEW);
            gl.glPopMatrix();
            gl.glPopAttrib();
        }

        private void applyLightFrustum(GL gl)
        {
            // Set up the viewing frustum so that the light only sees the
            // volume between itself and the unit X-Y square at the origin
            // (because that is the only volume in which we will be casting
            // shadows).
            gl.glFrustum(-2.0 / 10, 0, -1.0 / 10, 1.0 / 10, 1, 10);
        }

        private void applyLightTransformation(GL gl)
        {
            gl.glTranslated(-lightPosition[0],
                            -lightPosition[1],
                            -lightPosition[2]);
        }

        public void prepareForShadowedRendering(GLAutoDrawable targetDrawable,
                                                int textureUnitID)
        {
            GL gl = targetDrawable.getGL();

            // The state modified by this preparation will be restored by a call
            // to finishShadowedRendering()
            gl.glPushAttrib(GL.GL_TEXTURE_BIT);

            gl.glActiveTexture(textureUnitID);
            gl.glBindTexture(GL.GL_TEXTURE_2D, depthTextureID);
            gl.glPushAttrib(GL.GL_TEXTURE_BIT);
            // Generate texture coordinates - an object-linear texture
            // generation that sets the texture coordinates to be the
            // object-space vertex coordinates (we will use the texture matrix
            // to transform from object-space to light-space)
            gl.glEnable(GL.GL_TEXTURE_GEN_S);
            gl.glEnable(GL.GL_TEXTURE_GEN_T);
            gl.glEnable(GL.GL_TEXTURE_GEN_R);
            gl.glEnable(GL.GL_TEXTURE_GEN_Q);
            gl.glTexGeni(GL.GL_S, GL.GL_TEXTURE_GEN_MODE, GL.GL_OBJECT_LINEAR);
            gl.glTexGeni(GL.GL_T, GL.GL_TEXTURE_GEN_MODE, GL.GL_OBJECT_LINEAR);
            gl.glTexGeni(GL.GL_R, GL.GL_TEXTURE_GEN_MODE, GL.GL_OBJECT_LINEAR);
            gl.glTexGeni(GL.GL_Q, GL.GL_TEXTURE_GEN_MODE, GL.GL_OBJECT_LINEAR);
            gl.glPushAttrib(GL.GL_TRANSFORM_BIT);
            {
                gl.glMatrixMode(GL.GL_MODELVIEW);
                gl.glPushMatrix();
                {
                    gl.glLoadIdentity();
                    float[] objectPlane = new float[]
                    {
                        1, 0, 0, 0
                    };
                    gl.glTexGenfv(GL.GL_S, GL.GL_OBJECT_PLANE, objectPlane, 0);
                    objectPlane[0] = 0;
                    objectPlane[1] = 1;
                    gl.glTexGenfv(GL.GL_T, GL.GL_OBJECT_PLANE, objectPlane, 0);
                    objectPlane[1] = 0;
                    objectPlane[2] = 1;
                    gl.glTexGenfv(GL.GL_R, GL.GL_OBJECT_PLANE, objectPlane, 0);
                    objectPlane[2] = 0;
                    objectPlane[3] = 1;
                    gl.glTexGenfv(GL.GL_Q, GL.GL_OBJECT_PLANE, objectPlane, 0);
                }
                gl.glPopMatrix();

                // Modify texture matrix - the texture matrix will transform
                // from object-space coordinates to light-space (i.e. the same
                // transformation that was applied when rendering the shadow
                // texture)
                gl.glMatrixMode(GL.GL_TEXTURE);
                // The texture matrix will be restored by
                // stopShadowedRendering()
                gl.glPushMatrix();
                gl.glLoadIdentity();
                // Go from the unit square to the upper quadrant (because
                // texture coords run from 0 to 1)
                gl.glScalef(0.5f, 0.5f, 0.5f);
                gl.glTranslatef(1, 1, 1);
                // Apply the transformation to light-space
                applyLightFrustum(gl);
                applyLightTransformation(gl);
            }
            gl.glPopAttrib();

            // Set the texture up to be used as a depth comparison
            int textureTarget = GL.GL_TEXTURE_2D;
            gl.glEnable(textureTarget);
            gl.glTexEnvi(GL.GL_TEXTURE_ENV,
                         GL.GL_TEXTURE_ENV_MODE,
                         GL.GL_MODULATE);
            gl.glTexParameteri(textureTarget,
                               GL.GL_TEXTURE_MIN_FILTER,
                               GL.GL_LINEAR);
            gl.glTexParameteri(textureTarget,
                               GL.GL_TEXTURE_MAG_FILTER,
                               GL.GL_LINEAR);
            gl.glTexParameteri(textureTarget,
                               GL.GL_TEXTURE_WRAP_S,
                               GL.GL_CLAMP_TO_EDGE);
            gl.glTexParameteri(textureTarget,
                               GL.GL_TEXTURE_WRAP_T,
                               GL.GL_CLAMP_TO_EDGE);
            gl.glTexParameteri(textureTarget,
                               GL.GL_TEXTURE_COMPARE_MODE,
                               GL.GL_COMPARE_R_TO_TEXTURE);
            gl.glTexParameteri(textureTarget,
                               GL.GL_TEXTURE_COMPARE_FUNC,
                               GL.GL_LEQUAL);
            gl.glTexParameteri(textureTarget,
                               GL.GL_DEPTH_TEXTURE_MODE,
                               GL.GL_LUMINANCE);
        }

        public void stopShadowedRendering(GLAutoDrawable targetDrawable,
                                          int textureUnitID)
        {
            GL gl = targetDrawable.getGL();
            gl.glActiveTexture(textureUnitID);
            gl.glPushAttrib(GL.GL_TRANSFORM_BIT);
            {
                gl.glMatrixMode(GL.GL_TEXTURE);
                gl.glPopMatrix();
            }
            gl.glPopAttrib();

            // Restore the texture attributes for the textureUnith texture
            gl.glPopAttrib();

            // Unbind the depth texture
            gl.glBindTexture(GL.GL_TEXTURE_2D, 0);

            // Restore the active texture
            gl.glPopAttrib();
        }

        public void prepareForColouredRendering(GLAutoDrawable targetDrawable,
                                                int textureUnitID)
        {
            GL gl = targetDrawable.getGL();

            // The state modified by this preparation will be restored by a call
            // to finishShadowedRendering()
            gl.glPushAttrib(GL.GL_TEXTURE_BIT);

            gl.glActiveTexture(textureUnitID);
            gl.glBindTexture(GL.GL_TEXTURE_2D, colourTextureID);

            // Set the texture up to be used for painting a surface
            int textureTarget = GL.GL_TEXTURE_2D;
            gl.glEnable(textureTarget);
            gl.glTexEnvi(GL.GL_TEXTURE_ENV,
                         GL.GL_TEXTURE_ENV_MODE,
                         GL.GL_MODULATE);
            gl.glTexParameteri(textureTarget,
                               GL.GL_TEXTURE_MIN_FILTER,
                               GL.GL_LINEAR);
            gl.glTexParameteri(textureTarget,
                               GL.GL_TEXTURE_MAG_FILTER,
                               GL.GL_LINEAR);
            gl.glTexParameteri(textureTarget,
                               GL.GL_TEXTURE_WRAP_S,
                               GL.GL_REPEAT);
            gl.glTexParameteri(textureTarget,
                               GL.GL_TEXTURE_WRAP_T,
                               GL.GL_REPEAT);
        }

        public void stopColouredRendering(GLAutoDrawable targetDrawable)
        {
            GL gl = targetDrawable.getGL();

            gl.glBindTexture(GL.GL_TEXTURE_2D, 0);

            // Restore the active texture
            gl.glPopAttrib();
        }

    }

}