Render To Texture in OpenGL

Render To Texture (RTT) technique is one of the most important things to know in modern computer graphics programming. It’s heavily used in rendering complex scenes because it lets you create a bunch of effects and it open doors for other techniques like shadow mapping, deferred lighting or post-processing. Although the definition of the following terms and techniques is not the same, they are related to this method. So when you hear about: Multi-Pass Rendering, Off-Screen Rendering, Render Target Texture, Layered Rendering; you will know that they deal somehow with this Render To Texture technique, so don’t get confused right here.

In this article we will talk about the following topics:

  1. How Render To Texture works?
  2. Framebuffers Objects (FBOs)
  3. Our main scene
  4. Initialize Framebuffer
  5. Render to texture
  6. Depth Texture
  7. Blur effect
  8. MRT and multiple FBO in Part 2

  1. How Render To Texture works?

The concept behind this method is super simple, instead of rendering directly into the back buffer (main buffer) you will render the objects from your scene into a intermediate buffer which creates a texture or multiple texture (in a complex scenario). This texture is used to generate some cool effects and finally render it to the back buffer (main buffer).

Render to texture concept

  1. Framebuffers Objects (FBOs)

In OpenGL we can implement this technique by using Framebuffer objects (FBOs). These FBOs are not actually buffers, they just hold attachements (actual buffers) to read or write them during rendering time. The Framebuffer is llke a manager for these attachements which in the end they are textures or Renderbuffers (not covered here). Framebuffers (FBOs) were introduced in OpenGL core since version 3.0 was out and they offers a great control over how the textures are stored and used. Prior to version 3.0 you had to use ARB extensions. Now let’s see the main steps that we have to follow in order to get things done:

draw_to_texture_OpenGL

  1. Our main scene

For this tutorial we need to begin with a scene. You can create a new scene with some objects and lights or use one scene that you already have created. I used my scene from Rim Lighting tutorial (source code provided) so feel free to use it. Initially my scene is rendered directly into main buffer so we have just a single pass.

Basic Scene

Basic Scene

 

  1. Initialize Framebuffer

Now, what we have to do, is to create a class with a few methods that will generate a FBO and attach a texture to it. This texture that is attached to the FBO will be filled in Pass 1 and rendered in a second pass Pass 2. It’s usually better to create a class that handles the FBOs because sometimes you may want to create more complex stuff and you need more than 2 passes (multiple passes), so writing a class it’s a good practice to keep your code structured and easy to follow.

//Generate FBO and two empty textures
void GenerateFBO(unsigned int width, unsigned int height)
{
 glGenFramebuffers(1, &FBO);                     //Generate a framebuffer object(FBO)
 glBindFramebuffer(GL_FRAMEBUFFER, FBO);         // and bind it to the pipeline
        
 generateColorTexture(width, height);//generate empty texture
 generateDepthTexture(width, height);//generate empty texture
          
 unsigned int attachment_index_color_texture = 0;   //to keep track of our textures
 //bind textures to pipeline. texture_depth is optional .0 is the mipmap level. 0 is the heightest
 glFramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0 + attachment_index_color_texture, texture_color, 0);
 glFramebufferTexture(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, texture_depth, 0);//optional

 drawbuffer.push_back(GL_COLOR_ATTACHMENT0 + attachment_index_color_texture);    //add attachements

 if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE){  //Check for FBO completeness
 	std::cout << "Error! FrameBuffer is not complete" << std::endl;
 }

 glBindFramebuffer(GL_FRAMEBUFFER, 0);    //unbind framebuffer

}

Let’s focus a little bit on the most important method from the class that we have created above: the GenerateFBO method (don’t worry the rest of the class is in the cpp file at the end of this article). As the name implies this method generates a FBO in a few steps:

try1

  1. First thing we have to do is to create the actual framebuffer object using OpenGL’s method glGenFramebuffers(GLsizei n, GLuint * framebuffers). This method takes two parameters: the number of FBOs we want to create and their ID’s as a list. For the moment we want to create just one FBO.
  1. Next step is to use our new created FBO instead of the default one. So we have to bind it to the pipeline using OpenGL’s method glBindFramebuffer (GLenum target, GLuint framebuffer)The first parameter sets the FBO behaviour. There are three types of targets that we can use here. We can set it just for reading data, using GL_READ_FRAMEBUFFER. We can set the target just for drawing data, using GL_DRAW_FRAMEBUFFER(including stencil and depth values). And finally we can use GL_FRAMEBUFFER which reads and writes data. During the flow of your game you have to go with the same target for your framebuffer.
  1. Excellent, we have an empty framebuffer which is tied to the pipeline. Now we can create two empty textures one for colors and the other one for depth values (this is optional). I created two methods  to create these textures. Keep in mind that when you resize your window, you have to resize these textures too and call our class method GenerateFBO.
  1. We created these textures but they are still not bound to our framebuffer. To do that we will use OpenGL’s method glFramebufferTexture(GLenum target, Glenum attachment, Gluint texture, GLint level)We must use the same target that we used when we bound our FBO. The next parameter tells what kind of textures we want to attach. We can use GL_COLOR_ATTACHMENTn to attach a color texture to color buffer, where n is usually 8. If you want to use attach a texture to depth buffer use GL_DEPTH_ATTACHMENT and for stencil use GL_STENCIL_ATTACHMENT. The third param is the actual texture that we have just created. And finally the last parameter is the mipmap level of the texture, where 0 is the biggest level.
  1. Next we have to tell OpenGL in which buffers he is going to draw. We can set them his using OpenGL’s method glDrawBuffers(GLsizei n, const GLenum *bufs), where bufs is a vector enum with our attachments and n is the size of the vector. Notice that we don’t pass the GL_DEPTH_ATTACHMENT.
  1. Next we have to check for framebuffer completeness to see if the framebuffer was set up right.
  1. Finally unbind this FBO by binding the default framebuffer and continue with other things in your program intialization.

 

  1. Render to texture

