ZTK's picture

Rendering off screen dynamic textures using FBOs: Conway's Game of Life

Hello everyone,

In this tutorial, we will explore using FrameBuffer Objects to render dynamic textures. We will create two textures, named texture1 and texture2. texture1 will be seeded with random data, and we will apply the rules to Conway's Game of Life in the shader. The output will then be rendered offscreen to texture2. In the next frame, texture2 will be the input to the shader, and the output will be written to texture 1. With this ping-pong approach, we continue to process the game indefinitely.

Let's first take a look at how to generate an empty texture.

        private void CreateNullTexture(out int texture)
        {
            // load texture 
            GL.GenTextures(1, out texture);
 
            // Still required else TexImage2D will be applyed on the last bound texture
            GL.BindTexture(TextureTarget.Texture2D, texture);
 
            // generate null texture
            GL.TexImage2D(TextureTarget.Texture2D, 0, PixelInternalFormat.Rgba, size_w, size_h, 0,
            OpenTK.Graphics.OpenGL.PixelFormat.Bgra, PixelType.UnsignedByte, IntPtr.Zero);
 
            // set filtering to nearest so we get "atari 2600" look
            GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Nearest);
            GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Nearest);
        }

Note that we don't load any actual data from disk. We specify the dimensions of the texture with the private variables size_w and size_h, and then load empty data with IntPtr.Zero.

Another thing to notice is the use of Nearest filters. We don't want linear filters as that would give a blurred look to our textures. We want a nice blocky look, like the old Atari 2600 video game console, so we use Nearest filtering.

Now that we have our empty textures, we want to create two FrameBufferObjects, and bind a texture to each of them. This is done in the CreateFBOandAssignTexture function.

            // create and bind an FBO
            GL.Ext.GenFramebuffers(1, out fbo);
            GL.Ext.BindFramebuffer(FramebufferTarget.DrawFramebuffer, fbo);
 
            // assign texture to FBO
            GL.Ext.FramebufferTexture2D(FramebufferTarget.DrawFramebuffer, FramebufferAttachment.ColorAttachment0Ext, TextureTarget.Texture2D, texture, 0);
 
            #region Test for Error
           // verbose error handling code here ...
 
            GL.Ext.BindFramebuffer(FramebufferTarget.FramebufferExt, 0); // disable rendering into the FBO

Note the last line, which makes sure that default rendering in on screen rather than in the FBO. It's not strictly necessary here, but I think it's a good habit to get into.

Next we'll generate random data, and render it in texture1.

        private void RenderRandomStartTextureInFBO(int FBOHandle)
        {
            GL.Ext.BindFramebuffer(FramebufferTarget.DrawFramebuffer, FBOHandle);
            GL.PushAttrib(AttribMask.ViewportBit);
            {
                GL.Viewport(0, 0, size_w, size_h);
 
                // clear the screen in green, to make it very obvious what the clear affected. only the FBO, not the real framebuffer
                GL.ClearColor(0.0f, 1.0f, 0.0f, 1.0f);
                GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit | ClearBufferMask.StencilBufferBit);
 
                RenderRandomPoints();
            }
            GL.PopAttrib();
 
            GL.Ext.BindFramebuffer(FramebufferTarget.FramebufferExt, 0); // disable rendering into the FBO
        }
 
        private void RenderRandomPoints()
        {
            GL.PushAttrib(AttribMask.ColorBufferBit);
 
            // make sure no lingering textures are bound to draw vertices clearly.
            GL.BindTexture(TextureTarget.Texture2D, 0);
 
            GL.Begin(BeginMode.Points);
            {
                GL.Color3(1.0f, 0.0f, 0.0f);
                for (float x = 0; x <= size_w; x += 0.9f)
                {
                    for (float y = 0; y <= size_h; y+= 0.9f)
                    {
                        if (rnd.Next(2) == 0)
                            GL.Color3(1.0f, 0.0f, 0.0f);
                        else
                            GL.Color3(0.0f, 0.0f, 0.0f);
                        GL.Vertex2(-1.0 + 2 * x / (float)size_w, -1.0 + 2 * y / (float)size_h);
                    }
                }
            }
 
            GL.Color4(1.0f, 1.0f, 1.0f, 1.0f);
            GL.End();
            GL.PopAttrib();
        }

