OpenGL Made Simple with Qt5

Drawing and painting a 3D image with GL – the Graphic Library – is not as trivial as doing the same on a 2D canvas: no 3D versions of the 2D canvas drawing methods are defined in GL classes. Instead, programs written with GL send data to the GPU (Graphic Processing Unit), and directions for organizing the data in data structures and for the rendition of data to the screen. The data is stored in buffers, and processed by shader programs.

Once you know how to use the OpenGL API, drawing the following image is simple:

The way Qt simplifies the use of GL are as follows:

  • it offers classes instead of integers to wrap GL resources: buffers, textures, vertex array and more.
  • the classes include overloaded functions.
  • the developer can create shaders without specifying the GL version.
  • Qt manages the main loop.

The following sections will describe the process of painting a 3D surface.

Initializing

To start working with GL, create an object of class QOpenGLWidget. Better create a subclass of QOpenGLWidget, if you work with Qt 5.4 or a later version. Don’t perform any GL operations before the method initializeGL is called. If you extend OpenGLWidget, any GL operation should be performed by one of the following methods: initializeGL, paintGL and resizeGL.

In initializeGL place operation to be performed once only at the initialization time. Some examples of initializeGL can be found in

Qt Assistnat->Qt Widgets->C++ Classes->QOpenGLWidget

click the code examples link.

Following is the initialization stage of my program to paint the triangle:

The Class

Following is a definition of the ExtendedOpenGLWidget class:

#include <QtWidgets/QOpenGLWidget>
#include <QOpenGLVertexArrayObject>
#include <QOpenGLBuffer>
#include <QOpenGLShaderProgram>
#include <QOpenGLShader>
#include <QOpenGLContext>


class ExtendedOpenGLWidget:public QOpenGLWidget {
    private:
        QOpenGLVertexArrayObject m_vao;
        QOpenGLBuffer m_vbo;
        QOpenGLShaderProgram *m_program;
        QOpenGLShader *m_fragmentShader, *m_vertexShader;
    public:
        ExtendedOpenGLWidget(QWidget *parent=nullptr);
        ~ExtendedOpenGLWidget();
        void initializeGL();
        void paintGL();
};

The class is defined in file “ui_opengl.h”.

Include Headers

#include <ui_opengl.h>
#include <iostream>

using namespace std;

The file “ui_opengl” is the header file generated by th Qt Designer. The rest can be used for debug printing.

The vertices

Vertices coordinates and colors are defined in my program as follows:

struct vertex {
float coords[3];
float colors[4];
};

struct vertex {
     float coords[3];
     float colors[4];
 };
 vertex vertices[]={
     {{-0.9, -0.9, 0.0}, {0.0, 0.0, 1.0, 1.0}},
     {{ 0.9, -0.9, 0.0}, {0.0, 1.0, 0.0, 1.0}},
     {{ 0.0,  0.9, 0.0}, {1.0, 0.0, 0.0, 1.0}}
 };

The above is a definition of a global variable. If you put it in a class, you will have to specify array sizes.

Constructor

The only thing the constructor does is call the parent constructor:

ExtendedOpenGLWidget::ExtendedOpenGLWidget(QWidget *parent):QOpenGLWidget(parent){
 }

The initializeGL method

This function creates the vertex array object(VAO). If the VAO cannot be created, it cannot be bound to the context. Your program can draw with or without it. In my example, I bind the VAO.

What actually does the drawing is the shader program. For it to draw you should link it at run time with the shaders. If you use Qt 5, the class QOpenGLShaderProgram exists with a method addShaderFromSourceCode, so you can create program in separate text files without the need to recompile your GL program when the shader code changes.

The example in Qt Assistant->Qt GUI->C++ Classes->QOpenGLShaderProgram does not complete the example in Qt Assistnat->Qt Widgets->C++ Classes->QOpenGLWidget; Don’t use it if you bind a buffer, because the coordinates and colors defined by setAttributeArray and setUniformValue will not set the correct locations of the colors of coordinates for the drawing method. In addition, only use coordinates specified in pixel if the GL Widget is of fixed size.

Following is an initializeGL method for coordintes whose values are between -1 and 1, and the color RGBA values are floats between 0 and 1:

void ExtendedOpenGLWidget::initializeGL(){
     glEnable(GL_BLEND);
     glEnable(GL_DEPTH_TEST);
     glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
     m_vao.create();
     if (m_vao.isCreated()){
         cout<<"Created"<<endl;
         m_vao.bind();
     }
     m_vbo.create();
     m_vbo.bind();

     // Allocating buffer memory and populating it
     m_vbo.allocate(vertices, sizeof(vertices));

     m_fragmentShader=new QOpenGLShader(QOpenGLShader::Fragment);
     m_fragmentShader->compileSourceFile("../fragmentshader.txt");
     m_vertexShader=new QOpenGLShader(QOpenGLShader::Vertex);
     m_vertexShader->compileSourceFile("../vertexshader.txt");
     m_program=new QOpenGLShaderProgram();
     m_program->addShader(m_fragmentShader);
     m_program->addShader(m_vertexShader);
     m_program->link();
     m_program->bind();

     // Arguments of setAttributeBuffer in this example:
     //    1: A string, the corresponding attribute in the shaders.
     //    2: the type of each array element.
     //    3: the start point relative to the beginning of the current buffer.
     //    4: the number of elements in each vector.
     //    5: stride: the difference in bytes between two successive vectors
     m_program->setAttributeBuffer("vertex", GL_FLOAT,0,3,sizeof(vertex));
     m_program->setAttributeBuffer("color", GL_FLOAT,sizeof(vertices[0].coords),4,sizeof(vertex));
     m_program->enableAttributeArray("vertex");
     m_program->enableAttributeArray("color");
     m_vao.release();
 }

Shaders

The program uses two shaders:

  • a vertex shader – outputs the coordinates of which point
  • a fragment shader – outputs the color of which point.
  • Following are the files:

Vertex shader:

 attribute highp vec3 vertex;
 attribute highp vec4 color;
 varying highp vec4 f_color;
 void main(void){
     gl_Position=vec4(vertex,1.0);
     f_color=color;
 }

Fragment shader:

 varying mediump vec4 f_color;
 void main(void){
     gl_FragColor = f_color;
 }

Drawing

The method paintGL draws the image. In this example, it uses the function glDrawArrays with the mode GL_TRIANGLES. In 3D the only 3D polygon type a graphical program always knows how to fill because the vertices are on the same plan.

Following is the paintGL method:

void ExtendedOpenGLWidget::paintGL(){
     cout<<"paint"<<endl;
     glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
     m_vao.bind();
     // Arguments of setAttributeBuffer in this example:
     //    1. mode - how the shaders will draw the array
     //    2. start - first enabled array
     //    3. count - the number of indices.
     glDrawArrays(GL_TRIANGLES,0,3);
     m_vao.release();
}

The main function

An Open GL Widget may be a top-level window or a widget inside another container. In the following main function, the widget is inside a main window:

int main(int argc, char *argv[]){
     QApplication app(argc, argv);
     QMainWindow mainWindow;
     Ui_MainWindow mainObject;
     mainObject.setupUi(&mainWindow);
     mainWindow.show();
     return app.exec();
}

A Destructor

If you want to avoid warnings at the end of the execution, you better define a destructor to clean up the allocated objects.

Following is the code of one:

ExtendedOpenGLWidget::~ExtendedOpenGLWidget(){
     makeCurrent();
     m_vao.destroy();
     m_vbo.destroy();
     delete m_fragmentShader;
     delete m_vertexShader;
     delete m_program;
     doneCurrent();
 }

Learn more about OpenGL here

Design and Develop GUI with Qt5

Qt allows you to develop a multi-platform application, that you can develop a GUI application on one platform and deploy to many others (if you avoid system-specific extra modules). Qt contains modules for creating widgets from elementary ones to OpenGL 3D, which I will discuss later. In addition it contains module with other functionalities an application may need such as SQL, XML, web channels, web views and web sockets.

Qt can be downloaded from https://www.qt.io/ or installed using the UNIX/Linux package installers.

In addition to the QT libraries and header files, there are 3 useful resources:

  • Qt Designer – A GUI tool for the generation of Python and C++ headers from a form graphically designed.
  • Qt Assistant – A reference to the Qt classes, Example, the QMake tutorial, etc.
  • qmake – an easy-to-use utility that creates the Makefile.

In the following sections, I will show how to create a project using the three tools:

Creating the Main Window

Qt Designer is an editor with which GUI designers and developers can communicate. With the designer you can create, save and edit Designer UI Files (*.ui) and create class files from them for Python and C++ programs. The QMake program discussed later can use the “.ui” file to generate a header file from it. The file can be edited by creating top level windows such as widows and dialog, and dragging and dropping widgets into them. You can also access and modify their properties; if you are a developer and have a Desiger UI file, you can find names of object in your program.

