ZTK's picture

texture to texture copy, running through a shader?

Hello everyone,

I have what I think is a fairly simple problem. I have a texture. I want to run that texture through a shader that rotates colors (blue->red, red->green, green->blue). So a blue rectangle running through this shader would output as a red rectangle.

I've got texture A. I would like to create texture B, that is a copy of texture A run through the shader (thus colors rotated).

The way I'm trying to do this is by creating an FBO, attaching texture B to it, and then rendering texture A to that FBO.

This sort of works, but not really. Texture B ends up having a smaller version of texture A.. I've probably got the viewport and/or projection matrix settings off.. I keep tweaking them but it never looks quite right.

Rather than trying goldilocks programming (mess with it until it looks just right), I was hoping that someone here could aid me. I'm sure this is a problem that's been solved before. Does anyone have a short code snippet of how to do this?

The first image attachment below shows what I mean. The blue square on the left is the original texture. The red square on the right is the color rotated texture. Here is how I do it:

        protected override void OnRenderFrame(FrameEventArgs e)
        {
 
            base.OnRenderFrame(e);
 
            // clear background to pink
            GL.ClearColor(0.9f, 0.7f, 0.7f, 0f);
            GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);
            GL.MatrixMode(MatrixMode.Modelview);
            GL.LoadMatrix(ref modelviewMatrix);
 
            // draw the triangle into the FBO that holds texture1
            GL.BindTexture(TextureTarget.Texture2D, 0);
            RenderTriangleInFBO(FBOHandle1);
            RenderTriangleInFBO(FBOHandle2);
 
            // draw a square that holds texture1
            GL.BindTexture(TextureTarget.Texture2D, texture1);
            GL.Begin(BeginMode.Quads);
            GL.TexCoord2(0.0f, 1.0f); GL.Vertex3(-3.0f, -1.0f, 0.0f);
            GL.TexCoord2(1.0f, 1.0f); GL.Vertex3(-3.0f, 1.0f, 0.0f);
            GL.TexCoord2(1.0f, 0.0f); GL.Vertex3(-1.0f, 1.0f, 0.0f);
            GL.TexCoord2(0.0f, 0.0f); GL.Vertex3(-1.0f, -1.0f, 0.0f);
            GL.End();
 
            // now render texture 2 in a square, shading it.
            GL.UseProgram(shaderProgramHandle1);
            GL.BindTexture(TextureTarget.Texture2D, texture2);
            GL.Begin(BeginMode.Quads);
            GL.TexCoord2(0.0f, 1.0f); GL.Vertex3(1.0f, -1.0f, 0.0f);
            GL.TexCoord2(1.0f, 1.0f); GL.Vertex3(1.0f, 1.0f, 0.0f);
            GL.TexCoord2(1.0f, 0.0f); GL.Vertex3(3.0f, 1.0f, 0.0f);
            GL.TexCoord2(0.0f, 0.0f); GL.Vertex3(3.0f, -1.0f, 0.0f);
            GL.End();
            GL.UseProgram(0);
 
            SwapBuffers();
        }

That was done by just displaying the first texture, and running it through a shader.

But what I'd like really like to do is take the first (blue) texture, render it offscreen into a FBO, and then grab that second texture. So I want the final state to have two textures -- texture1 the original blue texture, and texture2 the red one.

