Friday, September 9, 2016

Define: Rendering to a Texture

Rendering to a texture


Now that we know how framebuffers (sort of) work it's time to put them to good use. We're going to render the scene into a color texture attached to a framebuffer object we created and then draw this texture over a simple quad that spans the whole screen. The visual output is then exactly the same as without a framebuffer, but this time it's all printed on top of a single quad. Now why is this useful? In the next section we'll see why.

First thing to do is to create an actual framebuffer object and bind it, this is all relatively straightforward:

GLuint framebuffer;
glGenFramebuffers(1, &framebuffer);
glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);    

Next we create a texture image that we attach as a color attachment to the framebuffer. We set the texture's dimensions equal to the width and height of the window and keep its data uninitialized:


// Generate texture
GLuint texColorBuffer;
glGenTextures(1, &texColorBuffer);
glBindTexture(GL_TEXTURE_2D, texColorBuffer);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, 800, 600, 0, GL_RGB, GL_UNSIGNED_BYTE, NULL);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR );
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glBindTexture(GL_TEXTURE_2D, 0);

// Attach it to currently bound framebuffer object
glFramebufferTexture2D(GL_FRAMEBUFFER



                       , GL_COLOR_ATTACHMENT0                                            , GL_TEXTURE_2D, texColorBuffer, 0);  


We also want to make sure OpenGL is able to do depth testing (and optionally stencil testing if you're into that) so we have to make sure to add a depth (and stencil) attachment to the framebuffer as well. Since we'll only be sampling the color buffer and not the other buffers we can create a renderbuffer object for this purpose. Remember that they're a good choice when you're not going to sample from the specific buffer(s)?

Creating a renderbuffer object isn't too hard. The only thing we have to remember is that we're creating it as a depth and stencil attachment renderbuffer object. We set its internal format to GL_DEPTH24_STENCIL8 which is enough precision for our purposes.


GLuint rbo;
glGenRenderbuffers(1, &rbo);
glBindRenderbuffer(GL_RENDERBUFFER, rbo); 
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, 800, 600);  
glBindRenderbuffer(GL_RENDERBUFFER, 0);


Once we've allocated enough memory for the renderbuffer object we can unbind the renderbuffer.
Then, as a final step before we can complete the framebuffer, we attach the renderbuffer object to the depth and stencil attachment of the framebuffer:


glFramebufferRenderbuffer(GL_FRAMEBUFFER




                         , GL_DEPTH_STENCIL_ATTACHMENT



                         , GL_RENDERBUFFER, rbo);

Then as a final measure we want to check if the framebuffer is actually complete and if it's not, we print an error message.


if(glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
 cout << "ERROR::FRAMEBUFFER:: Framebuffer is not complete!" << endl;
glBindFramebuffer(GL_FRAMEBUFFER, 0);  



Then also be sure to unbind the framebuffer to make sure we're not accidentally rendering to the wrong framebuffer.

Now that the framebuffer is complete all we need to do to render to the framebuffer's buffers instead of the default framebuffers is simply bind to the framebuffer object. All subsequent rendering commands will then influence the currently bound framebuffer. All the depth and stencil operations will also read from the currently bound framebuffer's depth and stencil attachments if they're available. If you were to omit a depth buffer for example, all depth testing operations will no longer work, because there's not a depth buffer present in the currently bound framebuffer.

So, to draw the scene to a single texture we'll have to take the following steps:
  • Render the scene as usual with the new framebuffer bound as the active framebuffer.
  • Bind to the default framebuffer.
  • Draw a quad that spans the entire screen with the new framebuffer's color buffer as its texture.

We'll draw the same scene we've used in the depth testing tutorial, but this time with the old-school container texture.

To draw the quad we're going to create a fresh set of simple shaders. We're not going to include any fancy matrix transformations since we'll just be supplying the vertex coordinates as normalized device coordinates so we can directly specify them as the output of the vertex shader. The vertex shader looks like this:


#version 330 core
layout (location = 0) in vec2 position;
layout (location = 1) in vec2 texCoords;

out vec2 TexCoords;

void main()
{
    gl_Position = vec4(position.x, position.y, 0.0f, 1.0f); 
    TexCoords = texCoords;
}  

Nothing too fancy. The fragment shader will be even more basic since the only thing we have to do is sample from a texture:


#version 330 core
in vec2 TexCoords;
out vec4 color;

uniform sampler2D screenTexture;

void main()
{ 
    color = texture(screenTexture, TexCoords);
}

It is then up to you to create and configure a VAO for the screen quad. A render iteration of the framebuffer procedure then has the following structure:


// First pass
glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
glClearColor(0.1f, 0.1f, 0.1f, 1.0f);




 // We're not using stencil buffer now
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);



glEnable(GL_DEPTH_TEST);
DrawScene(); 
  
// Second pass
glBindFramebuffer(GL_FRAMEBUFFER, 0); // back to default
glClearColor(1.0f, 1.0f, 1.0f, 1.0f); 
glClear(GL_COLOR_BUFFER_BIT);
  
screenShader.Use();  
glBindVertexArray(quadVAO);
glDisable(GL_DEPTH_TEST);
glBindTexture(GL_TEXTURE_2D, textureColorbuffer);
glDrawArrays(GL_TRIANGLES, 0, 6);
glBindVertexArray(0);  



There are a few things to note. First, since each framebuffer we're using has its own set of buffers, we want to clear each of those buffers with the appropriate bits set by calling glClear. Second, when drawing the quad, we're disabling depth testing since we don't really care about depth testing because we're drawing a simple quad; we'll have to enable depth testing again when we draw the normal scene though.

There are quite some steps that could go wrong here, so if you have no output, try to debug where possible and re-read the relevant sections of the tutorial. If everything did work out successfully you'll get a visual result that looks like this:

An image of a 3D scene in OpenGL rendered to a texture via framebuffers






The left shows the visual output which is exactly the same as we've seen in the depth testing tutorial, but this time, rendered to a simple quad. If we render the scene in wireframe it becomes obvious we've only drawn a single quad in the default framebuffer.

You can find the source code of the application here.

So what was the use of this again? Well, because we can now freely access each of the pixels of the completely rendered scene as a single texture image, we can create some interesting effects in the fragment shader. The combination of all these interesting effects are called post-processing effects.













No comments:

Post a Comment