Note the GL.Clear() command with a green background. This would render the background to green on screen if we didn't have a FBO attached, which we do in the first line.

Another gotcha is to make sure texturing is off when we begin rendering random points. This is done with GL.BindTexture(TextureTarget.Texture2D, 0);.

The for loop has x and y as floats. The natural approach of treating them as ints and incrementing by one doesn't work correctly here, as some squares will be missed. Try changing them to ints, you'll see green bands where some squares were not hit. This comes from the float->int interpolation.

The fragment shader was the most fun part of this tutorial to write! It's written for clarity, not efficiency. A few characters of code changing is all it takes to modify the rules of life for some fun variants. The heart of the shader code is here

 bool wasAlive = isRed(c);
 
 if (wasAlive){
  // if we dont have 2 or 3 neighbors, die
  if (count != 2 && count !=3){
    gl_FragColor.rgb = vec3(0.0, 0.0, 0.0);
    gl_FragColor.r = color.r * 0.49;
  }
  else{
    gl_FragColor.rgb = vec3(1.0, 0.0, 0.0);
  }
 }
 else{
  // spawn if 3 neighbors
 if (count == 3){
    gl_FragColor.rgb = vec3(1.0, 0.0, 0.0);
  }
 else{
    // fade away slowly
    gl_FragColor.rgb = vec3(0.0, 0.0, 0.0);
    gl_FragColor.r = color.r - 0.002;
  } 
 }

Red cells are alive, black ones are dead. Recently dead cells get their red value cut in half, then progressively fade out over time. It's interesting to note that if the fadeout value is changed from 0.002 to 0.001, it doesn't fade at all! This is because of the float->int conversion back to the texture. The change is not large enough to make a change to the int representation of the color value when encoded back to the texture.

In the OnRenderFrame function, we make sure to set Orthographic projection. We keep this mode for rendering to the texture as well. First we render the contents of textures 1 and 2 in their own rectangles:

            // draw a square that holds texture1 in upper left
            GL.UseProgram(0);
            GL.BindTexture(TextureTarget.Texture2D, texture1);
            GL.Begin(BeginMode.Quads);
            GL.TexCoord2(0.0f, 0.0f); GL.Vertex3(-1.0f, -1.0f + 2* third, 0.0f);
            GL.TexCoord2(0.0f, 1.0f); GL.Vertex3(-1.0f, -1.0f + 2* third + third, 0.0f);
            GL.TexCoord2(1.0f, 1.0f); GL.Vertex3(-1.0f + third, -1.0f + 2* third + third, 0.0f);
            GL.TexCoord2(1.0f, 0.0f); GL.Vertex3(-1.0f + third, -1.0f + 2*third, 0.0f);
            GL.End();
 
            // now render texture 2 in a square in upper right
            GL.UseProgram(0);
            GL.Viewport(ClientRectangle.X, ClientRectangle.Y, ClientRectangle.Width, ClientRectangle.Height);
            GL.BindTexture(TextureTarget.Texture2D, texture2);
            GL.Begin(BeginMode.Quads);
            GL.TexCoord2(0.0f, 0.0f); GL.Vertex3(-1.0f + 2 * third, -1.0f + 2* third, 0.0f);
            GL.TexCoord2(0.0f, 1.0f); GL.Vertex3(-1.0f + 2 * third, -1.0f + 2* third + third, 0.0f);
            GL.TexCoord2(1.0f, 1.0f); GL.Vertex3(-1.0f + third + 2 * third, -1.0f + 2* third + third, 0.0f);
            GL.TexCoord2(1.0f, 0.0f); GL.Vertex3(-1.0f + third + 2 * third, -1.0f + 2* third, 0.0f);
            GL.End();