Now our FBO is set and ready to draw our scene into its buffers. Before we render our scene, we must create a quad where we will render the generated textures from our FBO in Pass 2. To create the quad we need to provide 4 vertices in NDC (Normalized Device Coordinates) and 6 indices (more about NDC here). Because we want to see these textures we have to create a basic vertex shader to draw a quad in NDC and texture it in fragment shader. To keep article clear I didn’t put quad creation and shader creation code here but you can find it my source code.

Beside GenerateFBO method I created two more simple methods which binds and unbinds our FBO in the drawing loop.

  • bind() which bind our framebuffer object to pipeline using OpenGL’s method glBindFramebuffer(GL_FRAMEBUFFER, FBO),
  • unbind() which unbind the our framebuffer object and bind the default OpenGL framebuffer using glBindFramebuffer(GL_FRAMEBUFFER, 0);

//main drawing method
void notifyDisplayFrame(){
    
    //PASS 1 ->draw into FBO
    fbo.bind();
    {
        //draw rim lighting scene like you normally do
    }
    fbo.unbind();
    
    //PASS 2 -> daw to default FBO(backbuffer)
    {
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    
       	glUseProgram(rtt_program_shader);
    	glActiveTexture(GL_TEXTURE0 + 1);
        //use texture from our FBO generated in PASS 1     
    	glBindTexture(GL_TEXTURE_2D, fbo.getColorTexture());
    	glUniform1i(glGetUniformLocation(rtt_program_shader, "texture_color"), 1);
    	
        glBindVertexArray(rtt_vao);
    	glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
    }
    
}

render pass1 and pass2

We draw the texture from Pass 1 into a quad from Pass 2. Pass2 is the final pass

Because we didn’t do anything with our generated texture the result after Pass 2 is exactly the same as after Pass 1.

  1. Depth Texture

First thing you want to do is to see how depth buffer looks like. We already told in GenerateFBO to create a depth texture and attach it to FBO. Now we draw depth buffer into the depth texture in Pass 1 and use it in Pass 2. We have to slightly modify the  Pass 2 render code:

    //PASS 2 -> daw to default FBO(backbuffer)
    {
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    
       	glUseProgram(rtt_program_shader);
    	glActiveTexture(GL_TEXTURE0 + 1);
        //use texture from our FBO generated in PASS 1   
        glBindTexture(GL_TEXTURE_2D, fbo.getColorTexture());
        glUniform1i(glGetUniformLocation(rtt_program_shader, "texture_color"), 1);
    
        glActiveTexture(GL_TEXTURE0 + 2);
        //use depth texture from our FBO generated in PASS 1   
    	glBindTexture(GL_TEXTURE_2D, fbo.getDepthTexture());
        glUniform1i(glGetUniformLocation(rtt_program_shader, "texture_depth"), 2);
        
        glBindVertexArray(rtt_vao);
    	glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
    }

Also we have to modify the fragment shader used to draw the texture over the quad. Beacuse depth values are small, making them hard to see, we have to change them into a visible spectra by raising it to power (modify scale). Depth values are the same on RGB channels(greyscale), so we can use any channel in our math function.

//ddetph texture modified spectra fragment shader 
#version 330

layout(location = 0) out vec4 out_color;

uniform sampler2D texture_color;
uniform sampler2D texture_depth;
in vec2 texcoord;

vec3 depth(){
    float v = pow(texture(texture_depth, texcoord).r , 256);
	return vec3(v,v,v);
}

void main(){

   //out_color = vec4(texture(textura_color, texcoord).rgb, 1);
   out_color = vec4(texture(textura_depth, texcoord).rgb, 1);
}

depth texture

Depth texture with visible spectra

Depth Texture Flow

We draw the depth texture from Pass 1 into a quad from Pass 2. Pass2 is the final pass

  1. Blur effect

Another thing we can quickly do is blur. Blur is just a post processing effect that we can create with render to texture technique. You can use blur in games to make some cool effects like bloom or noise. To create blur we have to compute some weights based on the size of your texture. After that  we sum up neighbour values to current pixel. Finally you sample this value with a value. You can take these values and change them and see what’s best for you.

//blur fragment shader
#version 330

layout(location = 0) out vec4 out_color;

uniform sampler2D texture_color;
uniform sampler2D texture_depth;//not used here 

uniform int screen_width, screen_height;

in vec2 texcoord;

vec3 blur(){
	float dx = 1.0f/screen_width;//step on x
	float dy = 1.0f/screen_height;//step on y
    
	vec3 sum = vec3(0,0,0);
	for(int i = -3; i< 3; i++) 
	   for(int j = -3;j < 3; j++) 
		sum += texture(textura_color, texcoord + vec2(i * dx, j * dy)).rgb;
	return sum/30;
}

void main(){

	out_color = vec4(blur(),1);
}

blur image

blur image

Don’t forget to send width and height uniforms to fragment shader. And don’t look to much at the image or you will have a headache :).

blur flow

We draw the texture from Pass 1 into a quad from Pass 2. Pass2 is the final pass

In my next article I will talk about Multiple Framebuffer Objects and Multiple Render Targets (MRT).

Source Code: render_To_texture_rtt My code is structured from one of my school projects and it should be used only for learning purpose.

Further reading:

Tutorial 14: Render To Texture

Lighthouse3D.com

OpenGL SuperBible 6th Edition

OpenGL 4.0 Shading Language CookBook


blog comments powered by Disqus