Fixed Function to Modern OpenGL (Part 1 of 4)
The code referenced in this blog is available here.
In this first of a four part blog series, we outline differences between Fixed-Function OpenGL with Modern OpenGL and show effective solutions for common issues encountered when porting your code to Modern OpenGL.
I was once faced with the task of “modernizing” a large fixed-function rendering engine. At the time, I was very well versed in the fixed-function programming model but only had vague knowledge of the “new” shader-based OpenGL. With the release of OpenGL 3.3, all of the fixed-function APIs that I knew and loved were removed, and so I was faced with the task of not only learning the modern API but also porting this large fixed-function code base to work with it. There were many lessons learned along the way and many late nights staring at code that “should work”, but did not.
In this series of blog posts, I am going to try to help you avoid the “gotchas” that kept me from my beauty sleep by discussing the basic things you need to do to get rendering going in OpenGL 3.3 (or OpenGL ES 2.0) from the perspective of porting a fixed-function application.
Rendering
So what is the minimum you need to “light up” a pixel? With fixed-function as well as modern programming, there is still the problem of creating a window, responding to size changes and other platform interaction. Many of the books you may have used in learning OpenGL relied on the GLUT library for this; here we are going to use Qt. Qt provides all the functionality of GLUT plus a whole lot more, which we will need later on.
In Qt, the class to use to setup rendering is QOpenGLWindow. It provides virtual functions that you override for the same things provided by GLUT callbacks. To initialize, initializeGL() is used for one time initialization, resizeGL(int w, int h) is called whenever the window resizes, paintGL() is called with an active opengl context bound to the window, and keyPressEvent(QKeyEvent *) is used to receive key events.
When you are at the point where you have a window on the screen and an OpenGL context bound to it, you are ready to render. In fixed-function OpenGL you can use something as simple as:
Listing 1 Shows the famous OpenGL triangle
void init(void) { glMatrixMode(GL_MODELVIEW); glLoadIdentity(); } void display(void) { glClear(GL_COLOR_BUFFER_BIT); glBegin(GL_TRIANGLES); glColor3f(1.0, 0.0, 0.0); glVertex3f(-1.0, -1.0, 0.0); glColor3f(0.0, 1.0, 0.0); glVertex3f( 0.0, 1.0, 0.0); glColor3f(0.0, 0.0, 1.0); glVertex3f( 1.0, -1.0, 0.0); glEnd(); glutSwapBuffers(); } void reshape(int w, int h) { glViewport(0, 0, (GLsizei) w, (GLsizei) h); glMatrixMode(GL_PROJECTION); glLoadIdentity(); }
Here we use the glBegin/glEnd pair along with glVertex and glColor to specify a triangle in clip-space that results in the famous “color triangle” that we all know and love. In modern OpenGL these functions are missing, so what can we do? Well, even in OpenGL 1.1, there was the concept of vertex arrays. Modern OpenGL keeps this concept, if not the actual API, and it introduces the notion of “buffer objects” (memory buffers that live on the graphics card) in which you store your attribute data.
Listing 2.1 Shows the same fixed-function program using OpenGL 1.1 Vertex Arrays
void init(void) { glMatrixMode(GL_MODELVIEW); glLoadIdentity(); } void display(void) { glClear(GL_COLOR_BUFFER_BIT); static struct Vertex { GLfloat position[3], color[3]; } attribs[3] = { { { -1.0, -1.0, 0.0 }, { 1.0, 0.0, 0.0 } }, // left-bottom, red { { 0.0, 1.0, 0.0 }, { 0.0, 1.0, 0.0 } }, // center-top, blue { { 1.0, -1.0, 0.0 }, { 0.0, 0.0, 1.0 } }, // right-bottom, green }; glEnableClientState(GL_VERTEX_ARRAY); glVertexPointer(3, GL_FLOAT, sizeof(Vertex), (char*)attribs + offsetof(Vertex, position)); glEnableClientState(GL_COLOR_ARRAY); glColorPointer(3, GL_FLOAT, sizeof(Vertex), (char*)attribs + offsetof(Vertex, color)); glDrawArrays (GL_TRIANGLES, 0, 3); glutSwapBuffers(); } void reshape(int w, int h) { glViewport(0, 0, (GLsizei) w, (GLsizei) h); glMatrixMode(GL_PROJECTION); glLoadIdentity(); }
Listing 2.2 shows the modern equivalent, albeit using Qt’s convenience functions for “buffer object” management.
struct Window : QOpenGLWindow, QOpenGLFunctions { Window() : m_vbo(QOpenGLBuffer::VertexBuffer) { } void createGeometry() { // Initialize and bind the VAO that's going to capture all this vertex state m_vao.create(); m_vao.bind(); // interleaved data -- https://www.opengl.org/wiki/Vertex_Specification#Interleaved_arrays struct Vertex { GLfloat position[3], color[3]; } attribs[3] = { { { -1.0, -1.0, 0.0 }, { 1.0, 0.0, 0.0 } }, // left-bottom, red { { 0.0, 1.0, 0.0 }, { 0.0, 1.0, 0.0 } }, // center-top, blue { { 1.0, -1.0, 0.0 }, { 0.0, 0.0, 1.0 } }, // right-bottom, green }; // 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)); // Okay, we've finished setting up the vao m_vao.release(); } void initializeGL() { QOpenGLFunctions::initializeOpenGLFunctions(); createGeometry(); } void resizeGL(int w, int h) { glViewport(0, 0, w, h); update(); } void paintGL() { glClear(GL_COLOR_BUFFER_BIT); m_vao.bind(); glDrawArrays(GL_TRIANGLES, 0, 3); } QOpenGLVertexArrayObject m_vao; QOpenGLBuffer m_vbo; };
However, Listing 2.2 still will not render a triangle. Why not? Well the fixed-function example uses “fixed functionality” to render. With modern OpenGL, you have to program that functionality yourself in the form of “shaders”.
Shaders
There can be many different kinds of shaders in modern OpenGL, but only two are required to actually render - vertex shader and pixel shader. The vertex shader is a program that runs on the GPU once for each vertex in the primitive shapes you draw. It takes as input the attribute data in your vertex arrays and outputs data to the pixel shader through special “varying” variables. It is responsible for translating the vertex to clip coordinates and outputting it to the clipping stage hardware by setting the special reserved variable gl_Position.
A pixel shader is run once for each fragment (pixel-candidate) that is displayed on your screen, takes as input parameters the output from the vertex shader and outputs the fragment’s color using the special reserved variable gl_FragColor. As you know, pixels are produced by rasterizing the geometry described by the vertex attributes and primitive type parameter of the glBegin() or glDraw() call. A pixel shader is run for each pixel generated during rasterization.
These shaders are compiled and linked together into a program very similar to what is done with a C/C++ program. Note how Qt makes this very simple and straightforward with the QOpenGLShader and QOpenGLShaderProgram objects. Once you’ve compiled and linked your program you have to make it “active” by calling the bind() method and then you can start rendering with glDraw calls. Listing 2.3 defines the two simplest possible vertex and fragment shaders (literally 3 lines of executable code total) and puts this all together to complete the modern version of the famous OpenGL triangle.
Listing 2.3 Famous OpenGL triangle with modern API
static QString vertexShader = "#version 100\n" "\n" "attribute vec3 position;\n" "attribute vec3 color;\n" "\n" "varying vec3 v_color;\n" "\n" "void main()\n" "{\n" " v_color = color;\n" " gl_Position = vec4(position, 1);\n" "}\n" ; static QString fragmentShader = "#version 100\n" "\n" "varying vec3 v_color;\n" "\n" "void main()\n" "{\n" " gl_FragColor = vec4(v_color, 1);\n" "}\n" ; struct Window : QOpenGLWindow, QOpenGLFunctions { Window() : m_vbo(QOpenGLBuffer::VertexBuffer) { } 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(); // interleaved data -- https://www.opengl.org/wiki/Vertex_Specification#Interleaved_arrays struct Vertex { GLfloat position[3], color[3]; } attribs[3] = { { { -1.0, -1.0, 0.0 }, { 1.0, 0.0, 0.0 } }, // left-bottom, red { { 0.0, 1.0, 0.0 }, { 0.0, 1.0, 0.0 } }, // center-top, blue { { 1.0, -1.0, 0.0 }, { 0.0, 0.0, 1.0 } }, // right-bottom, green }; // 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)); // Configure the vertex streams for this attribute data layout m_pgm.enableAttributeArray("position"); m_pgm.setAttributeBuffer("position", GL_FLOAT, offsetof(Vertex, position), 3, sizeof(Vertex) ); m_pgm.enableAttributeArray("color"); m_pgm.setAttributeBuffer("color", GL_FLOAT, offsetof(Vertex, color), 3, sizeof(Vertex) ); // Okay, we've finished setting up the vao m_vao.release(); } void initializeGL() { QOpenGLFunctions::initializeOpenGLFunctions(); createShaderProgram(); m_pgm.bind(); createGeometry(); } void resizeGL(int w, int h) { glViewport(0, 0, w, h); update(); } void paintGL() { glClear(GL_COLOR_BUFFER_BIT); m_pgm.bind(); m_vao.bind(); glDrawArrays(GL_TRIANGLES, 0, 3); } QOpenGLShaderProgram m_pgm; QOpenGLVertexArrayObject m_vao; QOpenGLBuffer m_vbo; };
In examining the shader source code in Listing 2.3, you may notice the pixel shader just sets the output fragment’s color to the input color that was output by vertex shader, and then wonder why we have all those different colors inside the triangle. The answer is that the outputs of the vertex shader and corresponding pixel shader inputs are, by default, interpolated between the vertices, thus blending the colors of the triangle corners. This interpolation is the equivalent to calling glShadeModel(GL_SMOOTH). As you know, there is another value you can pass to this function - GL_FLAT. This non-interpolation behavior can be accomplished by setting an attribute on the varying parameters in the vertex and pixel shader. The default interpolation is smooth, so in the shader code varying vColor; is equivalent to smooth varying vColor; and flat interpolation is accomplished by flat varying vColor;.
QGLBufferObject
Now that we have our first triangle rendering, let us go back and take a closer look at the QGLBufferObject. I said earlier that it was a memory buffer on the graphics card that holds vertex attribute data. Since it lives on the video card, it is equivalent to using the old glBegin/glEnd style vertex attribute API in a display list. Recall that when you define a display list nothing is actually rendered, what is really happening, among other things, is that the attributes you are specifying are being copied to video card memory. This is also what happens when you call alloc() on the QGLBufferObject (data is copied to the video card). A big difference between display lists and vbos (vertex buffer objects) is that glBegin also defines a primitive type for drawing, while vbos just specify attribute data. That primitive type is now a parameter to glDraw calls, and as such, you will explicitly need to make the draw calls for each type of primitive you want to draw each time you want to draw them. (Note that if you used OpenGL 1.1 Vertex Arrays you also had to make these explicit draw calls as shown in Listing 2.1).
QGLVertexArrayObject
Another thing you may have noticed is that in Listing 2.1 we had to setup the various attribute array specifications each time before we called glDraw but in Listing 2.3 this all was replaced by a single call to vao.bind(). A VAO (Vertex Array Object) is an “object” that “remembers” the vertex array state and automatically applies said state when bind() is called. This is why we can describe the array state we want once when we load the vertex data on to the video card using the QGLBufferObject and then restore the state just before drawing with a bind() call. VAOs go hand-in-hand with “buffer objects” since without array state specifiers the buffer object is not very useful.
One thing to note while we are talking about drawing geometry is that the primitive types: GL_QUAD, GL_QUAD_STRIP, and GL_POLYGON have been removed. You will have to change your geometry to triangle strips, triangle fans or triangles.
Wrapping Up
We have covered the necessary steps you need to create the famous OpenGL triangle with the modern API. We've introduced shaders and described their inputs and outputs along with various Qt convenience objects that make working with the new modern API easier. In the next part we'll move from clip-space to world space and go over the modern replacements for the GL_MODELVIEW and GL_PROJECTION matrix stacks and then port this example to world space.
Part 1 | Part 2 | Part 3 | Part 4