Then we render texture 1 off screen into texture 2, through the FBO. This is switched every other frame.

            GL.UseProgram(shaderProgramHandle1);
            GL.Uniform1(GL.GetUniformLocation(shaderProgramHandle1, "MyTexture1"), 0);
            GL.Uniform1(GL.GetUniformLocation(shaderProgramHandle1, "pixel_w"), 1.0f / size_w);
            GL.Uniform1(GL.GetUniformLocation(shaderProgramHandle1, "pixel_h"), 1.0f / size_h);
            GL.Viewport(0, 0, size_w, size_h);
 
                    // run texture1 through a shader, and set to texture2
                    GL.Ext.BindFramebuffer(FramebufferTarget.DrawFramebuffer, FBOHandle1);
                    GL.BindTexture(TextureTarget.Texture2D, texture2);
                    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

Finally, let's draw a rectangle at the bottom of the screen that shows the full visual animation. This is what we would draw if we were presenting a simulation of Life to a user. Note that this texture is drawn as texture 1 half the time, and texture 2 half the time.

            // to give final "life" effect, render in a square alternating between texture 1 and 2 in bottom center
            GL.UseProgram(0);
            GL.Viewport(ClientRectangle.X, ClientRectangle.Y, ClientRectangle.Width, ClientRectangle.Height);
            if (evenFrame)
                GL.BindTexture(TextureTarget.Texture2D, texture2);
            else
                GL.BindTexture(TextureTarget.Texture2D, texture1);
            GL.Begin(BeginMode.Quads);
            GL.TexCoord2(0.0f, 0.0f); GL.Vertex3(-1.0f + 1 * third, -1.0f, 0.0f);
            GL.TexCoord2(0.0f, 1.0f); GL.Vertex3(-1.0f + 1 * third, -1.0f + third, 0.0f);
            GL.TexCoord2(1.0f, 1.0f); GL.Vertex3(-1.0f + third + 1 * third, -1.0f + third, 0.0f);
            GL.TexCoord2(1.0f, 0.0f); GL.Vertex3(-1.0f + third + 1 * third, -1.0f, 0.0f);
            GL.End();

Finally, a bit of user control. Users can press "F" for the simulation to run at full screen, and "S" for it to slow down. The space bar reseeds the textures with random data in case the simulation stalls to a stable sate.

        protected override void OnUpdateFrame(FrameEventArgs e)
        {
            base.OnUpdateFrame(e);
 
            if (Keyboard[Key.Escape])
                Exit();
            if (Keyboard[Key.F])
            {
                slow = false;
            }
            if (Keyboard[Key.S])
            {
                slow = true;
            }
            if (Keyboard[Key.Space])
            {
                RenderRandomStartTextureInFBO(FBOHandle1);
                RenderRandomStartTextureInFBO(FBOHandle2);
            }
        }

