Plotting 4D Graphs With QOpenGLWidget

A 4D graph can be used for plotting a graph of a complex function, with complex numbers as its input and output. A complex number can be represented as a pair of two real numbers:

• A real part and an imaginary part.
• A magnitude and an argument.

An image has a width and a height, but you can add an illusion of depth using a linear transform and changes in hue. A common way to add the fourth dimension is to use colors to represent its value. If you use a color for the fourth dimension, you want to add a legend to tell the viewer how to estimate the value of the fourth coordinate.

A good example of one such graph is the graph of the Lambert function found in the Wikipedia.

The Lambert W function is defined as follows:

$x=ye^y\Rightarrow y=W_n(x)$

where $n \in \mathbb{Z}$ is the branch number.

In this post, I will discuss the use of Qt5 modules in creating my version of the graph:

The graph is not perfect and complete, but in this post I will explain how to create the components of the graph.

Combining OpenGL and 2D Painting

In Qt5, the OpenGL widget is a painting device like any other widget. So, you can use a painter of class QPainter to paint 2D shapes on it. The painter is useful if you want to add text to an image. Remember to unbind every GL object (vertex array objects, buffers, textures) before painting with the painter.

Before painting with the GL functions, call the painter’s method beginNativePainting. After finishing the use of GL functions, call endNativePainting

If you want the text rotation, you can use SVG. A simple way to add SVG text is using the QSvgRenderer class, that can create an SVG image from a file or a string.

Single Points

You can draw single points in OpenGL. To do so, you must enable GL_PROGRAM_POINT_SIZE using glEnable in your calling program, and define glPointSize in the vertex shader.

Matrices

The main types of matrices used in my example are:

• Frustum – A perspective matrix defined using the minimum and maximum values of coordinates.
• Viewport – a matrix that transforms the coordinates used by GL into the coordinates of the input rectangle. That transform will be used for placing the axis ticks snd texts.

The Program

The main window is a Qt Widget, so

The QMake File: qmake.pro:

The following definition will be used for creating the make file.

TARGET=executable
SOURCES=main.cpp

DESTDIR=bin
QT=core
QT+=gui
QT+=svg

greaterThan(QT_MAJOR_VERSION, 4): QT+=widgets


The header files ‘graph.h’ contains some definition to be used by the main program and the widget extending the QT Widget:

#include <QtWidgets/QOpenGLWidget>
#include <QOpenGLBuffer>
#include <QOpenGLVertexArrayObject>
#include <QOpenGLTexture>
#include <QPainter>
#include <QtSvg>
#include <QStyleOptionGraphicsItem>

struct graph_point {
GLfloat coords[3];
GLfloat hue;
};

struct vertex2D {
GLfloat coords[2];
GLfloat hue;
};

struct axis_ticks {
GLfloat rect_coords[3];
GLfloat texture_coords[2];
};

class GraphWidget:public QOpenGLWidget{
private:
QOpenGLVertexArrayObject m_vao;
QOpenGLBuffer m_point_vbo;
QOpenGLBuffer m_ibo;

QPainter m_painter;
QFont m_font;
int fontSize;
int m_width, m_height;
float legend_left;
QMatrix4x4 transform;
QMatrix4x4 viewport;

void populatePointBuffer(void);
void paintGraph(void);
void draw3DLine(QVector4D fromVec, QVector4D toVec);

public:
GraphWidget(QWidget *parent=nullptr);
~GraphWidget();
void initializeGL();
void paintGL();
void resizeGL(int w, int h);
};


HSV to RGB

The fourth coordinate of our graph represents arg(W(z)), an angle. In the graph we’ll use HSV (Hue, Saturation, Value) because the hue is given by the value of an angle. A shader-language function to covert from HSV to RGB is defined in file “shaders.h“:

#define TO_RGB_FUNC					\
"#version 140\n"					\
"vec4 to_rgb(float hue){ \n"				\
"float s=1.0;\n"					\
"float v=1.0;\n"					\
"float c=v*s;\n"					\
"float   x=c*(1-abs(mod(3*hue,2)-1));\n"		\
"float   m=v-c;\n"					\
"vec3  tempRGB=hue< -2./3?  vec3(0,x,c):\n"		\
"              hue< -1./3?  vec3(x,0,c): \n"		\
"              hue<     0?  vec3(c,0,x): \n"		\
"              hue<  1./3?  vec3(c,x,0): \n"		\
"              hue<  2./3?  vec3(x,c,0): \n"		\
"		              vec3(0,c,x); \n"		\
"\n"							\
"return vec4(tempRGB+vec3(m,m,m),1);\n"		\
"}\n"


The shader fragment for both the graph points and the legend is defined as follows:

#define FRAGMENT_SHADER				\
TO_RGB_FUNC					\
"varying float f_hue;\n"			\
"void main(void){\n"				\
"    gl_FragColor=to_rgb(f_hue);\n"		\
"}\n"


Main Program Includes and Definitions

#include <stdio.h>

#include <graph.h>
#include <QApplication>
#include "svg.h"
#include <complex>
#include <iostream>

#define MIN_WIDTH 640
#define MIN_HEIGHT 480

using namespace std;



Constructor and Destructor

GraphWidget::GraphWidget(QWidget *parent):
QOpenGLWidget(parent),
m_vao(),
m_point_vbo(),
m_ibo(QOpenGLBuffer::IndexBuffer),
m_font()
{
setGeometry(0,0,MIN_WIDTH,MIN_HEIGHT);
setMinimumWidth(MIN_WIDTH);
setMinimumHeight(MIN_HEIGHT);
}

GraphWidget::~GraphWidget(){
makeCurrent();
m_point_vbo.destroy();
m_ibo.destroy();
m_vao.destroy();
doneCurrent();

}

// NUM_ARGS - Args of complex numbers.
// NUM_MAGNS - Number of magnitudes.
#define NUM_ARGS 140
#define NUM_MAGNS 200


Initializing, Populating the Point Buffer

Here we’ll define the variables that are initialize once. The following function, initializeGL, is called once upon the widget’s initization:

void GraphWidget::initializeGL(){
makeCurrent();
glEnable(GL_BLEND);
glEnable(GL_DEPTH_TEST);
glEnable( GL_PROGRAM_POINT_SIZE );
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

m_vao.create();

m_point_vbo.create();
m_point_vbo.bind();
m_point_vbo.allocate(NUM_MAGNS*NUM_ARGS*sizeof(graph_point));
populatePointBuffer();

GLshort indices[6]={0,1,2,2,3,0};
m_ibo.create();
m_ibo.bind();
m_ibo.allocate(indices,sizeof(indices));
m_ibo.release();
m_point_vbo.release();
}


A call to glEnable( GL_PROGRAM_POINT_SIZE ); enables drawing single points.

The function populatePointBuffer uses the method ‘write‘ of class QOpenGLBuffer to write points to the vertex buffer. I use the buffer this way so I can use functions of complex variables.

void GraphWidget::populatePointBuffer(void){
int vboOffset=0;
graph_point vec[NUM_MAGNS];
for (float magn=0;magn<1.4;magn+=1.4/NUM_MAGNS){
int pos=0;
for (float arg=-1;arg<1;arg+=2./NUM_ARGS){
// The arg of W(Z) divided by pi.
complex<float> iarg;
iarg=complex<float>(0,M_PI*arg);
complex<float> w=magn*exp(iarg);
complex<float> z=w*exp(w);
vec[pos].hue=arg;
vec[pos].coords[0]=imag(z);
vec[pos].coords[1]=magn;
vec[pos].coords[2]=real(z);
pos+=1;
}
m_point_vbo.write(vboOffset,vec,sizeof(vec));
vboOffset+=sizeof(vec);
}
}


Resizing

The function resizeGL is called as a response to a widget resize event. In this function the values that change due to a resize event are set. The properties set here are:

• The font size for the painter
• The transform matrix
• The viewport matrix
void GraphWidget::resizeGL(int w, int h){
m_width=w;
m_height=h;
fontSize=12.* min((float)m_width/MIN_WIDTH,(float)m_height/MIN_HEIGHT);
m_font.setPixelSize(fontSize);
float z_far=0.05;
float z_near=0.01;
float a=2./(m_height-2*fontSize);
float b=1.8-a*m_height;
float y_floor=b-2.*a*fontSize;

// leftInPixels,RightInPixels - left and right boundaries
//                              of the graph.
float leftInPixels=12*fontSize+7;
float rightInPixels=m_width - 12 - 15*fontSize;
a=2/(rightInPixels-leftInPixels);
b=-1-a*leftInPixels;
float x_left=b;
float x_right=a*m_width+b;

transform.setToIdentity();

transform.frustum(x_left,x_right,y_floor,1.8,z_near,z_far);
transform.translate(0,0,-(z_far+z_near)/2);
transform.scale(1,1,(z_far-z_near)/2);
viewport.setToIdentity();
viewport.viewport(rect());
}


Painting

The function paintGL is called as a response to paint events. Following is the code of paintGL:

void GraphWidget::paintGL(){
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
m_painter.begin(this);
m_painter.setPen(QColor(255,255,255,255));
m_painter.setFont(m_font);

paintGraph();

m_painter.end();
}


Let us look at the functions called by the paint event handler:

The function addLegend uses OpenGL function to create the HSV gradient and the painter to add ticks and text values:

void GraphWidget::addLegend(void){
QString str="arg(W(Z))";

QRect boundingRect=m_painter.boundingRect(rect(),0,str);

m_painter.beginNativePainting();

m_vao.bind();
QMatrix4x4 mat;
mat.ortho(rect());
GLfloat top=boundingRect.height()+20;
GLfloat bottom=top+0.75*m_height;
GLfloat left=m_width-boundingRect.width()-10;
GLfloat right=left+0.25*boundingRect.width();
legend_left = left; // Add it to the private members.
vertex2D rect_vertices[4]={{{left,top},1},{{right,top},1},
{{right,bottom},-1},{{left,bottom},-1}};
m_ibo.bind();
GLshort indices[6]={0,1,2,2,3,0};
m_ibo.write(0,indices,sizeof(indices));

m_program->bind();
m_program->setAttributeBuffer("legend_coords",
GL_FLOAT,
0,
2,
sizeof(vertex2D));
m_program->setAttributeBuffer("legend_hue",
GL_FLOAT,
sizeof(rect_vertices[0].coords),
1,
sizeof(vertex2D));
m_program->setUniformValue("ortho",mat);

m_vao.release();
m_vao.bind();
m_program->enableAttributeArray("legend_coords");
m_program->enableAttributeArray("legend_hue");
glDrawElements(GL_TRIANGLES,6,GL_UNSIGNED_SHORT,0);

m_vao.release();
m_program->disableAttributeArray("legend_coords");
m_program->disableAttributeArray("legend_hue");
m_program->release();
m_ibo.release();

m_painter.endNativePainting();
m_painter.drawText(m_width-boundingRect.width()-10,boundingRect.height()+10,str);

QString vals[7]={"pi","2pi/3","pi/3","0","-pi/3","-2pi/3","-pi"};
float curr_height=top;
float dist=(bottom-top)/6;
for (int i=0;i<7;i++){
m_painter.drawLine(right,curr_height,right+10,curr_height);
m_painter.drawText(right+15,curr_height+0.4*boundingRect.height(),vals[i]);
curr_height+=dist;
}
}


Following is the macro in shaders.h, which defines the vertex shader for the legend:

#define LEGEND_VERTEX_SHADER					\
"attribute vec2 legend_coords;\n"				\
"attribute float legend_hue;\n"				\
"uniform mat4 ortho;\n"					\
"varying float f_hue;\n"					\
"void main(void){\n"						\
"    gl_Position=ortho*vec4(legend_coords,0,1);\n"		\
"    f_hue=legend_hue;\n"					\
"}"