So what I do is render texture1 through a shader into an FBO. Then I try to draw the captured texture from the FBO. So the render code looks like this

        protected override void OnRenderFrame(FrameEventArgs e)
        {
 
            base.OnRenderFrame(e);
 
            // clear background to pink
            GL.ClearColor(0.9f, 0.7f, 0.7f, 0f);
            GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);
            GL.MatrixMode(MatrixMode.Modelview);
            GL.LoadMatrix(ref modelviewMatrix);
 
            // draw the triangle into the FBO that holds texture1 and texture2
            GL.BindTexture(TextureTarget.Texture2D, 0);
            RenderTriangleInFBO(FBOHandle1);
            RenderTriangleInFBO(FBOHandle2);
 
            // draw a square that holds texture1
            GL.BindTexture(TextureTarget.Texture2D, texture1);
            GL.Begin(BeginMode.Quads);
            GL.TexCoord2(0.0f, 1.0f); GL.Vertex3(-3.0f, -1.0f, 0.0f);
            GL.TexCoord2(1.0f, 1.0f); GL.Vertex3(-3.0f, 1.0f, 0.0f);
            GL.TexCoord2(1.0f, 0.0f); GL.Vertex3(-1.0f, 1.0f, 0.0f);
            GL.TexCoord2(0.0f, 0.0f); GL.Vertex3(-1.0f, -1.0f, 0.0f);
            GL.End();
 
            // run texture1 through a shader, and set to texture2
            GL.UseProgram(shaderProgramHandle1);
            GL.Ext.BindFramebuffer(FramebufferTarget.DrawFramebuffer, FBOHandle2);
            GL.BindTexture(TextureTarget.Texture2D, texture1);
            GL.Begin(BeginMode.Quads);
            GL.TexCoord2(0.0f, 1.0f); GL.Vertex3(1.0f, -1.0f, 0.0f);
            GL.TexCoord2(1.0f, 1.0f); GL.Vertex3(1.0f, 1.0f, 0.0f);
            GL.TexCoord2(1.0f, 0.0f); GL.Vertex3(3.0f, 1.0f, 0.0f);
            GL.TexCoord2(0.0f, 0.0f); GL.Vertex3(3.0f, -1.0f, 0.0f);
            GL.End();
            GL.Ext.BindFramebuffer(FramebufferTarget.FramebufferExt, 0); // disable rendering into the FBO
 
 
            // now render texture 2 in a square (without shader)
            // this ends up looking wrong
            GL.UseProgram(0);
            GL.BindTexture(TextureTarget.Texture2D, texture2);
            GL.Begin(BeginMode.Quads);
            GL.TexCoord2(0.0f, 1.0f); GL.Vertex3(1.0f, -1.0f, 0.0f);
            GL.TexCoord2(1.0f, 1.0f); GL.Vertex3(1.0f, 1.0f, 0.0f);
            GL.TexCoord2(1.0f, 0.0f); GL.Vertex3(3.0f, 1.0f, 0.0f);
            GL.TexCoord2(0.0f, 0.0f); GL.Vertex3(3.0f, -1.0f, 0.0f);
            GL.End();
 
            SwapBuffers();
        }

But, what happens is that the color rotated texture is smaller, because the view isn't quite the same. It looks like the second pic below.

I know fundamentally the problem here. The problem is in this block of code

            // run texture1 through a shader, and set to texture2
            GL.UseProgram(shaderProgramHandle1);
            GL.Ext.BindFramebuffer(FramebufferTarget.DrawFramebuffer, FBOHandle2);
            GL.BindTexture(TextureTarget.Texture2D, texture1);
            GL.Begin(BeginMode.Quads);
            GL.TexCoord2(0.0f, 1.0f); GL.Vertex3(1.0f, -1.0f, 0.0f);
            GL.TexCoord2(1.0f, 1.0f); GL.Vertex3(1.0f, 1.0f, 0.0f);
            GL.TexCoord2(1.0f, 0.0f); GL.Vertex3(3.0f, 1.0f, 0.0f);
            GL.TexCoord2(0.0f, 0.0f); GL.Vertex3(3.0f, -1.0f, 0.0f);
            GL.End();
            GL.Ext.BindFramebuffer(FramebufferTarget.FramebufferExt, 0); // disable rendering into the FBO

The problem here is that I'm rendering into a quad that will then be displayed in the quad (if that makes sense). I realize I can tweak the values of the quad to get bigger or smaller, but I don't want to mess around until it looks right. I'm sure there has to be a correct way to do this.

So to reiterate, what I want to do is
1) Create an original texture by rending into an FBO (this I can do)
2) Take that texture, run it through a shader (this I can do)
3) take the output of that shader and assign it to a texture with the same view as step 1 (this I am having problems with)
4) render two quads side by side, one with each texture (this I can do)

I would very much appreciate any help. For reference, here is my entire source

// Released to the public domain. Use, modify and relicense at will.
 
using System;
using OpenTK;
using OpenTK.Graphics;
using OpenTK.Graphics.OpenGL;
using OpenTK.Audio;
using OpenTK.Audio.OpenAL;
using OpenTK.Input;
 
using System.Drawing;
using System.Drawing.Imaging;
using System.Diagnostics;
 
namespace StarterKit
{
 
    class Game : GameWindow
    {
        private Matrix4 modelviewMatrix, projectionMatrix;
 
        int texture1, texture2, fragmentShaderHandle1, shaderProgramHandle1;
        int FBOHandle1, FBOHandle2, depthbuffername1, depthbuffername2;
 
