Rendering to a texture

Sometimes it makes sense to generate textures "on the fly" during the execution of the program. The texture could be a pattern that is generated from some internal algorithm (a so-called procedural texture), or it could be that the texture is meant to represent another portion of the scene. An example of the latter case might be a video screen where one can see another part of the "world", perhaps via a security camera in another room. The video screen could be constantly updated as objects move around in the other room, by re-rendering the view from the security camera to the texture that is applied to the video screen!

In the following image, the texture appearing on the cube was generated by rendering a teapot to an internal texture and then applying that texture to the faces of the cube.

Rendering to a texture

In recent versions of OpenGL, rendering directly to textures has been greatly simplified with the introduction of framebuffer objects (FBOs). We can create a separate rendering target buffer (the FBO), attach our texture to that FBO, and render to the FBO in exactly the same way that we would render to the default framebuffer. All that is required is to swap in the FBO, and swap it out when we are done.

Basically, the process involves the following steps when rendering:

  1. Bind to the FBO.
  2. Render the texture.
  3. Unbind from the FBO (back to the default framebuffer).
  4. Render the scene using the texture.

There's actually not much that we need to do on the GLSL side in order to use this kind of texture. In fact, the shaders will see it as any other texture. However, there are some important points that we'll talk about regarding fragment output variables.

In this example, we'll cover the steps needed to create the FBO and its backing texture, and how to set up a shader to work with the texture.

Getting ready

For this example, we'll use the shaders from the previous recipe Applying a 2D texture, with some minor changes. Set up your OpenGL program as described in that recipe. The only change that we'll make to the shaders is changing the name of the sampler2D variable from Tex1 to Texture.

How to do it...

To render to a texture and then apply that texture to a scene in a second pass, use the following steps:

  1. Within the main OpenGL program, use the following code to set up the framebuffer object:
    GLuint fboHandle;  // The handle to the FBO
    
    // Generate and bind the framebuffer
    glGenFramebuffers(1, &fboHandle);
    glBindFramebuffer(GL_FRAMEBUFFER, fboHandle);
    
    // Create the texture object
    GLuint renderTex;
    glGenTextures(1, &renderTex);
    glActiveTexture(GL_TEXTURE0);  // Use texture unit 0
    glBindTexture(GL_TEXTURE_2D, renderTex);
    glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, 512, 512);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, 
                    GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, 
                    GL_LINEAR);
    
    // Bind the texture to the FBO
    glFramebufferTexture2D(GL_FRAMEBUFFER,GL_COLOR_ATTACHMENT0, 
                           GL_TEXTURE_2D, renderTex, 0);
    
    // Create the depth buffer
    GLuint depthBuf;
    glGenRenderbuffers(1, &depthBuf);
    glBindRenderbuffer(GL_RENDERBUFFER, depthBuf);
    glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT, 
                          512, 512);
    
    // Bind the depth buffer to the FBO
    glFramebufferRenderbuffer(GL_FRAMEBUFFER, 
                              GL_DEPTH_ATTACHMENT,
                              GL_RENDERBUFFER, depthBuf);
    
    // Set the target for the fragment shader outputs
    GLenum drawBufs[] = {GL_COLOR_ATTACHMENT0};
    glDrawBuffers(1, drawBufs);
    
    // Unbind the framebuffer, and revert to default
    glBindFramebuffer(GL_FRAMEBUFFER, 0);
  2. Use the following code to create a simple 1 x 1 texture that can be used as a "non-texture texture". Note that we place this one in texture unit 1:
    // One pixel white texture
    GLuint whiteTexHandle;
    GLubyte whiteTex[] = { 255, 255, 255, 255 };
    glActiveTexture(GL_TEXTURE1);
    glGenTextures(1, &whiteTexHandle);
    glBindTexture(GL_TEXTURE_2D,whiteTexHandle);
    glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, 1, 1);
    glTexSubImage2D(GL_TEXTURE_2D,0,0,0,1,1,GL_RGBA,
                 GL_UNSIGNED_BYTE,whiteTex);
  3. In your render function within the OpenGL program, use the following code, or something similar:
    // Bind to texture's FBO
    glBindFramebuffer(GL_FRAMEBUFFER, fboHandle);
    glViewport(0,0,512,512);  // Viewport for the texture
    // Use the "white" texture here
    int loc = glGetUniformLocation(programHandle, "Texture");
    glUniform1i(loc, 1);
    
    // Setup the projection matrix and view matrix
    // for the scene to be rendered to the texture here.
    // (Don't forget to match aspect ratio of the viewport.)
    
    
    renderTextureScene();
    
    // Unbind texture's FBO (back to default FB)
    glBindFramebuffer(GL_FRAMEBUFFER, 0);
    glViewport(0,0,width,height);  // Viewport for main window
    
    // Use the texture that is associated with the FBO
    int loc = glGetUniformLocation(programHandle, "Texture");
    glUniform1i(loc, 0);
    
    // Reset projection and view matrices here
    
    
    renderScene();