The function addAxes uses the painter and the viewport matrix to draw axes and add the ticks. You hove noticed that the matrices are of size 4×4 and the vectors are of length 4; when a matrix is multiplied by 4, you should divide coordinates x,y by coordinate w to find the vector’s location on the 2D window. Special treatment is added to the slanted z-axis, where SVG is used:

void GraphWidget::addAxes(void){

QVector4D startVec,endVec;

// Draw the x-axis
startVec=viewport*transform*QVector4D(-1,-0.2,1.1,1);
endVec=viewport*transform*QVector4D(1,-0.2,1.1,1);
draw3DLine(startVec,endVec);

// Draw the y-axis
startVec=viewport*transform*QVector4D(-1,-0.2,1.1,1);
endVec=viewport*transform*QVector4D(-1,1.4,1.1,1);
draw3DLine(startVec,endVec);

// Draw the z-axis
startVec=viewport*transform*QVector4D(1,-0.2,-1,1);
endVec=viewport*transform*QVector4D(1,-0.2,1.1,1);
draw3DLine(startVec,endVec);
}

void GraphWidget::draw3DLine(QVector4D fromVec, QVector4D toVec){
m_painter.drawLine(fromVec.x()/fromVec.w(),
m_height-fromVec.y()/fromVec.w(),
toVec.x()/toVec.w(),
m_height-toVec.y()/toVec.w());
}

QVector4D start_point,end_point;
QString vals[]={"-1","-0.5","0","0.5","1"};
int i=0;
for (float loc=-1;loc<=1;loc+=0.5){
start_point=viewport*transform*QVector4D(loc,-0.2,1.1,1);
start_point.setX(start_point.x()/start_point.w());
start_point.setY(m_height-start_point.y()/start_point.w());
end_point=start_point;
end_point.setY(end_point.y()+5);
m_painter.drawLine(start_point.x(),start_point.y(), end_point.x(),end_point.y());
m_painter.drawText(start_point.x(),end_point.y()+fontSize+1,vals[i++]);
}
const char *text="Im(Z)";
QVector4D start_vector=viewport*transform*QVector4D(-1,-0.2,1.1,1);
QVector4D end_vector=viewport*transform*QVector4D(1,-0.2,1.1,1);
float text_start=(start_vector.x()+end_vector.x())/(2.*end_vector.w())-2*fontSize;
float bottom=m_height-end_vector.y()/end_vector.w()+2*fontSize+5;
m_painter.drawText(text_start,bottom,text);
}

QVector4D start_point,end_point;
QString vals[]={"0.0","0.2","0.4","0.6","0.8","1.0","1.2","1.4"};
int i=0;
for (float loc=0;loc<=1.41;loc+=0.2){
start_point=viewport*transform*QVector4D(-1,loc,1.1,1);
start_point.setX(start_point.x()/start_point.w());
start_point.setY(m_height-start_point.y()/start_point.w());
end_point=start_point;
end_point.setX(end_point.x()-5);
m_painter.drawLine(start_point.x(),start_point.y(), end_point.x(),end_point.y());
m_painter.drawText(end_point.x()-2*fontSize,end_point.y()+0.5*fontSize,vals[i++]);

}
const char *text="|W(z)|";
QVector4D start_vector=viewport*transform*QVector4D(-1,-0.2,1.1,1);
QVector4D end_vector=viewport*transform*QVector4D(1,1,1.1,1);
float text_y_position=(start_vector.y()+end_vector.y())/(2.*end_vector.w())-2*fontSize;
float text_rightmost=start_vector.x()/start_vector.w()-6*fontSize;
m_painter.drawText(text_rightmost,text_y_position,text);
}

QVector4D start_point,end_point;
QString vals[]={"-1","-0.5","0","0.5","1"};
int i=0;
for (float loc=-1;loc<=1;loc+=0.5){
start_point=viewport*transform*QVector4D(1,-0.2,loc,1);
start_point.setX(start_point.x()/start_point.w());
start_point.setY(m_height-start_point.y()/start_point.w());
end_point=start_point;
end_point.setY(end_point.y()-5);
m_painter.drawLine(start_point.x(),start_point.y(), end_point.x(),end_point.y());
if (i!=1){
m_painter.drawText(end_point.x()-fontSize/2.,end_point.y()-1,vals[i]);
}
i+=1;
}
}