        Bitmap bitmap1 = new Bitmap(@"..\..\img\pic1.bmp");
        Bitmap bitmap2 = new Bitmap(@"..\..\img\pic2.bmp");
 
        string fragmentShaderSource1 = @"
 
#version 150
uniform sampler2D MyTexture1;
 
void main(void)
{
  gl_FragColor = texture2D( MyTexture1, gl_TexCoord[0].st );  
  gl_FragColor.rgb = gl_FragColor.brg;
}";
 
        void CreateShaders()
        {
 
            fragmentShaderHandle1 = GL.CreateShader(ShaderType.FragmentShader);
            GL.ShaderSource(fragmentShaderHandle1, fragmentShaderSource1);
            GL.CompileShader(fragmentShaderHandle1);
            Debug.WriteLine(GL.GetShaderInfoLog(fragmentShaderHandle1));
 
            // Create program
            shaderProgramHandle1 = GL.CreateProgram();
            GL.AttachShader(shaderProgramHandle1, fragmentShaderHandle1);
            GL.LinkProgram(shaderProgramHandle1);
            Debug.WriteLine(GL.GetProgramInfoLog(shaderProgramHandle1));
 
            float aspectRatio = ClientSize.Width / (float)(ClientSize.Height);
            Matrix4.CreatePerspectiveFieldOfView((float)Math.PI / 4, aspectRatio, 1, 100, out projectionMatrix);
        }
 
        /// <summary>Creates a 800x600 window with the specified title.</summary>
        public Game()
            : base(800, 600, GraphicsMode.Default, "OpenTK Quick Start Sample")
        {
            VSync = VSyncMode.On;
        }
 
        /// <summary>Load resources here.</sudfmmary>
        /// <param name="e">Not used.</param>
        protected override void OnLoad(EventArgs e)
        {
            base.OnLoad(e);
            CreateShaders();
            GL.Enable(EnableCap.DepthTest);
            GL.Enable(EnableCap.Texture2D);
            GL.Hint(HintTarget.PerspectiveCorrectionHint, HintMode.Nicest);
 
            CreateTexture(out texture1, bitmap1);
            CreateTexture(out texture2, bitmap2);
 
            CreateFBOandAssignTexture(out FBOHandle1, texture1, out depthbuffername1);
            CreateFBOandAssignTexture(out FBOHandle2, texture2, out depthbuffername2);
 
            modelviewMatrix = Matrix4.LookAt(7 * Vector3.UnitZ, Vector3.Zero, Vector3.UnitY);
 
        }
 