How it works...

Let's start by looking at the code for creating the framebuffer object (the preceding step 1). Our FBO will be 512 pixels square because we intend to use it as a texture. We begin by generating the FBO using glGenFramebuffers and binding the framebuffer to the GL_FRAMEBUFFER target with glBindFramebuffer. Next, we create the texture object to which we will be rendering, and use glActiveTexture to select texture unit zero. The rest is very similar to creating any other texture. We allocate space for the texture using glTexStorage2D. We don't need to copy any data into that space (using glTexSubImage2D), because we'll be writing to that memory later when rendering to the FBO.

Next, we link the texture to the FBO by calling the function glFramebufferTexture2D. This function attaches a texture object to an attachment point in the currently bound framebuffer object. The first argument (GL_FRAMEBUFFER) indicates that the texture is to be attached to the FBO currently bound to the GL_FRAMEBUFFER target. The second argument is the attachment point. Framebuffer objects have several attachment points for color buffers, one for the depth buffer, and a few others. This allows us to have several color buffers to target from our fragment shaders. We'll see more about this later. We use GL_COLOR_ATTACHMENT0 to indicate that this texture is linked to color attachment 0 of the FBO. The third argument (GL_TEXTURE_2D) is the texture target, and the fourth (renderTex) is the handle to our texture. The last argument (0) is the mip-map level of the texture that is being attached to the FBO. In this case, we only have a single level, so we use a value of zero.

As we want to render to the FBO with depth testing, we need to also attach a depth buffer. The next few lines of code create the depth buffer. The function glGenRenderbuffer creates a renderbuffer object, and glRenderbufferStorage allocates space for the renderbuffer. The second argument to glRenderbufferStorage indicates the internal format for the buffer, and as we are using this as a depth buffer, we use the special format GL_DEPTH_COMPONENT.

Next, the depth buffer is attached to the GL_DEPTH_ATTACHMENT attachment point of the FBO using glFramebufferRenderbuffer.

The shader's output variables are assigned to the attachments of the FBO using glDrawBuffers. The second argument to glDrawBuffers is an array indicating the FBO buffers to be associated with the output variables. The ith element of the array corresponds to the fragment shader output variable at location i. In our case, we only have one shader output variable (FragColor) at location zero. This statement associates that output variable with GL_COLOR_ATTACHMENT0.

The last statement in step 1 unbinds the FBO to revert back to the default framebuffer.

Step 2 creates a 1 x 1 white texture in texture unit one. We use this texture when rendering the texture so that we don't need to change anything about our shader. As our shader multiplies the texture color by the result of the Phong reflection model, this texture will effectively work as a "non-texture" because multiplying will not change the color. When rendering the texture, we want to use this "non-texture", but when rendering the scene, we'll use the texture attached to the FBO.

Note

This use of a 1 x 1 texture is certainly not necessary in general. We use it here just so that we can draw to the FBO without a texture being applied to the scene. If you have a texture that should be applied, then that would be more appropriate here.

In step 3 (within the render function), we bind to the FBO, use the "non-texture" in unit one, and render the texture. Note that we need to be careful to set up the viewport (glViewport), and the view and projection matrices appropriately for our FBO. As our FBO is 512 x 512, we use glViewport(0,0,512,512). Similar changes should be made to the view and projection matrices to match the aspect ratio of the viewport and set up the scene to be rendered to the FBO.

Once we've rendered to the texture, we unbind from the FBO, reset the viewport, and the view and projection matrices, use the FBO's texture (texture unit 0), and draw the scene!

There's more...

As FBOs have multiple color attachment points, we can have several output targets from our fragment shaders. Note that so far, all of our fragment shaders have only had a single output variable assigned to location zero. Hence, we set up our FBO so that its texture corresponds to color attachment zero. In later chapters, we'll look at examples where we use more than one of these attachments for things like deferred shading.

See also

  • The Applying a 2D texture recipe
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.137.185.180