char *svg_str=(char *)calloc(strlen(SVG_TEXT_STR)+1,sizeof(char));
const char *text="Re(Z)";
QVector4D start_point,end_point;
start_point=viewport*transform*QVector4D(1,-0.2,1,1);
start_point.setX(start_point.x()/start_point.w());
start_point.setY(m_height-start_point.y()/start_point.w());
end_point=viewport*transform*QVector4D(1,-0.2,-1,1);
end_point.setX(end_point.x()/end_point.w());
end_point.setY(m_height-end_point.y()/end_point.w());
float centerX=(start_point.x()+end_point.x())/2.;
float centerY=(start_point.y()+end_point.y())/2.;
float lenOfText=(strlen(text)-1.5)*fontSize;
float textX=centerX-lenOfText;
float textY=centerY+fontSize;

sprintf(svg_str,SVG_TEXT_STR,textX,textY,fontSize,inDegrees,centerX,centerY,text);
cout<<svg_str<<endl;
cout<<"Length of svg_str"<<strlen(svg_str)<<endl;
cout<<"Lenght of SVG_TEXT_STR"<<strlen(SVG_TEXT_STR)<<endl;

QSvgRenderer renderer((QByteArray(svg_str)));
renderer.render(&m_painter,rect());

}



The function add_z_text renders the SVG text using an object of call QSvgRendered. It accepts a byte array made from a string. It can also take a file name. The macro SVG_TEXT_STR is defined in file “svg.h” as follows:/code

#define SVG_TEXT_STR							\
"<svg>"								\
"    <text x='%.0f'" 							\
"          y='%.0f'"							\
"          font-size='%dpx'"						\
"          stroke='white'"						\
"          fill='white'"						\
"          transform='rotate(%.0f ,  %.0f ,  %.0f)'>%s</text>"	\
"</svg>"



Painting the Graph

Following is the function that paints the graph:

void GraphWidget::paintGraph(){
m_painter.beginNativePainting();
m_vao.bind();
m_point_vbo.bind();
m_program->bind();
m_program->setAttributeBuffer("graph_coords",
GL_FLOAT,
0,
3,
sizeof(graph_point));
m_program->setAttributeBuffer("hue",
GL_FLOAT,
3*sizeof(GLfloat),
1,
sizeof(graph_point));
m_program->setUniformValue("transform",transform);
m_vao.release();
m_vao.bind();
m_program->enableAttributeArray("graph_coords");
m_program->enableAttributeArray("hue");
glDrawArrays(GL_POINTS,0,NUM_ARGS*NUM_MAGNS);
m_program->release();
m_point_vbo.release();
m_vao.release();
m_painter.endNativePainting();
}


The Main Function

Following is the code of the main function:

int main(int argc, char *argv[]){
QApplication app(argc, argv);
GraphWidget w;
w.show();

return app.exec();
}


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:

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.

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
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’.

GUI Problems? Time to Upgrade Qt

Having upgraded my FreeBSD system and VirtualBox, I found that VirtualBox does not work when invoked from the window manager’s menu. So, I tried running it from the command line, and got the following message:

/usr/local/lib/virtualbox/VirtualBox.so: Undefined symbol “_ZNK6QColor6darkerEi@Qt_5”

I tried to search the web for the cause, and found a link to a compatibility report including added symbols. My symbol was nott found there, but at least I got a clue about what I had to do: upgrade Qt5.

From some reasons I’m not notified that it’s time to upgrade my Qt5.

I found a great option for command pkg:I

pkg version| grep qt5


The output reveals the current version of a package, and if there are later versions; following is a typical output line:

qt5-printsupport-5.12.2 <

vlc cdda:///dev/cd0