Have a Question ?
Ask about our products, services, or latest research. Let's discuss how we can help you solve your problem.
Wednesday, December 05, 2018
Building graphics applications using modern OpenGL (Shaders)
By
Vinayak Sutar, Charuhas Naigaonkar
OpenGL Evolution
Over recent years, OpenGL has tremendously evolved and a significant change occurred in the year 2003. With its new programmable pipeline (OpenGL 2.0), it allows you to have direct access to the GPU and these programs are known as Shaders.
OpenGL Evolution
As this previous version was using the fixed pipeline but appearance of this constitutional way changed the face of OpenGL programming which makes it far more difficult to program but gave a robust tool.
Fixed Pipeline vs Programmable Pipeline
Maths
In fixed-pipeline OpenGL, all your matrix math is handled by OpenGL itself inside its APIs but in programmable pipeline OpenGL you have to manually apply this math to your vertex shader.
Operations
One of the important change in the programmable pipeline is that you can create most of the graphics effects such as lighting and texturing in a shader now, instead of the API. This makes the role of the API somewhat less important.
Shaders
Shaders in computer graphics is a type of program which is built in GPU and it compels its operations during the rendering pipeline. It performs a variety of functions to give special effects and does post-processing related to the computer graphics. Depending on the type of the version they will act at a different stage of the pipeline. There are various types of shader but to render, vertex shader and fragment shader are necessary. The pipeline shows stages they need to go ie from the vertices creation to the rendering of the framebuffer. The image below shows the whole process briefly.
OpenGL Shaders
Types/Stages of Shaders
Vertex Shaders
Vertex Shader (GL_VERTEX_SHADER) is most established shader and runs once for each vertex shader to the processor. The main motive is to transform each 3D position into 2D coordinate which appears on the screen. It can manipulate the properties such as position, texture, color and also it can be robust in details of movement, lighting and any scene involving 3D model.
Tessellation Control and Evaluation Shaders
In OpenGL 4.0 and more new shader class was introduced known as tessellation control shaders (GL_TESS_CONTROL_SHADER) and tessellation evaluation shader (GL_TESS_EVALUATION_SHADER) which combinedly allows the mesh to be further divided into the simpler finer mesh at runtime. This allows object near to camera to have fine details while further away to have coarse mesh. This builds the level of details of the object and image seems to be superior quality.
Geometry Shaders
Geometry Shaders (GL_GEOMETRY_SHADER) generates primitive graphics object such as points, line, triangles and are executed after the vertex shader taking the whole information as an input which is then rasterized and their fragments are given to pixel shader. Its process includes sprite generation, geometry tessellation, shadow volume extrusion, and single pass rendering to a cube map. It provides a better approximation of curve as it can generate extra lines of mesh for the complexity.
Fragment Shaders
Fragment Shader (GL_FRAGMENT_SHADER) is the shader that computes color and other attributes of the fragments generated by the rasterization into a single depth value ie. output pixel. Pixel shader range from outputting the shades of color, to applying the depth and lighting. Each fragment which is generated has a Window Space position, a few other values, and it contains all of the interpolated per-vertex output values from the last Vertex Processing stage. Fragment shaders may also be applied in intermediate stages to any two-dimensional images or textures in the pipeline. The output of a fragment shader is given to the buffers in the current framebuffers.
Compute Shaders
Compute shaders (GL_COMPUTE_SHADER) requires OpenGL 4.3 and it uses the resources for GPGPU for the same execution. They can be used for additional stages in animation or lighting algorithm in the graphics. Some rendering APIs allow compute shaders to easily share data resources with the graphics pipeline.
Building Application
Many libraries like GLUT helps to create OpenGL rendering window and handle its events like resizing the window, keyboard events, and mouse events. The framework like Qt provides all the functionality of GLUT plus a whole lot more, so we are going to use Qt in our application.
In Qt, the class used to set-up rendering is QOpenGLWidget. 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.
Legacy OpenGL code block
Let’s show the famous OpenGL triangle.
class Window : QOpenGLWidget
{
    Window()
    {

    }

    void initializeGL()
    {
        QOpenGLWindow::initializeGL();
        glMatrixMode(GL_MODELVIEW);
        glLoadIdentity();
    }

    void resizeGL(int w, int h)
    {
        glViewport(0, 0, (GLsizei) w, (GLsizei) h);
        glMatrixMode(GL_PROJECTION);
        glLoadIdentity();
        update();
    }

    void paintGL()
    {
        glClear(GL_COLOR_BUFFERBIT);
        glBegin(GL_TRIANGLES);
            glColor3f(1.0, 0.0 0.0); glColor3f(-1.0, -1.0, 0.0);
            glColor3f(0.0, 1.0 0.0); glColor3f( 0.0,  1.0, 0.0);
            glColor3f(0.0, 0.0 1.0); glColor3f( 1.0, -1.0, 0.0);
        glEnd();
        glutSwapBuffers();
    }
}
Above code will render a triangle in OpenGL window.
Modern OpenGL code block
Now let’s use modern OpenGL for drawing the same triangle
class Window: public QOpenGLWidget, public 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();

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

    QOpenGLShaderProgram     m_pgm;
	QOpenGLVertexArrayObject m_vao;
	QOpenGLBuffer            m_vbo;
};
However, the above code 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”.
For using shaders we need to modify createGeoemtry() function and also need one more function to create shaders.
void createGeometry()
    {
        // Initialize and bind the VAO that's going to capture all this vertex state
        m_vao.create();
        m_vao.bind();

        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();
    }
Replace createGeometry() function with the above new code. Add below createShaderProgram() function and call it in initializeGL() function before createGeometry() function call.
But before that, we require shader programs. Let's write them.
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"
;
Fragment Shader
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"
;
Below is the createShaderProgram() function used to initialize and bind shader programs to OpenGL pipeline.
void Window::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);
	}
}
Output
Rendering a triangle in the OpenGL window with Shader
About authors
Vinayak Sutar
Vinayak is experienced Software Development Manager with a demonstrated history of working in the computer software industry. He is skilled in programming languages such as C++, C#, Javascript, Typescript, Python, OpenGL-GLSL, technologies like Computer-Aided Design (CAD), Autodesk Forge Platform, Cloud Computing (Amazon Web Services), and Multithreading. He is a strong information technology professional with a master's in computational technology degree, focused in IT from Tilak Maharashtra Vidyapeeth. Vinayak is passionate about exploring the joys of computing. He likes to explore new cutting-edge technologies such as Machine Learning, Artificial Intelligence etc. And he wants to do this while having fun... pushing boundaries and imagining possibilities.
Charuhas Naigaonkar
Charuhas is a Member of Technical Staff at CCTech. He holds the B.E in Electronics and Telecommunication from Pune University. He is a software developer and has experience in web development as well as in desktop applications. He enjoys experimenting with various technologies and skillsets like C#, C++, Javascript framework Meteor, Node, ExpressJS. He has contributed in various projects which include simulationHub, Onshape and Autodesk's Dasher. He likes to explore new Computing technologies like Virtual Reality, Machine Learning, and AI. Apart from this he is an enthusiast, traveler, loves to read books and play sports.
Comments