Starting:

As you start the QT Designer, the following dialog pops up:

If you don’t see the dialog, choose File->New from the editor’s menu bar, to make the dialog pop up.

Choose your top-level widget from the “templates/forms” menu, and click Create; a window will be created:

Now, from the right-hand Property Editor, you can change some properties, for example: let us change the window title:

Adding widgets and promoting them

Now, you can choose widgets from the left-hand side of the editors and drag-and-drop them into the window:

For example, let us add a push button, and change its QAbstractButton property “text” to “Push me!”.

One of the other properties we can change is the QObject property “objectName”, which will be the name of the appropriate field of class QPushButton in the created class.

By right-clicking inside the widget and choosing “Promote to..” from the context-menu, you can choose to use a class that inherits from the gadget’s class. Don’t forget to define the new class and its constructor and methods.

Previewing

You can view your design by clicking Form->Preview from the editor’s menu bar.

Saving the created class

If you don’t want the Make program to generate a header file, you can save it from the Qt Designer tool.

  1. Choose Form->View C++ Vode/View Python Code from the editor’s menu bar.
    a dialog will open.
  2. Click the “save” icon to save the created header file.

Adding Functionality

This section will describe a little Hello World program – in C++ – that uses a header file created wit QtDesigner. The purpose of the program is to print “Hello, world” to the standard output, when the “Push Me” button is clicked. To add the mouse event handling the class QPushButton has been promoted to Extended Button. This section will describe the program, and will help you train yourself using Qt Assistant.

The main function

To learn what a the main function of a Qt Application with widgets should look like:

  1. Start the Qt Assistant if not started yet.
  2. Click the Contents tab
  3. Click Qt Widgets (highlighted in the following image:

You” see a heading reading “Getting Started Programming with Qt Widgets”. Scroll down and see the contents of a main source line.

In the Hello World program the code is as follows:

 #include "ui_example.h"
 #include <iostream>

 using namespace std;

 ExtendedButton::ExtendedButton(QWidget *parent):QPushButton(parent) {
 }
 void ExtendedButton::mouseReleaseEvent(QMouseEvent *event){
     cout<<"Hello, world!"<<endl;
 }
 int main(int argc, char **argv){
     QApplication a(argc,argv);
     Ui_MainWindow mainObject;
     QMainWindow mainWindow;
     mainObject.setupUi(&mainWindow);
     mainWindow.show();
     return a.exec();
 }

The main file includes the constructor of the class ExtendedButton. Its role is to call the parent’s constructor. Another function implemented is mouseReleaseEvent, which is an event handler. You can learn from Qt Assistant, that the method mouseReleaseEvent is an event handler by clicking Qt Widget->QWidget in the right-hand Contents tab, and then click the link reading “events” under detailed description in the pages contents.

The class ExtendedButton is defined in “extendedbutton.h”. Following is the definition:

#include <QtWidgets/QPushButton>

 class ExtendedButton:public QPushButton {
     public:
         ExtendedButton(QWidget *parent=nullptr);
         void mouseReleaseEvent(QMouseEvent *event);
 };

The main object of the application is defined in “ui_example.h”, the file generated by “Qt Designer”, you better avoid changing it manually if you want to modify the “.ui” file from which it was generated. You’ll see a warning at the beginning. The role of the Class Ui_MainWindow is to contain main window (or central widget) and its underlying widgets as public members. The method setupUi binds them.

Creating the qmake file

“qmake” is an easy-to-use utility that generates a Makefile to be used by the make command to generate binaries, objects, libraries. etc. Qt Assistant includes a Qt Manual. Following is the content of the example’s qmake file named ‘qmake.pro’:

TARGET=executable
SOURCES+=main.cpp
HEADERS+=extendedbutton.h
FORMS+=example.ui
DESTDIR=bin
QT = core gui
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets

If you want the Make program to create the main header file, you can include the “.ui” file generated by Qt Designer in a variable named “FORMS”. The “make” program in turn will generate a header file from it with the prefix “ui_” added to the “.ui” file name. For example, from a file named “example.ui” the “make” program will generate a header file named “ui_example.h”. In this case, don’t add “ui_example.h” to the variable “HEADERS”.

If you want to know what to add to QT, go to the <Class Category>->C++ Classes and click the Detailed Description link.If you want to run qmake without command line arguments, call the qmake file ‘qmake.pro’.