Fixed Function to Modern OpenGL (Part 2 of 4)

John Stone

John Stone


John Stone is a seasoned software developer with an extensive computer science education and entrepreneurial background and brings a wealth of practical real-world experience working with graphical visualization and virtual reality (VR). As a former technical director of research for a VR laboratory at the University of Texas at Austin, he spent years honing his craft developing VR-based data acquisition systems in OpenGL for scientific experiments and projects.

By John Stone | Thursday, February 18, 2016

The code referenced in this blog is available here.

This is the second of a four part blog series, in which we compare and contrast Fixed-Function OpenGL with Modern OpenGL and introduce you to the issues involved in porting your code to Modern OpenGL.

Last time we 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 Part 2, we 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. 

Math

Fixed-function OpenGL had the concept of Matrix Stacks built in the API.  There was a GL_MODELVIEW matrix stack and a GL_PROJECTION matrix stack.  These matrices were automatically applied to the geometry you submitted between glBegin/glEnd and were usually used to emulate the concept of a “camera” (GL_MODELVIEW), rendering the scene through a window (GL_PROJECTION) that is then painted on your computer’s screen.  It had the convenience functions glLoadIdenty, glTranslate, glRotate, and glScale that you would apply to the top of the currently selected matrix stack.  You would change matrix stacks with glMatrixMode and glPushMatrix and glPopMatrix between glBegin/glEnd pairs to place and then draw your geometry in the scene.  All of this is now gone.

In modern OpenGL, you have to manually apply this math to your vertices in the vertex shader.  The typical thing to do is recreate the idioms of model transform matrix, view transform matrix and projection transform matrix and then pass these to your vertex shader as “uniforms” (another name for constant data).  You then perform the matrix calculations in your vertex shader in the proper order – model, view, perspective.  Another approach is to combine all these matrices together into a Model-View-Projection (MVP) matrix on the CPU and then pass only that matrix to your vertex shader.  Please take note, these are only conventions that you can optionally follow.  Modern OpenGL is agnostic about these concepts and while it does directly support matrix math in the shader language, GLSL, you are not required to use it.  You are completely free to manipulate (or not) the vertices in the vertex shader any way you desire.  If you can code it, you can use it. 

What this all means is that if you, the programmer, desire to use the Model-View-Perspective concept in your programs, you have to find a way to perform matrix math yourself.  Fortunately, Qt has a very powerful yet concise Matrix Math library built in.  It supports 2-D, 3-D and 4-D vectors, 3x3 and 4x4 matrices, as well as quaternions.  It has all the matrix convenience functions that used to be in the GLU library including replacements for gluPerspective, gluOrtho2D, glFrustum, gluLookat, glTranslate, glRotate, glScale and glLoadIdentity, among many others.  Check out the QVector2D, QVector3D, QVector4D, QQuaternion and QMatrix4x4 objects in Qt.

So after all this we are finally ready to move out of clip-space and start rendering in world coordinates like you are used to. 

Listing 3.1 OpenGL triangle rendered in fixed-function world space.

#include <gl/glut.h>
#include <stdlib.h>

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(-2.0, -2.0, 0.0);
    glColor3f(0.0, 1.0, 0.0); glVertex3f( 0.0,  2.0, 0.0);
    glColor3f(0.0, 0.0, 1.0); glVertex3f( 2.0, -2.0, 0.0);
   glEnd();
   glutSwapBuffers();
}

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);

   glMatrixMode (GL_MODELVIEW);
   glLoadIdentity ();
   if (w <= h)
      glScalef(1, float(h)/w, 1);
   else
      glScalef(float(w)/h, 1, 1);
}

void keyboard (unsigned char key, int , int )
{
   switch (key) {
      case 27:
         exit(0);
         break;
      default:
         break;
   }
}

int main(int argc, char** argv)
{
   glutInit(&argc, argv);
   glutInitDisplayMode (GLUT_DOUBLE | GLUT_RGB);
   glutInitWindowSize (640, 480);
   glutInitWindowPosition (50, 50);
   glutCreateWindow (argv);
   init ();
   glutDisplayFunc(display);
   glutReshapeFunc(reshape);
   glutKeyboardFunc(keyboard);
   glutMainLoop();
   return 0;
}

