Render to Texture: Fixed Function vs. Modern OpenGL
Welcome back! Last time we talked about texturing, lighting and geometry in regards to porting fixed function OpenGL to Modern OpenGL and while it covered the basic features in OpenGL programs, it left out a lot of advanced techniques that your fixed-function code may have used. In this article I’m going to talk about one of those advanced features, render-to-texture.
Render-to-Texture
Render to texture is used in a variety of graphical techniques including shadow mapping, multi-pass rendering and other advanced visual effects. It involves taking the results of one rendering pass and using those produced pixels as a texture image in another rendering pass. There a few ways you could have done this in your fixed-function applications:
- Render to back buffer, read the pixels back via glReadPixels, and then set the texture image using those pixels via glTexImage2d or glTexSubImage2d. This isn’t very performant since the pixel data is moving from video card, back to CPU ram, and then back to the video card again.
- Render to back buffer, copy the pixels to the texture image via glTexCopy2D. This is better since it keeps the pixel data on the video card, but it is still making a copy.
- Render to the texture image directly on the video card. This is the best since no copies are involved.
In this article we’re going to talk about scenario number three exclusively as this is approach most likely taken by your existing code base.
PBuffers
Pixel buffer (pbuffers for short) are fixed-function off-screen memory areas on the video card that replace the normal back-buffer as the destination for rendering. In normal rendering there is a font-buffer, a back-buffer, a depth buffer and (optionally) a stencil buffer allocated for you when you create the OpenGL graphics context associated with your window. This default set of buffers is called the window-system-provided framebuffer. The front-buffer is the video card memory that’s actively being scanned out on your monitor and the back buffer is the target for pixel data that’s begin generated when you render with OpenGL draw calls. What a pbuffer does is provide another set of buffers to use as a destination for the pixel data created when you render with OpenGL.
Pbuffer support falls into the category of an OpenGL extension. An OpenGL extension is an optional feature of OpenGL that some video card vendors can elect to support. Like creating an OpenGL context, it is also a platform-specific OpenGL extension. This means that there’s a different implementation with different API calls for each platform including Windows, Mac, Linux and a variety of embedded platforms like Android and iOS. Here, I’m going to discuss the Windows version of pbuffers and its API described by the two OpenGL extensions WGL_ARB_pbuffer and WGL_ARB_pixel_format.
With these two extensions you now can render an image to the pbuffer off-screen memory on your video card. The question becomes; how can you use it in your next rendering pass as a texture image? The answer is yet another OpenGL extension WGL_ARB_render_texture. What this extension does is let you describe the type of texture image (pixel format and texture-target) that you want your off-screen pbuffer to be used for. With these three extensions we have everything we need to create a copy-free fixed-function render-to-texture application.
The following procedure describes exactly how to use the APIs in these extensions to perform render-to-texture. It is taken directly from the WGL_ARB_render_texture specification and paraphrased:
- Create the rendering window. Call wglChoosePixelFormatARB and find a suitable pixel format for rendering the image. Set the pixel format for the rendering window to this pixel format.
- Create a context for the window. Make the context current to the window and initialize the contexts attributes. Bind a texture object to the GL_TEXTURE_2D target and set the texture parameters to the desired values.
- Create the pbuffer. Call wglChoosePixelFormatARB and find a suitable pixel format for rendering the texture. WGL_DRAW_TO_PBUFFER and (WGL_BIND_TO_TEXTURE_RGB_ARB or WGL_BIND_TO_TEXTURE_RGBA_ARB must be TRUE). Create the pbuffer with this pixel format. Set the pbuffer width and height to the width and height of the level zero image. Set WGL_TEXTURE_FORMAT_ARB to be WGL_TEXTURE_RGB_ARB or WGL_TEXTURE_RGBA_ARB. Also set WGL_TEXTURE_TARGET_ARB to WGL_TEXTURE_2D_ARB.
- Create a context for the pbuffer. Make the pbuffer context current and initialize the context's attributes.
- Render the image to the pbuffer. Call glFlush.
- Make the window context current and call wglBindTexImageARB to bind the pbuffer drawable to the texture created in step 2. Set <iBuffer> to WGL_FRONT_LEFT or WGL_BACK_LEFT depending upon which color buffer was used for rendering the pbuffer.
- Render to the window using the pbuffer attached to the 2D texture.
- Call wglReleaseTexImageARB to release the color buffer of the Pbuffer from the texture. Goto step 5 to generate more frames.
Wow, that looks like quite a few steps to follow, but really it isn’t that bad at all. As a demonstration, let’s take our last fixed-function example (textured, lit, rotating cube) and use a pbuffer to create an interesting “hall of mirrors” effect. What we’ll do is render the scene twice. The first time we’ll render this last example’s scene into the pbuffer. Then we’ll render that same scene again, but instead of using the old “circle” texture we’ll use the results from our first rendering pass.
Figure 1 shows the result Listing 6.1 shows the code that produced it.
#include <gl/glew.h> #include <gl/wglew.h> #include <gl/glut.h> #include <stdlib.h> #include <stdio.h> static HPBUFFERARB pBuffer; static HDC pBufferHDC, screenHDC; static HGLRC pBufferCtx, screenCtx; static GLuint pBufferTex, screenTex; static GLuint list; static void initPbuffer() { // Create the pbuffer with the help of glew and glut int ia[] = { WGL_DRAW_TO_PBUFFER_ARB, true, WGL_BIND_TO_TEXTURE_RGBA_ARB, true, WGL_DEPTH_BITS_ARB, glutGet(GLUT_WINDOW_DEPTH_SIZE), WGL_RED_BITS_ARB, glutGet(GLUT_WINDOW_RED_SIZE), WGL_GREEN_BITS_EXT, glutGet(GLUT_WINDOW_GREEN_SIZE), WGL_BLUE_BITS_EXT, glutGet(GLUT_WINDOW_BLUE_SIZE), 0, 0 }; float fa[] = { 0, 0 }; int fmts[64]; unsigned nfmts = 0; if (!wglChoosePixelFormatARB(wglGetCurrentDC(), ia, fa, _countof(fmts), fmts, &nfmts) || !nfmts) { printf("wglChoosePixelFormat FAILED -- nfmts %d, GetLastError 0x%08X\n", nfmts, GetLastError()); getchar(); exit(0); } int pb[] = { WGL_TEXTURE_FORMAT_ARB, WGL_TEXTURE_RGBA_ARB, WGL_TEXTURE_TARGET_ARB, WGL_TEXTURE_2D_ARB, WGL_PBUFFER_LARGEST_ARB, true, 0, 0 }; if (!(pBuffer = wglCreatePbufferARB(wglGetCurrentDC(), fmts[0], 640, 480, pb))) __debugbreak(); if (!(pBufferHDC = wglGetPbufferDCARB(pBuffer))) __debugbreak(); if (!(pBufferCtx = wglCreateContext(pBufferHDC))) __debugbreak(); // Get it's actual size int w; if (!wglQueryPbufferARB(pBuffer, WGL_PBUFFER_WIDTH_ARB, &w)) __debugbreak(); int h; if (!wglQueryPbufferARB(pBuffer, WGL_PBUFFER_HEIGHT_ARB, &h)) __debugbreak(); // Initialize it's projection matrix if (!wglMakeCurrent(pBufferHDC, pBufferCtx)) __debugbreak(); void reshape(int w, int h); reshape(w, h); if (!wglMakeCurrent(screenHDC, screenCtx)) __debugbreak(); if (!wglShareLists(screenCtx, pBufferCtx)) __debugbreak(); } static void initLights() { GLfloat light_ambient[] = { 0.0f, 0.0f, 0.0f, 1.0f }; /* default value */ GLfloat light_diffuse[] = { 1.0f, 1.0f, 1.0f, 1.0f }; /* default value */ GLfloat light_specular[] = { 1.0f, 1.0f, 1.0f, 1.0f }; /* default value */ GLfloat light_position[] = { 0.0f, 1.0f, 1.0f, 0.0f }; /* NOT default value */ GLfloat lightModel_ambient[] = { 0.2f, 0.2f, 0.2f, 1.0f }; /* default value */ GLfloat material_specular[] = { 1.0f, 1.0f, 1.0f, 1.0f }; /* NOT default value */ GLfloat material_emission[] = { 0.0f, 0.0f, 0.0f, 1.0f }; /* default value */ glLightfv(GL_LIGHT0, GL_AMBIENT, light_ambient); glLightfv(GL_LIGHT0, GL_DIFFUSE, light_diffuse); glLightfv(GL_LIGHT0, GL_SPECULAR, light_specular); glLightfv(GL_LIGHT0, GL_POSITION, light_position); glLightModelfv(GL_LIGHT_MODEL_AMBIENT, lightModel_ambient); glColorMaterial(GL_FRONT, GL_AMBIENT_AND_DIFFUSE); glMaterialfv(GL_FRONT, GL_SPECULAR, material_specular); glMaterialfv(GL_FRONT, GL_EMISSION, material_emission); glMaterialf(GL_FRONT, GL_SHININESS, 10.0); /* NOT default value */ glEnable(GL_LIGHTING); glEnable(GL_LIGHT0); glEnable(GL_NORMALIZE); glEnable(GL_COLOR_MATERIAL); } /* * Texture copied and modifided modified from: * https://www.opengl.org/archives/resources/code/samples/mjktips/TexShadowReflectLight.html */ static char *circles[] = { "................", "................", "......xxxx......", "....xxxxxxxx....", "...xxxxxxxxxx...", "...xxx....xxx...", "..xxx......xxx..", "..xxx......xxx..", "..xxx......xxx..", "..xxx......xxx..", "...xxx....xxx...", "...xxxxxxxxxx...", "....xxxxxxxx....", "......xxxx......", "................", "................", }; static void initTextures() { GLubyte floorTexture[16][16][3]; GLubyte *loc; int s, t; /* Setup RGB image for the texture. */ loc = (GLubyte*)floorTexture; for (t = 0; t < 16; t++) { for (s = 0; s < 16; s++) { if (circles[t][s] == 'x') { /* Nice green. */ loc[0] = 0x1f; loc[1] = 0x8f; loc[2] = 0x1f; } else { /* Light gray. */ loc[0] = 0xaa; loc[1] = 0xaa; loc[2] = 0xaa; } loc += 3; } } // create, configure and initialize the textures for (int i = 0; i < 2; ++i) { glGenTextures(1, i ? &screenTex : &pBufferTex); glBindTexture(GL_TEXTURE_2D, i ? screenTex : pBufferTex); glPixelStorei(GL_UNPACK_ALIGNMENT, 1); glTexImage2D(GL_TEXTURE_2D, 0, 3, 16, 16, 0, GL_RGB, GL_UNSIGNED_BYTE, floorTexture); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE); } } void init(void) { screenHDC = wglGetCurrentDC(); screenCtx = wglGetCurrentContext(); initPbuffer(); initTextures(); } void display(void) { // First time? if (!list) { // Yes, Create the display list list = glGenLists(1); glNewList(list, GL_COMPILE); // Clear back-buffer glClearColor(.5f, .5f, .5f, 1.f); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // Be sure light doesn't move glMatrixMode(GL_MODELVIEW); glPushMatrix(); glLoadIdentity(); initLights(); glPopMatrix(); // Enable textures and depth testing glEnable(GL_TEXTURE_2D); glEnable(GL_DEPTH_TEST); // Draw the cube with 6 quads glBegin(GL_QUADS); // Top face (y = 1.0f) glColor3f(0.0f, 1.0f, 0.0f); // Green glNormal3f(0.0f, 1.0f, 0.0f); glTexCoord2f(0.0, 0.0); glVertex3f( 1.0f, 1.0f, -1.0f); glTexCoord2f(0.0, 1.0); glVertex3f(-1.0f, 1.0f, -1.0f); glTexCoord2f(1.0, 1.0); glVertex3f(-1.0f, 1.0f, 1.0f); glTexCoord2f(1.0, 0.0); glVertex3f( 1.0f, 1.0f, 1.0f); // Bottom face (y = -1.0f) glColor3f(1.0f, 0.5f, 0.0f); // Orange glNormal3f(0.0f, -1.0f, 0.0f); glTexCoord2f(0.0, 0.0); glVertex3f( 1.0f, -1.0f, 1.0f); glTexCoord2f(0.0, 1.0); glVertex3f(-1.0f, -1.0f, 1.0f); glTexCoord2f(1.0, 1.0); glVertex3f(-1.0f, -1.0f, -1.0f); glTexCoord2f(1.0, 0.0); glVertex3f( 1.0f, -1.0f, -1.0f); // Front face (z = 1.0f) glColor3f(1.0f, 0.0f, 0.0f); // Red glNormal3f(0.0f, 0.0f, 1.0f); glTexCoord2f(0.0, 0.0); glVertex3f( 1.0f, 1.0f, 1.0f); glTexCoord2f(0.0, 1.0); glVertex3f(-1.0f, 1.0f, 1.0f); glTexCoord2f(1.0, 1.0); glVertex3f(-1.0f, -1.0f, 1.0f); glTexCoord2f(1.0, 0.0); glVertex3f( 1.0f, -1.0f, 1.0f); // Back face (z = -1.0f) glColor3f(1.0f, 1.0f, 0.0f); // Yellow glNormal3f(0.0f, 0.0f, -1.0f); glTexCoord2f(0.0, 0.0); glVertex3f( 1.0f, -1.0f, -1.0f); glTexCoord2f(0.0, 1.0); glVertex3f(-1.0f, -1.0f, -1.0f); glTexCoord2f(1.0, 1.0); glVertex3f(-1.0f, 1.0f, -1.0f); glTexCoord2f(1.0, 0.0); glVertex3f( 1.0f, 1.0f, -1.0f); // Left face (x = -1.0f) glColor3f(0.0f, 0.0f, 1.0f); // Blue glNormal3f(-1.0f, 0.0f, 0.0f); glTexCoord2f(0.0, 0.0); glVertex3f(-1.0f, 1.0f, 1.0f); glTexCoord2f(0.0, 1.0); glVertex3f(-1.0f, 1.0f, -1.0f); glTexCoord2f(1.0, 1.0); glVertex3f(-1.0f, -1.0f, -1.0f); glTexCoord2f(1.0, 0.0); glVertex3f(-1.0f, -1.0f, 1.0f); // Right face (x = 1.0f) glColor3f(1.0f, 0.0f, 1.0f); // Magenta glNormal3f(1.0f, 0.0f, 0.0f); glTexCoord2f(0.0, 0.0); glVertex3f( 1.0f, 1.0f, -1.0f); glTexCoord2f(0.0, 1.0); glVertex3f( 1.0f, 1.0f, 1.0f); glTexCoord2f(1.0, 1.0); glVertex3f( 1.0f, -1.0f, 1.0f); glTexCoord2f(1.0, 0.0); glVertex3f( 1.0f, -1.0f, -1.0f); glEnd(); glEndList(); } // The frame counter static unsigned cnt; for (int i = 0; i < 2; ++i) { // first-pass, draw into pbuffer; second-pass, draw into backbuffer if (i) { if (!wglMakeCurrent(screenHDC, screenCtx)) __debugbreak(); glBindTexture(GL_TEXTURE_2D, screenTex); } else { if (!wglMakeCurrent(pBufferHDC, pBufferCtx)) __debugbreak(); glBindTexture(GL_TEXTURE_2D, pBufferTex); } // Set absolute rotation glMatrixMode(GL_MODELVIEW); glLoadIdentity(); glRotatef(float(cnt % 360), 1, 0, 0); glRotatef(45.f, 0, 0, 1); // Draw; on the second-pass use pbuffer as texture image if (i && !wglBindTexImageARB(pBuffer, WGL_FRONT_LEFT_ARB)) __debugbreak(); glCallList(list); if (i && !wglReleaseTexImageARB(pBuffer, WGL_FRONT_LEFT_ARB)) __debugbreak(); } // Count ++cnt; // Swap glutSwapBuffers(); } void timeout(int) { glutPostRedisplay(); glutTimerFunc(1000 / 60, timeout, 1); } void reshape(int w, int h) { glViewport(0, 0, (GLsizei)w, (GLsizei)h); glMatrixMode(GL_PROJECTION); glLoadIdentity(); if (w <= h) glOrtho(-2.f, 2.f, -2.f*h / w, 2.f*h / w, -10.f, 10.f); else glOrtho(-2.f*w / h, 2.f*w / h, -2.f, 2.f, -10.f, 10.f); } void keyboard(unsigned char key, int, int) { if (key == 27) exit(0); } int main(int argc, char** argv) { glutInit(&argc, argv); glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB); glutInitWindowSize(640, 480); glutInitWindowPosition(50, 50); glutCreateWindow(argv[0]); printf("OpenGL vendor string: %s\n", glGetString(GL_VENDOR)); printf("OpenGL renderer string: %s\n", glGetString(GL_RENDERER)); printf("OpenGL version string: %s\n", glGetString(GL_VERSION)); if (glewInit()) { printf("glewInit() failed\n"); getchar(); exit(0); } if (wglewInit()) { printf("wglewInit() failed\n"); getchar(); exit(0); } init(); glutDisplayFunc(display); glutReshapeFunc(reshape); glutKeyboardFunc(keyboard); glutTimerFunc(1000 / 60, timeout, 1); glutMainLoop(); return 0; }
FrameBufferObjects
Now that we’ve got copy-less render-to-texture going in our fixed-function example let’s move on to doing this with Modern OpenGL, but first, a thing to note with the previous example. When you use pbuffers you also must use separate rendering contexts (one for each pbuffer) and that brings along a host of issues since you now have to manage two (or more) completely separate OpenGL states.
Modern OpenGL solves this problem in an entirely different way with something called a FrameBufferObject or FBO for short. A FBO is a complete description of a set of compatible (with each other) off-screen rendering surfaces, including up 8 (or more) color-surfaces, a depth buffer surface, and a stencil surface. These various buffers are called framebuffer-attachable images and you’ll have to create and manage them yourself. You pretty much get everything you had with pbuffers but with the big difference of (besides having multiple color surfaces) having it all done in the single OpenGL context associated with your window. This means there is no context-switch overhead and no need to manage separate OpenGL states.
Let’s take a close look at this FBO thing and see what it’s about. We already know that a FBO contains a set of framebuffer-attachable images, but exactly what are they? There are off-screen memory buffers on the video card that can be used as the target and source of pixels produced and consumed when rendering in OpenGL. There are two types of framebuffer-attachable images; texture images and renderbuffer images. You use texture images when you want to use the resulting pixels as a texture in a later rendering pass and renderbuffer otherwise.
Each of these framebuffer-attachable images is assigned a specific attachment position in the containing framebuffer object. There are attachment points for multiple color buffers (GL_COLOR_ATTACHMENT0 … GL_COLOR_ATTACHMENTn, where n can be queried using GL_MAX_COLOR_ATTACHMENTS) and either a depth buffer (GL_DEPTH_ATTACHMENT) and a stencil buffer (GL_STENCIL_ATTACHMENT), or a combined depth-stencil buffer (GL_DEPTH_STENCIL_ATTACHMENT).
Framebuffer objects are optimized for attaching/detaching the image buffers associated with these attachment points. Doing this should be preferred over creating multiple FBOs, assigning a static set of image buffers and then switching between those FBOs.
You use glGenFramebuffers() and glDeleteFramebuffers() to create and destroy FBO objects. You use glBindframeBuffer() to make that frame buffer the target for active rendering. You use glGenRenderbuffers() to create render-buffer object, glBindRenderbuffer() to make it active, and glRenderbufferStorage() to actually create the off-screen memory buffer. You use glFramebufferRenderbuffer() and glFrameBufferTexture2D() to attach previously created render-buffer or texture-images to a framebuffer object.
Once you’ve attached all the texture-images and render-buffers you want to use to the FBO you must call glCheckFramebufferStatus(). glCheckFrameBufferStatus() is used by the OpenGL device driver to validate the compatibility of the various buffers with each other and with the video card’s hardware. If, and only if, the driver returns GL_FRAMEBUFFER_COMPLETE are you free to actually use the FBO.
When the driver doesn’t return GL_FRAMEBUFFER_COMPLETE is can be kind of tricky to figure out what the problem is. The OpenGL spec defines a set of “completeness rules” that you must satisfy to create a valid FBO. Here they are copied directly from https://www.opengl.org/wiki/Framebuffer_Object ¹ and edited for clarity.
Attachment Completeness
Each attachment point itself must be complete according to these rules. Empty attachments (attachments with no image attached) are complete by default. If an image is attached, it must adhere to the following rules:
- The source object for the image still exists and has the same type it was attached with.
- The image has a non-zero width and height (the height of a 1D image is assumed to be 1). The width/height must also be less than GL_MAX_FRAMEBUFFER_WIDTH andGL_MAX_FRAMEBUFFER_HEIGHT respectively.
- The layer for 3D or array textures attachments is less than the depth of the texture. It must also be less than GL_MAX_FRAMEBUFFER_.
- The number of samples must be less than GL_MAX_FRAMEBUFFER_.
- The image's format must match the attachment point's requirements, as defined above. Color-renderable formats for color attachments, etc.
Completeness Rules
These are the rules for framebuffer completeness. The order of these rules matters.
- If the target of glCheckFramebufferStatus references the Default Framebuffer (ie: FBO object number 0 is bound), and the default framebuffer does not exist, then you will get GL_FRAMEBUFFER_UNDEFINED. If the default framebuffer exists, then you always get GL_FRAMEBUFFER_COMPLETE. The rest of the rules apply when an FBO is bound.
- All attachments must be attachment complete. (GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT when false).
- There must be at least one image attached to the FBO. The GL_FRAMEBUFFER_DEFAULT_WIDTH and GL_FRAMEBUFFER_DEFAULT_HEIGHT parameters of the framebuffer must both be non-zero. (GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT when false).
- Each draw buffers must either specify color attachment points that have images attached or must be GL_NONE. (GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER when false). Note that this test is not performed if OpenGL 4.1 or ARB_ES2_compatibility is available.
- If the read buffer is set, then it must specify an attachment point that has an image attached. (GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER when false). Note that this test is not performed if OpenGL 4.1 or ARB_ES2_compatibility is available.
- All images must have the same number of multisample samples. (GL_FRAMEBUFFER_INCOMPLETE_MULTISAMPLE when false).
- If a layered image is attached to one attachment, then all attachments must be layered attachments. The attached layers do not have to have the same number of layers, nor do the layers have to come from the same kind of texture (a cubemap color texture can be paired with an array depth texture) (GL_FRAMEBUFFER_INCOMPLETE_LAYER_TARGETS when false).
Notice that there is no restriction based on size. The effective size of the FBO is the intersection of all of the sizes of the bound images (ie: the smallest in each dimension).
These rules are all code-based. If you ever get any of these values from glCheckFramebufferStatus, it is because your program has done something wrong in setting up the FBO. Each one has a specific remedy for it.
There's one more rule that can trip you up:
- The implementation likes your combination of attached image formats. (GL_FRAMEBUFFER_UNSUPPORTED when false).
OpenGL allows implementations to state that they do not support some combination of image formats for the attached images; they do this by returning GL_FRAMEBUFFER_UNSUPPORTED when you attempt to use an unsupported format combination.
However, the OpenGL specification also requires that implementations support certain format combinations; if you use these, implementations are forbidden to return GL_FRAMEBUFFER_UNSUPPORTED. Implementations must allow any combination of color formats, so long as all of those color formats come from the required set of color formats.
These color formats can be combined with a depth attachment with any of the required depth formats. Stencil attachments can also be used, again with the required stencil formats, as well as the combined depth/stencil formats. However, implementations are only required to support both a depth and stencil attachment simultaneously if both attachments refer to the same image.
This means that if you want stencil with no depth, you can use one of the required stencil formats. If you want depth with no stencil, you can use one of the required depth formats. But if you want depth and stencil, you must use a depth/stencil format and the same image in that texture must be attached to both the depth and stencil attachments.
Staying within these limits means you won't see GL_FRAMEBUFFER_UNSUPPORTED. Going outside of these limits makes it entirely possible to get this incompleteness.
Wow! That’s a lot of rules to follow and a lot of head scratching can occur when you forget just one tiny thing. This makes using FBOs, if not difficult, then at least tedious.
QOpenGLFramebufferObject
Fortunately, Qt comes to our rescue again and provides a platform-independent/driver-independent wrapper for FBOs that make them incredibly easy to use. The most common case, render-to-texture, can be accomplished in just 2 lines of code.
QOpenGLFramebufferObject myFBO(640, 480, QOpenGLFramebufferObject::CombinedDepthStencil, GL_TEXTURE_2D); myFBO.bind();
That’s it. The first line creates the framebuffer object, then creates a texture object with an associated texture-image sized 640 x 480, then creates a combine Depth/Stencil render buffer sized 640-480, attaches them the GL_COLOR_ATTACHMENT0 and GL_DEPTH_STENCIL_ATTACHMENT respectively, and validates FBO completeness with glCheckFrameBufferStatus(). The second line makes this FBO “active” and you’re ready to render-to-texture. Once you’ve finished rendering-to-texture unbind the FBO with myFBO.release() and get the texture object Qt created for you via myFBO.texture() and use it anyway you like in further rendering calls.
Let’s recreate the pbuffer example from the first part of this article but this time using Modern OpenGL with FBOs and Qt for our platform layer.
Listing 6.2 shows the complete program.
#include <qtcore/qscopedpointer> #include <qtgui/qguiapplication> #include <qtgui/qkeyevent> #include <qtgui/qopenglwindow> #include <qtgui/qopenglbuffer> #include <qtgui/qopenglfunctions> #include <qtgui/qopenglshaderprogram> #include <qtgui/qopenglvertexarrayobject> #include <qtgui/qopenglframebufferobject> static QString vertexShader = "#version 120\n" "\n" "attribute vec3 vertexPosition;\n" "attribute vec3 vertexNormal;\n" "attribute vec3 vertexColor;\n" "attribute vec2 texCoord2d;\n" "\n" "uniform mat4 modelViewMatrix;\n" "uniform mat3 normalMatrix;\n" "uniform mat4 projectionMatrix;\n" "\n" "struct LightSource\n" "{\n" " vec3 ambient;\n" " vec3 diffuse;\n" " vec3 specular;\n" " vec3 position;\n" "};\n" "uniform LightSource lightSource;\n" "\n" "struct LightModel\n" "{\n" " vec3 ambient;\n" "};\n" "uniform LightModel lightModel;\n" "\n" "struct Material {\n" " vec3 emission;\n" " vec3 specular;\n" " highp float shininess;\n" "};\n" "uniform Material material;\n" "\n" "varying vec3 v_color;\n" "varying vec2 v_texCoord2d;\n" "\n" "void main()\n" "{\n" " vec3 normal = normalize(normalMatrix * vertexNormal); // normal vector \n" " vec3 position = vec3(modelViewMatrix * vec4(vertexPosition, 1)); // vertex pos in eye coords \n" " vec3 halfVector = normalize(lightSource.position + vec3(0,0,1)); // light half vector \n" " float nDotVP = dot(normal, normalize(lightSource.position)); // normal . light direction \n" " float nDotHV = max(0., dot(normal, halfVector)); // normal . light half vector \n" " float pf = mix(0., pow(nDotHV, material.shininess), step(0., nDotVP)); // power factor \n" "\n" " vec3 ambient = lightSource.ambient;\n" " vec3 diffuse = lightSource.diffuse * nDotVP;\n" " vec3 specular = lightSource.specular * pf;\n" " vec3 sceneColor = material.emission + vertexColor * lightModel.ambient;\n" "\n" " v_color = clamp(sceneColor + \n" " ambient * vertexColor + \n" " diffuse * vertexColor + \n" " specular * material.specular, 0., 1. );\n" "\n" " v_texCoord2d = texCoord2d;\n" "\n" " gl_Position = projectionMatrix * modelViewMatrix * vec4(vertexPosition, 1);\n" "}\n" ; static QString fragmentShader = "#version 120\n" "\n" "uniform sampler2D texUnit;\n" "\n" "varying vec3 v_color;\n" "varying vec2 v_texCoord2d;\n" "\n" "void main()\n" "{\n" " gl_FragColor = vec4(v_color, 1) * texture2D(texUnit, v_texCoord2d);\n" "}\n" ; /* * Texture copied and modifided modified from: * https://www.opengl.org/archives/resources/code/samples/mjktips/TexShadowReflectLight.html */ static char *circles[] = { "................", "................", "......xxxx......", "....xxxxxxxx....", "...xxxxxxxxxx...", "...xxx....xxx...", "..xxx......xxx..", "..xxx......xxx..", "..xxx......xxx..", "..xxx......xxx..", "...xxx....xxx...", "...xxxxxxxxxx...", "....xxxxxxxx....", "......xxxx......", "................", "................", }; struct Window : QOpenGLWindow, QOpenGLFunctions { Window() : m_vbo(QOpenGLBuffer::VertexBuffer), m_ibo(QOpenGLBuffer::IndexBuffer) { } void createShaderProgram() { if ( !m_pgm.addShaderFromSourceCode( QOpenGLShader::Vertex, vertexShader)) { qDebug() << "Error in vertex shader:" << m_pgm.log(); exit(1); } if ( !m_pgm.addShaderFromSourceCode( QOpenGLShader::Fragment, fragmentShader)) { qDebug() << "Error in fragment shader:" << m_pgm.log(); exit(1); } if ( !m_pgm.link() ) { qDebug() << "Error linking shader program:" << m_pgm.log(); exit(1); } } void createGeometry() { // Initialize and bind the VAO that's going to capture all this vertex state m_vao.create(); m_vao.bind(); // we need 24 vertices, 24 normals, and 24 colors (6 faces, 4 vertices per face) // since we can't share normal data at the corners (each corner gets 3 normals) // and since we're not using glVertexAttribDivisor (not available in ES 2.0) struct Vertex { GLfloat position[3], normal [3], color [3], texcoord[2]; } attribs[24]= { // Top face (y = 1.0f) { { 1.0f, 1.0f, -1.0f}, {0.0f, 1.0f, 0.0f}, {0.0f, 1.0f, 0.0f}, { 0.0f, 0.0f} }, // Green { {-1.0f, 1.0f, -1.0f}, {0.0f, 1.0f, 0.0f}, {0.0f, 1.0f, 0.0f}, { 0.0f, 1.0f} }, // Green { {-1.0f, 1.0f, 1.0f}, {0.0f, 1.0f, 0.0f}, {0.0f, 1.0f, 0.0f}, { 1.0f, 1.0f} }, // Green { { 1.0f, 1.0f, 1.0f}, {0.0f, 1.0f, 0.0f}, {0.0f, 1.0f, 0.0f}, { 1.0f, 0.0f} }, // Green // Bottom face (y = -1.0f) { { 1.0f, -1.0f, 1.0f}, {0.0f, -1.0f, 0.0f}, {1.0f, 0.5f, 0.0f}, { 0.0f, 0.0f} }, // Orange { {-1.0f, -1.0f, 1.0f}, {0.0f, -1.0f, 0.0f}, {1.0f, 0.5f, 0.0f}, { 0.0f, 1.0f} }, // Orange { {-1.0f, -1.0f, -1.0f}, {0.0f, -1.0f, 0.0f}, {1.0f, 0.5f, 0.0f}, { 1.0f, 1.0f} }, // Orange { { 1.0f, -1.0f, -1.0f}, {0.0f, -1.0f, 0.0f}, {1.0f, 0.5f, 0.0f}, { 1.0f, 0.0f} }, // Orange // Front face (z = 1.0f) { { 1.0f, 1.0f, 1.0f}, {0.0f, 0.0f, 1.0f}, {1.0f, 0.0f, 0.0f}, { 0.0f, 0.0f} }, // Red { {-1.0f, 1.0f, 1.0f}, {0.0f, 0.0f, 1.0f}, {1.0f, 0.0f, 0.0f}, { 0.0f, 1.0f} }, // Red { {-1.0f, -1.0f, 1.0f}, {0.0f, 0.0f, 1.0f}, {1.0f, 0.0f, 0.0f}, { 1.0f, 1.0f} }, // Red { { 1.0f, -1.0f, 1.0f}, {0.0f, 0.0f, 1.0f}, {1.0f, 0.0f, 0.0f}, { 1.0f, 0.0f} }, // Red // Back face (z = -1.0f) { { 1.0f, -1.0f, -1.0f}, {0.0f, 0.0f, -1.0f}, {1.0f, 1.0f, 0.0f}, { 0.0f, 0.0f} }, // Yellow { {-1.0f, -1.0f, -1.0f}, {0.0f, 0.0f, -1.0f}, {1.0f, 1.0f, 0.0f}, { 0.0f, 1.0f} }, // Yellow { {-1.0f, 1.0f, -1.0f}, {0.0f, 0.0f, -1.0f}, {1.0f, 1.0f, 0.0f}, { 1.0f, 1.0f} }, // Yellow { { 1.0f, 1.0f, -1.0f}, {0.0f, 0.0f, -1.0f}, {1.0f, 1.0f, 0.0f}, { 1.0f, 0.0f} }, // Yellow // Left face (x = -1.0f) { {-1.0f, 1.0f, 1.0f}, {-1.0f, 0.0f, 0.0f}, {0.0f, 0.0f, 1.0f}, { 0.0f, 0.0f} }, // Blue { {-1.0f, 1.0f, -1.0f}, {-1.0f, 0.0f, 0.0f}, {0.0f, 0.0f, 1.0f}, { 0.0f, 1.0f} }, // Blue { {-1.0f, -1.0f, -1.0f}, {-1.0f, 0.0f, 0.0f}, {0.0f, 0.0f, 1.0f}, { 1.0f, 1.0f} }, // Blue { {-1.0f, -1.0f, 1.0f}, {-1.0f, 0.0f, 0.0f}, {0.0f, 0.0f, 1.0f}, { 1.0f, 0.0f} }, // Blue // Right face (x = 1.0f) { {1.0f, 1.0f, -1.0f}, {1.0f, 0.0f, 0.0f}, {1.0f, 0.0f, 1.0f}, { 0.0f, 0.0f} }, // Magenta { {1.0f, 1.0f, 1.0f}, {1.0f, 0.0f, 0.0f}, {1.0f, 0.0f, 1.0f}, { 0.0f, 1.0f} }, // Magenta { {1.0f, -1.0f, 1.0f}, {1.0f, 0.0f, 0.0f}, {1.0f, 0.0f, 1.0f}, { 1.0f, 1.0f} }, // Magenta { {1.0f, -1.0f, -1.0f}, {1.0f, 0.0f, 0.0f}, {1.0f, 0.0f, 1.0f}, { 1.0f, 0.0f} }, // Magenta }; // There's a bug in my nvidia driver that causes a glitch unless 4 more bytes are added to the VBO unsigned char nvidia_bug[4]; // Put all the attribute data in a FBO m_vbo.create(); m_vbo.setUsagePattern( QOpenGLBuffer::StaticDraw ); m_vbo.bind(); m_vbo.allocate(attribs, sizeof(attribs) + sizeof(nvidia_bug)); // Configure the vertex streams for this attribute data layout m_pgm.enableAttributeArray("vertexPosition"); m_pgm.setAttributeBuffer("vertexPosition", GL_FLOAT, offsetof(Vertex, position), 3, sizeof(Vertex) ); m_pgm.enableAttributeArray("vertexNormal"); m_pgm.setAttributeBuffer("vertexNormal", GL_FLOAT, offsetof(Vertex, normal), 3, sizeof(Vertex) ); m_pgm.enableAttributeArray("vertexColor"); m_pgm.setAttributeBuffer("vertexColor", GL_FLOAT, offsetof(Vertex, color), 3, sizeof(Vertex) ); m_pgm.enableAttributeArray("texCoord2d"); m_pgm.setAttributeBuffer("texCoord2d", GL_FLOAT, offsetof(Vertex, texcoord), 3, sizeof(Vertex) ); // we need 36 indices (6 faces, 2 triangles per face, 3 vertices per triangle) struct { GLubyte cube[36]; } indices; m_cnt=0; for (GLsizei i=0, v=0; v<6*4; v+=4) { // first triangle (ccw winding) indices.cube[i++] = v + 0; indices.cube[i++] = v + 1; indices.cube[i++] = v + 2; // second triangle (ccw winding) indices.cube[i++] = v + 0; indices.cube[i++] = v + 2; indices.cube[i++] = v + 3; m_cnt = i; } // Put all the index data in a IBO m_ibo.create(); m_ibo.setUsagePattern( QOpenGLBuffer::StaticDraw ); m_ibo.bind(); m_ibo.allocate(&indices, sizeof(indices)); // Okay, we've finished setting up the vao m_vao.release(); } void createTexture(void) { GLubyte image[16][16][3]; GLubyte *loc; int s, t; /* Setup RGB image for the texture. */ loc = (GLubyte*) image; for (t = 0; t < 16; t++) { for (s = 0; s < 16; s++) { if (circles[t][s] == 'x') { /* Nice green. */ loc[0] = 0x1f; loc[1] = 0x8f; loc[2] = 0x1f; } else { /* Light gray. */ loc[0] = 0xaa; loc[1] = 0xaa; loc[2] = 0xaa; } loc += 3; } } // Image texture glGenTextures (1, &m_tex); glBindTexture (GL_TEXTURE_2D, m_tex); glPixelStorei (GL_UNPACK_ALIGNMENT, 1); glTexImage2D (GL_TEXTURE_2D, 0, 3, 16, 16, 0, GL_RGB, GL_UNSIGNED_BYTE, image); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); } void createFBO() { m_fbo.reset(new QOpenGLFramebufferObject(640, 480, QOpenGLFramebufferObject::CombinedDepthStencil, GL_TEXTURE_2D )); } void initializeGL() { QOpenGLFunctions::initializeOpenGLFunctions(); createShaderProgram(); m_pgm.bind(); // Set lighting information m_pgm.setUniformValue("lightSource.ambient", QVector3D( 0.0f, 0.0f, 0.0f )); // opengl fixed-function default m_pgm.setUniformValue("lightSource.diffuse", QVector3D( 1.0f, 1.0f, 1.0f )); // opengl fixed-function default m_pgm.setUniformValue("lightSource.specular", QVector3D( 1.0f, 1.0f, 1.0f )); // opengl fixed-function default m_pgm.setUniformValue("lightSource.position", QVector3D( 0.0f, 1.0f, 1.0f )); // NOT DEFAULT VALUE m_pgm.setUniformValue("lightModel.ambient", QVector3D( 0.2f, 0.2f, 0.2f )); // opengl fixed-function default m_pgm.setUniformValue("material.emission", QVector3D( 0.0f, 0.0f, 0.0f )); // opengl fixed-function default m_pgm.setUniformValue("material.specular", QVector3D( 1.0f, 1.0f, 1.0f )); // NOT DEFAULT VALUE m_pgm.setUniformValue("material.shininess", 10.0f); // NOT DEFAULT VALUE createGeometry(); m_view.setToIdentity(); glEnable(GL_DEPTH_TEST); glEnable(GL_TEXTURE_2D); glActiveTexture(GL_TEXTURE0); m_pgm.setUniformValue("texUnit", 0); createTexture(); createFBO(); glClearColor(.5f,.5f,.5f,1.f); } void resizeGL(int w, int h) { glViewport(0, 0, w, h); m_projection.setToIdentity(); if (w <= h) m_projection.ortho(-2.f, 2.f, -2.f*h/w, 2.f*h/w, -2.f, 2.f); else m_projection.ortho(-2.f*w/h, 2.f*w/h, -2.f, 2.f, -2.f, 2.f); update(); } void paintGL() { static unsigned cnt; for (int i=0; i<2; ++i) { if(!i) m_fbo->bind(); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, !i ? m_tex : m_fbo->texture()); QMatrix4x4 model; model.rotate(cnt%360, 1,0,0); model.rotate(45, 0,0,1); QMatrix4x4 mv = m_view * model; m_pgm.bind(); m_pgm.setUniformValue("modelViewMatrix", mv); m_pgm.setUniformValue("normalMatrix", mv.normalMatrix()); m_pgm.setUniformValue("projectionMatrix", m_projection); m_vao.bind(); glDrawElements(GL_TRIANGLES, m_cnt, GL_UNSIGNED_BYTE, 0); if (!i) m_fbo->release(); } update(); ++cnt; } void keyPressEvent(QKeyEvent * ev) { if (ev->key() == Qt::Key_Escape) exit(0); } QScopedPointerm_fbo; QMatrix4x4 m_projection, m_view; QOpenGLShaderProgram m_pgm; QOpenGLVertexArrayObject m_vao; QOpenGLBuffer m_vbo; QOpenGLBuffer m_ibo; GLuint m_tex; GLsizei m_cnt; }; int main(int argc, char *argv[]) { QGuiApplication a(argc,argv); Window w; w.setWidth(640); w.setHeight(480); w.show(); return a.exec(); }
Wrapping Up
We covered quite a lot of material this time around but now we’re ready for a whole host of advanced rendering techniques. Next time we’ll use FBO extensively when I cover how to do “picking” with Modern OpenGL. Until then, it’s been a pleasure writing for you and I hope you come back soon.
Reference
- OpenGL website page, last accessed April 13, 2016, https://www.opengl.org/wiki/Framebuffer_Object
- OpenGL website page, last accessed April 13, 2016, https://www.opengl.org/registry/specs/ARB/wgl_render_texture.txt