That's most of our code. The full source is below

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 GameOfLife
{
    class Game : GameWindow
    {
        int texture1, texture2, fragmentShaderHandle1, shaderProgramHandle1, FBOHandle1, FBOHandle2;
 
        bool evenFrame = false;
        bool tempo_counter = true;
        int size_w = 40;
        int size_h = 40;
        private double elapsedTime = 0;
        private bool slow = true;
 
        Random rnd = new Random();
 
        string fragmentShaderSource1 = @"
#version 400
uniform sampler2D MyTexture1;
uniform float pixel_w;
uniform float pixel_h;
bool isRed(vec2 coor);
 
void main(void)
{
// c is our current coordinate
// color is our current texture
 vec2 c = gl_TexCoord[0].xy;
 vec4 color = texture2D( MyTexture1, c);  
 
// keypad coordinates for neighbors -- ie: 8 => North, 3 => SE, etc...
 vec2 neighbor_1 = c;
 vec2 neighbor_2 = c;
 vec2 neighbor_3 = c;
 vec2 neighbor_4 = c;
 vec2 neighbor_6 = c;
 vec2 neighbor_7 = c;
 vec2 neighbor_8 = c;
 vec2 neighbor_9 = c;
 
// define neighbors
 neighbor_1.x -= pixel_w;
 neighbor_1.y -= pixel_h;
 
 neighbor_2.y -= pixel_h;
 
 neighbor_3.x += pixel_w;
 neighbor_3.y -= pixel_h;
 
 neighbor_4.x -= pixel_w;
 
 neighbor_6.x += pixel_h;
 
 neighbor_7.x -= pixel_w;
 neighbor_7.y += pixel_h;
 
 neighbor_8.y += pixel_h;
 
 neighbor_9.x += pixel_w;
 neighbor_9.y += pixel_h;
 
// calculate number of alive neighbors
 int count = 0;
 if (isRed(neighbor_1))
 {
    count++;
 }
 if (isRed(neighbor_2))
 {
    count++;
 }
 if (isRed(neighbor_3))
 {
    count++;
 }
 if (isRed(neighbor_4))
 {
    count++;
 }
 if (isRed(neighbor_6))
 {
    count++;
 }
 if (isRed(neighbor_7))
 {
    count++;
 }
 if (isRed(neighbor_8))
 {
    count++;
 }
 if (isRed(neighbor_9))
 {
    count++;
 }
 
 bool wasAlive = isRed(c);
 
 if (wasAlive){
  // if we dont have 2 or 3 neighbors, die
  if (count != 2 && count !=3){
    gl_FragColor.rgb = vec3(0.0, 0.0, 0.0);
    gl_FragColor.r = color.r * 0.49;
  }
  else{
    gl_FragColor.rgb = vec3(1.0, 0.0, 0.0);
  }
 }
 else{
  // spawn if 3 neighbors
 if (count == 3){
    gl_FragColor.rgb = vec3(1.0, 0.0, 0.0);
  }
 else{
    // fade away slowly
    gl_FragColor.rgb = vec3(0.0, 0.0, 0.0);
    gl_FragColor.r = color.r - 0.002;
  } 
 }
}
 
bool isRed(vec2 coor){
    vec4 color = texture2D( MyTexture1, coor );  
    if (color.r > 0.5){
        return true;
    }
    else{
        return false;
    }
}
 
";
        void CreateShaders()
        {
            // create shader
            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));
        }
 
        /// <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);
            int x = Width;
            CreateNullTexture(out texture1);
            CreateNullTexture(out texture2);
 
            CreateFBOandAssignTexture(out FBOHandle1, texture1);
            CreateFBOandAssignTexture(out FBOHandle2, texture2);
 
            // draw random points into the FBO that holds texture1 
            RenderRandomStartTextureInFBO(FBOHandle1);
 
            Console.WriteLine("Press F for fast clock, S for slow clock, Space to reseed textures");
        }
 
        private void CreateFBOandAssignTexture(out int fbo, int texture)
        {
            // create and bind an FBO
            GL.Ext.GenFramebuffers(1, out fbo);
            GL.Ext.BindFramebuffer(FramebufferTarget.DrawFramebuffer, fbo);
 
            // assign texture to FBO
            GL.Ext.FramebufferTexture2D(FramebufferTarget.DrawFramebuffer, FramebufferAttachment.ColorAttachment0Ext, TextureTarget.Texture2D, texture, 0);
 
            #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 " + fbo + " 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;
                    }
            }
 
            #endregion Test for Error
            GL.Ext.BindFramebuffer(FramebufferTarget.FramebufferExt, 0); // disable rendering into the FBO
        }
 
        private void RenderRandomStartTextureInFBO(int FBOHandle)
        {
            GL.Ext.BindFramebuffer(FramebufferTarget.DrawFramebuffer, FBOHandle);
            GL.PushAttrib(AttribMask.ViewportBit);
            {
                GL.Viewport(0, 0, size_w, size_h);
 
                // clear the screen in green, to make it very obvious what the clear affected. only the FBO, not the real framebuffer
                GL.ClearColor(0.0f, 1.0f, 0.0f, 1.0f);
                GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit | ClearBufferMask.StencilBufferBit);
 
                RenderRandomPoints();
            }
            GL.PopAttrib();
 
            GL.Ext.BindFramebuffer(FramebufferTarget.FramebufferExt, 0); // disable rendering into the FBO
        }
 
        private void RenderRandomPoints()
        {
            GL.PushAttrib(AttribMask.ColorBufferBit);
 
            // make sure no lingering textures are bound to draw vertices clearly.
            GL.BindTexture(TextureTarget.Texture2D, 0);
 
            GL.Begin(BeginMode.Points);
            {
                GL.Color3(1.0f, 0.0f, 0.0f);
                for (float x = 0; x <= size_w; x += 0.9f)
                {
                    for (float y = 0; y <= size_h; y+= 0.9f)
                    {
                        if (rnd.Next(2) == 0)
                            GL.Color3(1.0f, 0.0f, 0.0f);
                        else
                            GL.Color3(0.0f, 0.0f, 0.0f);
                        GL.Vertex2(-1.0 + 2 * x / (float)size_w, -1.0 + 2 * y / (float)size_h);
                    }
                }
            }
 
            GL.Color4(1.0f, 1.0f, 1.0f, 1.0f);
            GL.End();
            GL.PopAttrib();
        }
 
        private void CreateNullTexture(out int texture)
        {
            // load texture 
            GL.GenTextures(1, out texture);
 
            // Still required else TexImage2D will be applyed on the last bound texture
            GL.BindTexture(TextureTarget.Texture2D, texture);
 
            // generate null texture
            GL.TexImage2D(TextureTarget.Texture2D, 0, PixelInternalFormat.Rgba, size_w, size_h, 0,
            OpenTK.Graphics.OpenGL.PixelFormat.Bgra, PixelType.UnsignedByte, IntPtr.Zero);
 
            // set filtering to nearest so we get "atari 2600" look
            GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Nearest);
            GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Nearest);
        }
 
        /// <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);
        }
 
        /// <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();
            if (Keyboard[Key.F])
            {
                slow = false;
            }
            if (Keyboard[Key.S])
            {
                slow = true;
            }
            if (Keyboard[Key.Space])
            {
                RenderRandomStartTextureInFBO(FBOHandle1);
                RenderRandomStartTextureInFBO(FBOHandle2);
            }
        }
 
        /// <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);
            GL.ClearColor(0.4f, 0.3f, 0.9f, 0f);
 
            GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);
 
            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);
 
            float third = 2.0f / 3.0f;
 
            // draw a square that holds texture1 in upper left
            GL.UseProgram(0);
            GL.BindTexture(TextureTarget.Texture2D, texture1);
            GL.Begin(BeginMode.Quads);
            GL.TexCoord2(0.0f, 0.0f); GL.Vertex3(-1.0f, -1.0f + 2* third, 0.0f);
            GL.TexCoord2(0.0f, 1.0f); GL.Vertex3(-1.0f, -1.0f + 2* third + third, 0.0f);
            GL.TexCoord2(1.0f, 1.0f); GL.Vertex3(-1.0f + third, -1.0f + 2* third + third, 0.0f);
            GL.TexCoord2(1.0f, 0.0f); GL.Vertex3(-1.0f + third, -1.0f + 2*third, 0.0f);
            GL.End();
 
            // now render texture 2 in a square in upper right
            GL.UseProgram(0);
            GL.Viewport(ClientRectangle.X, ClientRectangle.Y, ClientRectangle.Width, ClientRectangle.Height);
            GL.BindTexture(TextureTarget.Texture2D, texture2);
            GL.Begin(BeginMode.Quads);
            GL.TexCoord2(0.0f, 0.0f); GL.Vertex3(-1.0f + 2 * third, -1.0f + 2* third, 0.0f);
            GL.TexCoord2(0.0f, 1.0f); GL.Vertex3(-1.0f + 2 * third, -1.0f + 2* third + third, 0.0f);
            GL.TexCoord2(1.0f, 1.0f); GL.Vertex3(-1.0f + third + 2 * third, -1.0f + 2* third + third, 0.0f);
            GL.TexCoord2(1.0f, 0.0f); GL.Vertex3(-1.0f + third + 2 * third, -1.0f + 2* third, 0.0f);
            GL.End();
 
 
            GL.UseProgram(shaderProgramHandle1);
            GL.Uniform1(GL.GetUniformLocation(shaderProgramHandle1, "MyTexture1"), 0);
            GL.Uniform1(GL.GetUniformLocation(shaderProgramHandle1, "pixel_w"), 1.0f / size_w);
            GL.Uniform1(GL.GetUniformLocation(shaderProgramHandle1, "pixel_h"), 1.0f / size_h);
            GL.Viewport(0, 0, size_w, size_h);
 
            elapsedTime += e.Time;
 
            if (tempo())
            {
                if (!evenFrame)
                {
                    evenFrame = true;
                    // run texture2 through a shader, and set to texture1
                    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
                }
            }
            else
            {
                if (evenFrame)
                {
                    evenFrame = false;
                    // run texture1 through a shader, and set to texture2
                    GL.Ext.BindFramebuffer(FramebufferTarget.DrawFramebuffer, FBOHandle1);
                    GL.BindTexture(TextureTarget.Texture2D, texture2);
                    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
                }
            }
 
            // to give final "life" effect, render in a square alternating between texture 1 and 2 in bottom center
            GL.UseProgram(0);
            GL.Viewport(ClientRectangle.X, ClientRectangle.Y, ClientRectangle.Width, ClientRectangle.Height);
            if (evenFrame)
                GL.BindTexture(TextureTarget.Texture2D, texture2);
            else
                GL.BindTexture(TextureTarget.Texture2D, texture1);
            GL.Begin(BeginMode.Quads);
            GL.TexCoord2(0.0f, 0.0f); GL.Vertex3(-1.0f + 1 * third, -1.0f, 0.0f);
            GL.TexCoord2(0.0f, 1.0f); GL.Vertex3(-1.0f + 1 * third, -1.0f + third, 0.0f);
            GL.TexCoord2(1.0f, 1.0f); GL.Vertex3(-1.0f + third + 1 * third, -1.0f + third, 0.0f);
            GL.TexCoord2(1.0f, 0.0f); GL.Vertex3(-1.0f + third + 1 * third, -1.0f, 0.0f);
            GL.End();
 
            SwapBuffers();
    }
 
        private bool tempo()
        {
            tempo_counter = !tempo_counter;
            if (slow)
                return ((int)(elapsedTime) % 2 == 0);
            else
                return tempo_counter;
 
        }
 
        /// <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);
            }
        }
    }
}
Images

Comments

Comment viewing options

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

This is an interesting example, thanks for posting.

One thing I will point out, is I had difficulty in originally getting the sample to work correctly as posted above, as the shader was unable to run by default - the following errosr would be displayed in the Immediate window each time you started the program:

0(3) : warning C7568: #version 400 not fully supported on current GPU target profile
0(12) : error C7533: global variable gl_TexCoord is deprecated after version 120
0(86) : warning C7533: global variable gl_FragColor is deprecated after version 120

I assume the first line is because my graphics card is a couple of years old.

Changing the version to 120 caused the sample project to run correctly, without anything being printed in the Immediate window. Anything higher than this seemed to be broken as the the textures were displayed as a coarse dither and did not update.

james_lohr's picture

It didn't work for me either, until I changed the version to 120. Otherwise it's a great little example of how to use FBOs and how to use shaders. Thanks a lot. :)