        private void CreateFBOandAssignTexture(out int fbo, int texture, out int depthbuffername)
        {
            // create and bind an FBO
            GL.Ext.GenFramebuffers(1, out fbo);
            GL.Ext.BindFramebuffer(FramebufferTarget.DrawFramebuffer, fbo);
 
            // create depth renderbuffer
            GL.Ext.GenRenderbuffers(1, out depthbuffername);
            GL.Ext.BindRenderbuffer(RenderbufferTarget.Renderbuffer, depthbuffername);
            GL.Ext.RenderbufferStorage(RenderbufferTarget.Renderbuffer, RenderbufferStorage.DepthComponent32, bitmap1.Width, bitmap1.Height);
 
            // assign texture to FBO
            GL.Ext.FramebufferTexture2D(FramebufferTarget.DrawFramebuffer, FramebufferAttachment.ColorAttachment0Ext, TextureTarget.Texture2D, texture, 0);
 
            // attach texture to color attachment and depth rbo
            GL.Ext.FramebufferTexture2D(FramebufferTarget.DrawFramebuffer, FramebufferAttachment.ColorAttachment0, TextureTarget.Texture2D, texture, 0);
            GL.Ext.FramebufferRenderbuffer(FramebufferTarget.DrawFramebuffer, FramebufferAttachment.DepthAttachment, RenderbufferTarget.Renderbuffer, depthbuffername);
 
 
            #region Test for Error
 
            string version = string.Empty;
            try
            {
                GLControl control = new GLControl();
                version = GL.GetString(StringName.Version);
            }
            catch (Exception ex)
            {
            }
 
            switch (GL.Ext.CheckFramebufferStatus(FramebufferTarget.FramebufferExt))
            {
                case FramebufferErrorCode.FramebufferCompleteExt:
                    {
                        Console.WriteLine("FBO: The framebuffer is complete and valid for rendering.");
                        break;
                    }
                case FramebufferErrorCode.FramebufferIncompleteAttachmentExt:
                    {
                        Console.WriteLine("FBO: One or more attachment points are not framebuffer attachment complete. This could mean there’s no texture attached or the format isn’t renderable. For color textures this means the base format must be RGB or RGBA and for depth textures it must be a DEPTH_COMPONENT format. Other causes of this error are that the width or height is zero or the z-offset is out of range in case of render to volume.");
                        break;
                    }
                case FramebufferErrorCode.FramebufferIncompleteMissingAttachmentExt:
                    {
                        Console.WriteLine("FBO: There are no attachments.");
                        break;
                    }
                /* case  FramebufferErrorCode.GL_FRAMEBUFFER_INCOMPLETE_DUPLICATE_ATTACHMENT_EXT: 
                     {
                         Console.WriteLine("FBO: An object has been attached to more than one attachment point.");
                         break;
                     }*/
                case FramebufferErrorCode.FramebufferIncompleteDimensionsExt:
                    {
                        Console.WriteLine("FBO: Attachments are of different size. All attachments must have the same width and height.");
                        break;
                    }
                case FramebufferErrorCode.FramebufferIncompleteFormatsExt:
                    {
                        Console.WriteLine("FBO: The color attachments have different format. All color attachments must have the same format.");
                        break;
                    }
                case FramebufferErrorCode.FramebufferIncompleteDrawBufferExt:
                    {
                        Console.WriteLine("FBO: An attachment point referenced by GL.DrawBuffers() doesn’t have an attachment.");
                        break;
                    }
                case FramebufferErrorCode.FramebufferIncompleteReadBufferExt:
                    {
                        Console.WriteLine("FBO: The attachment point referenced by GL.ReadBuffers() doesn’t have an attachment.");
                        break;
                    }
                case FramebufferErrorCode.FramebufferUnsupportedExt:
                    {
                        Console.WriteLine("FBO: This particular FBO configuration is not supported by the implementation.");
                        break;
                    }
                default:
                    {
                        Console.WriteLine("FBO: Status unknown. (yes, this is really bad.)");
                        break;
                    }
            }
 
            // using FBO might have changed states, e.g. the FBO might not support stereoscopic views or double buffering
            int[] queryinfo = new int[6];
            GL.GetInteger(GetPName.MaxColorAttachmentsExt, out queryinfo[0]);
            GL.GetInteger(GetPName.AuxBuffers, out queryinfo[1]);
            GL.GetInteger(GetPName.MaxDrawBuffers, out queryinfo[2]);
            GL.GetInteger(GetPName.Stereo, out queryinfo[3]);
            GL.GetInteger(GetPName.Samples, out queryinfo[4]);
            GL.GetInteger(GetPName.Doublebuffer, out queryinfo[5]);
            Console.WriteLine("max. ColorBuffers: " + queryinfo[0] + " max. AuxBuffers: " + queryinfo[1] + " max. DrawBuffers: " + queryinfo[2] +
                               "\nStereo: " + queryinfo[3] + " Samples: " + queryinfo[4] + " DoubleBuffer: " + queryinfo[5]);
 
            Console.WriteLine("Last GL Error: " + GL.GetError());
 
            #endregion Test for Error
            GL.Ext.BindFramebuffer(FramebufferTarget.FramebufferExt, 0); // disable rendering into the FBO
        }
 
        private void RenderTriangleInFBO(int FBOHandle)
        {
            GL.Ext.BindFramebuffer(FramebufferTarget.DrawFramebuffer, FBOHandle);
            GL.PushAttrib(AttribMask.ViewportBit);
            {
                GL.Viewport(0, 0, ClientRectangle.Width, ClientRectangle.Height);
 
                // clear the screen in blue, to make it very obvious what the clear affected. only the FBO, not the real framebuffer
                GL.ClearColor(Color.Blue);
                GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);
                GL.Color3(Color.White);
                DrawTriangle();
            }
            GL.PopAttrib();
 
            GL.Ext.BindFramebuffer(FramebufferTarget.FramebufferExt, 0); // disable rendering into the FBO
        }
 