Listing 3.2 is the equivalent version in modern OpenGL. 

#include <QtGui/QGuiApplication>
#include <QtGui/QKeyEvent>

#include <QtGui/QMatrix4x4>
#include <QtGui/QOpenglWindow>
#include <QtGui/QOpenglBuffer>
#include <QtGui/QOpenglFunctions>
#include <QtGui/QOpenglShaderProgram>
#include <QtGui/QOpenglVertexarrayObject>

static QString vertexShader =
        "#version 100\n"
        "\n"
        "attribute vec3 position;\n"
        "attribute vec3 color;\n"
        "\n"
        "uniform mat4 mvp;\n"
        "\n"
        "varying vec3 v_color;\n"
        "\n"
        "void main()\n"
        "{\n"
        "    v_color = color;\n"
        "    gl_Position = mvp * 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,
                    color;
        } attribs = {
            { {   -2.0, -2.0, 0.0 }, { 1.0, 0.0, 0.0 } },  // left-bottom,  red
            { {    0.0,  2.0, 0.0 }, { 0.0, 1.0, 0.0 } },  // center-top,   blue
            { {    2.0, -2.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();
        m_view.setToIdentity();
    }

    void resizeGL(int w, int h)
    {
        glViewport(0, 0, w, h);
        m_model.setToIdentity();
        m_projection.setToIdentity();
        if (w <= h) {
            m_model.scale(1, float(h)/w, 1);
            m_projection.ortho(-2.f, 2.f, -2.f*h/w, 2.f*h/w, -2.f, 2.f);
        } else {
            m_model.scale(float(w)/h, 1, 1);
            m_projection.ortho(-2.f*w/h, 2.f*w/h, -2.f, 2.f, -2.f, 2.f);
        }
        update();
    }

    void paintGL()
    {
        glClear(GL_COLOR_BUFFER_BIT);
        m_pgm.bind();
        m_pgm.setUniformValue("mvp", m_projection * m_view * m_model);
        m_vao.bind();
        glDrawArrays(GL_TRIANGLES, 0, 3);
    }

    void keyPressEvent(QKeyEvent * ev)
    {
        switch (ev->key()) {
           case Qt::Key_Escape:
              exit(0);
              break;
           default:
              QWindow::keyPressEvent(ev);
              break;
        }

    }

    QMatrix4x4 m_model, m_view, m_projection;
    QOpenGLShaderProgram     m_pgm;
    QOpenGLVertexArrayObject m_vao;
    QOpenGLBuffer            m_vbo;
};

int main(int argc, char *argv)
{
    QGuiApplication a(argc,argv);
    Window w;
    w.setWidth(640); w.setHeight(480);
    w.show();
    return a.exec();
}

I should stop here and take a moment to talk about the “Uniforms”.  A uniform in GLSL shading language is a constant parameter set in the CPU code before a draw call.  It is constant in the fashion that invocations of all the shader programs resulting from a particular draw call will have the same value for that uniform.  You use QOpenGLProgram’s setUniformValue convenience function to set the values of your uniforms between glDraw calls.

One thing to note is that glClipPlane has been removed.  If you want custom clip planes, you have to perform the point/plane equation tests yourself in your pixel shader and then use the keyword discard to inform OpenGL that that particular pixel should not be displayed.  Another thing is that the glPushAttrib, glPopAttrib, glPushClientAttrib and glPopClientAttrib have been removed.  You have to manually manage your OpenGL state.

Wrapping Up

Here we re-implemented the famous OpenGL triangle in world space and compared and contrasted the fixed-function GLUT program with an equivalent Modern OpenGL program.  We covered how to use Qt's math functions to replace the matrix mode that was removed from Fixed-Function OpenGL and replaced with code you write in your vertex shader.   We talked about the Model, View, Projection idioms and how you can use uniforms to pass these variables (and others) to your shaders programs between draw calls.   Next up, we're going to talk about OpenGL lighting , it's origins and theory, and how to implement that theory in your shader programs.  Stay tuned.

 

Part 1 | Part 2 | Part 3 | Part 4

 



Have a question or add to the conversation: Log in Register