        private void DrawTriangle()
        {
            // put 1 triangles into the FBO's textures
            GL.PushAttrib(AttribMask.ColorBufferBit);
            GL.Begin(BeginMode.Triangles);
            {
                GL.Color4(0.0f, 1.0f, 0.0f, 1.0f);
                GL.Vertex3(0.0, 0.0, 0.0);
 
                GL.Color4(1.0f, 0.0f, 0.0f, 1.0f);
                GL.Vertex3(0.0, 2.0, 0.0);
 
                GL.Color4(0.0f, 0.0f, 1.0f, 1.0f);
                GL.Vertex3(2.0, 2.0, 0.0);
            }
            // reset to white
            GL.Color4(1.0f, 1.0f, 1.0f, 1.0f);
            GL.End();
            GL.PopAttrib();
        }
 
        private void CreateTexture(out int texture, Bitmap bitmap)
        {
            // load texture 
            GL.GenTextures(1, out texture);
 
            //Still required else TexImage2D will be applyed on the last bound texture
            GL.BindTexture(TextureTarget.Texture2D, texture);
 
            BitmapData data = bitmap.LockBits(new System.Drawing.Rectangle(0, 0, bitmap.Width, bitmap.Height),
            ImageLockMode.ReadOnly, System.Drawing.Imaging.PixelFormat.Format32bppArgb);
 
            GL.TexImage2D(TextureTarget.Texture2D, 0, PixelInternalFormat.Rgba, data.Width, data.Height, 0,
            OpenTK.Graphics.OpenGL.PixelFormat.Bgra, PixelType.UnsignedByte, data.Scan0);
 
            bitmap.UnlockBits(data);
            GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Linear);
        }
 
        /// <summary>
        /// Called when your window is resized. Set your viewport here. It is also
        /// a good place to set up your projection matrix (which probably changes
        /// along when the aspect ratio of your window).
        /// </summary>
        /// <param name="e">Not used.</param>
 
        protected override void OnResize(EventArgs e)
        {
            base.OnResize(e);
            GL.Viewport(ClientRectangle.X, ClientRectangle.Y, ClientRectangle.Width, ClientRectangle.Height);
            projectionMatrix = Matrix4.CreatePerspectiveFieldOfView((float)Math.PI / 4, Width / (float)Height, 1.0f, 64.0f);
            GL.MatrixMode(MatrixMode.Projection);
            GL.LoadMatrix(ref projectionMatrix);
        }
 
        /// <summary>
        /// Called when it is time to setup the next frame. Add you game logic here.
        /// </summary>
        /// <param name="e">Contains timing information for framerate independent logic.</param>
 
        protected override void OnUpdateFrame(FrameEventArgs e)
        {
            base.OnUpdateFrame(e);
            if (Keyboard[Key.Escape])
                Exit();
        }
 
        /// <summary>
        /// Called when it is time to render the next frame. Add your rendering code here.
        /// </summary>
        /// <param name="e">Contains timing information.</param>
        protected override void OnRenderFrame(FrameEventArgs e)
        {
 
            base.OnRenderFrame(e);
 
            // clear background to pink
            GL.ClearColor(0.9f, 0.7f, 0.7f, 0f);
            GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);
            GL.MatrixMode(MatrixMode.Modelview);
            GL.LoadMatrix(ref modelviewMatrix);
 
            // draw triangle normally
            GL.BindTexture(TextureTarget.Texture2D, 0);
 
            // draw the triangle into the FBO that holds texture1 and texture2
            RenderTriangleInFBO(FBOHandle1);
            RenderTriangleInFBO(FBOHandle2);
 
            // draw a square that holds texture1
            GL.BindTexture(TextureTarget.Texture2D, texture1);
            GL.Begin(BeginMode.Quads);
            GL.TexCoord2(0.0f, 1.0f); GL.Vertex3(-3.0f, -1.0f, 0.0f);
            GL.TexCoord2(1.0f, 1.0f); GL.Vertex3(-3.0f, 1.0f, 0.0f);
            GL.TexCoord2(1.0f, 0.0f); GL.Vertex3(-1.0f, 1.0f, 0.0f);
            GL.TexCoord2(0.0f, 0.0f); GL.Vertex3(-1.0f, -1.0f, 0.0f);
            GL.End();
 
            // run texture1 through a shader, and set to texture2
            GL.UseProgram(shaderProgramHandle1);
            GL.Ext.BindFramebuffer(FramebufferTarget.DrawFramebuffer, FBOHandle2);
            GL.BindTexture(TextureTarget.Texture2D, texture1);
            GL.Begin(BeginMode.Quads);
            GL.TexCoord2(0.0f, 1.0f); GL.Vertex3(1.0f, -1.0f, 0.0f);
            GL.TexCoord2(1.0f, 1.0f); GL.Vertex3(1.0f, 1.0f, 0.0f);
            GL.TexCoord2(1.0f, 0.0f); GL.Vertex3(3.0f, 1.0f, 0.0f);
            GL.TexCoord2(0.0f, 0.0f); GL.Vertex3(3.0f, -1.0f, 0.0f);
            GL.End();
            GL.Ext.BindFramebuffer(FramebufferTarget.FramebufferExt, 0); // disable rendering into the FBO
 
 
            // now render texture 2 in a square (without shader)
            GL.UseProgram(0);
            GL.BindTexture(TextureTarget.Texture2D, texture2);
            GL.Begin(BeginMode.Quads);
            GL.TexCoord2(0.0f, 1.0f); GL.Vertex3(1.0f, -1.0f, 0.0f);
            GL.TexCoord2(1.0f, 1.0f); GL.Vertex3(1.0f, 1.0f, 0.0f);
            GL.TexCoord2(1.0f, 0.0f); GL.Vertex3(3.0f, 1.0f, 0.0f);
            GL.TexCoord2(0.0f, 0.0f); GL.Vertex3(3.0f, -1.0f, 0.0f);
            GL.End();
 
            SwapBuffers();
        }
 
        private void BindTexture(ref int textureId, TextureUnit textureUnit, string UniformName)
        {
            GL.ActiveTexture(textureUnit);
            GL.BindTexture(TextureTarget.Texture2D, textureId);
        }
 
        /// <summary>
        /// The main entry point for the application.
        /// </summary>
 
        [STAThread]
        static void Main()
        {
            // The 'using' idiom guarantees proper resource cleanup.
            // We request 30 UpdateFrame events per second, and unlimited
            // RenderFrame events (as fast as the computer can handle).
            using (Game game = new Game())
            {
                game.Run(30.0);
            }
        }
    }
}
Inline Images

Comments

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.
tksuoran's picture

Check your viewport and projection matrix. These depend on the size of your render target. When you render to window, set these as you do on your OnResize(). When you render to texture, set these based on the texture size. If you use window size based viewport and/or projection matrix when rendering to texture, you will not get correct results (unless they happen to be the same size).

ZTK's picture

Thansk tksuoran. I've been messing around with that, having a hard time getting it to look right. Would you happen to have an example of how to render texture A to texture B (assuming both are the same size) so that it looks like A has been copied to B?

Basically I just want to copy A -> B (except have the copy run through a shader). I'm not able to get it to look right by changing viewport/projection matrix :(

ZTK's picture

Solved. For those that come across this in the future, problem was I had not set to orthographic projection. Thus there was always some scaling due to frustrum.

So solution is to render texture in "full screen" mode to the FBO -- done by setting othorgraphic projection, setting clip to (-1,1) in X and Y dimensions and then drawing to a quad whose dimentions are (-1,1) in X and Y dimensions

Here is revised render with

            GL.Viewport(ClientRectangle.X, ClientRectangle.Y, ClientRectangle.Width, ClientRectangle.Height);
            Matrix4 modelview = Matrix4.LookAt(0, 0, 1,
                                               0, 0, 0,
                                               0, 1, 0);
            GL.MatrixMode(MatrixMode.Modelview);
            GL.LoadMatrix(ref modelview);
 
            GL.MatrixMode(MatrixMode.Projection);
            GL.LoadIdentity();
            GL.Ortho(-1, 1, -1, 1, -1, 1.1);
 
            // run texture1 through a shader, and set to texture2
            GL.UseProgram(shaderProgramHandle1);
            GL.Viewport(0, 0, bitmap2.Width, bitmap2.Height);
            GL.Ext.BindFramebuffer(FramebufferTarget.DrawFramebuffer, FBOHandle2);
            GL.BindTexture(TextureTarget.Texture2D, texture1);
            GL.Begin(BeginMode.Quads);
            GL.TexCoord2(0.0f, 0.0f); GL.Vertex3(-1.0f, -1.0f, 0.0f);
            GL.TexCoord2(0.0f, 1.0f); GL.Vertex3(-1.0f, 1.0f, 0.0f);
            GL.TexCoord2(1.0f, 1.0f); GL.Vertex3(1.0f, 1.0f, 0.0f);
            GL.TexCoord2(1.0f, 0.0f); GL.Vertex3(1.0f, -1.0f, 0.0f);
            GL.End();
            GL.Ext.BindFramebuffer(FramebufferTarget.FramebufferExt, 0); // disable rendering into the FBO