Adding some items to an OpenGL figure using a vertex buffer is difficult. Text is one example, if you don’t want it just across the screen. You can use the OpenGL widget as a simple 2D canvas, but you might want to rotate it around the y-axis for example and add varying depth to it as in the following figure:

To add texts and other images to surfaces, OpenGL offers the textures. In this post, I will show how to create and use one and apply transforms.
Images and Textures
A texture is another object to bind, which means telling OpenGL to use that texture. When drawing the image, the Fragment Shader will use a sampler to take the pixel color at the coordinates defined by the Vertex Shader.
Qt5 offers an OpenGL texture class named QOpenGLTexture. The texture can be generated from an image passed to the constructor. For example:
m_texture=new QOpenGLTexture(img);
The image is an object of class QImage, if you choose to draw it, instantiate the class QPainter passing the image to the constructor. For example:
QPainter painter(&img);
Note: choose the right size for your image and the text fonts because you create a raster image, and by scaling it you may get an image of poor quality.
Texture Coordinates
The values of each coordinate of the texture is a floating point number between 0 and 1. When passed to the Vertex Shader, each texture vector of coordinates (usually x and y coordinates) should match a vertex coordinate.
For example: (0,0) is the top-left coordinate and (1,1) is the bottom-right.
Transforms
Transforms are done by multiplying a 4X4 matrix by each vertex coordinate vector with a fourth coordinate added. It’s value is usually 1. The reason to add a coordinate is to apply more transforms than just scaling and rotating around the origins.
Interactivity
Sometimes we add an input widget, for example: push button and spin boxes. The input widget should react to operations done by the user, so it emits a signal. Another widget has to react to the signal, so it has a special function known as a slot, and the developer’s job is to connect the signal and the slot. The signal should have at least as many arguments as the slot. And a type in the slot should be implicitly converted to the corresponding one in the signal (for example: short to integer or float.
The connection is done using the static method ‘connect‘ of class QObject.
Here, for example is a connection of the SpinBox’s event ‘valueChanged‘ with the extended OpenGL Widget’s ‘changeVerticalAngle‘:
QObject::connect(mainObject.verticalAngleField,
SIGNAL(valueChanged(int)),
mainObject.imageWindow,
SLOT(changeVerticalAngle(int)));
A class that has a slot method should have the macro Q_OBJECT at the beginning of the private section. After adding that macro, run the command ‘qmake‘.
The Code
Following is the code of a program that draw text with a varying depth. It uses a spin box to change the vertical angle of the perspective transform.
The shaders
Following is the code of the vertex shader:
attribute vec3 vertex_coords;
attribute vec2 texture_coords;
varying vec2 f_texture_coords;
uniform mat4 ortho;
uniform mat4 perspective;
void main(void){
gl_Position=perspective*ortho*vec4(vertex_coords,1.0);
f_texture_coords=texture_coords;
}
The main function multiplys two transform matrix by the input vector. The global variable names are names of locations defined by the functions setAttributeByffer‘ and ‘setUniformLocation‘.
Following is the code pf the fragment shader:
varying vec2 f_texture_coords;
uniform sampler2D m_texture;
void main(void){
gl_FragColor=texture2D(m_texture, f_texture_coords);
}
The name ‘m_texture‘ is not set by the main program; it is just a name associated with the location of the bound texture. You canlearn more about the shading language here; Choose a book according to the version of shading language. You an find the version by printing the value of the string GL_SHADING_LANGUAGE_VERSION using the C++ command:
cout<<"Version: " << glGetString( GL_SHADING_LANGUAGE_VERSION )<< endl;
But it seems that the program links with an earlier version by default, so you better specify your version (if your graphical card supports it) using the directive “#version” at the very beginning of the shader’s code. For example: version 140
for SL version 1.40 Good example of shaders can be found in the Wikibooks OpenGL tutorial.
The extended OpenGL Widget
Following is the class extending QOpenGLWidget. It includes the vertical angle, aspect ratio, he slot function and other members:
#include <QtWidgets/QOpenGLWidget>
#include <QOpenGLVertexArrayObject>
#include <QOpenGLBuffer>
#include <QOpenGLShaderProgram>
#include <QOpenGLShader>
#include <QOpenGLTexture>
class ExtendedOpenGLWidget:public QOpenGLWidget {
Q_OBJECT
QOpenGLVertexArrayObject m_vao;
QOpenGLBuffer m_vbo;
QOpenGLBuffer m_ibo;
QOpenGLShaderProgram m_program;
QOpenGLTexture *m_texture;
QOpenGLShader *m_vertHexShader, *m_fragmentShader;
float aspectRatio;
int verticalAngle;
public:
ExtendedOpenGLWidget(QWidget *parent=nullptr);
~ExtendedOpenGLWidget();
void initializeGL();
void resizeGL(int w, int h);
void paintGL();
public slots:
void changeVerticalAngle(int value);
};
So, you can see how to add a slot.
Global Variables
The global variables of the main file are used for defining the data in the buffers:
- vertex coordinates and their matching texture coordinates side by side for the Vertex Buffer.
- vertex indices for the index buffer
The index buffer consists of the indices of vertices and their attributes that reside in the vertex buffer. The indices starting with 0,
struct vertex { float vertex_coords[3];
float texture_coords[2];
};
vertex vertices[]={{{0.0,0.0,1},{1.0,0.0}},
{{0.0,1,1},{1.0,1.0}},
{{0.0,1,0.5},{0.0,1.0}},
{{0.0,0.0,0.5},{0.0,0.0}}};
unsigned short vertex_indices[]={0,1,2,2,3,0};
Constructor and Destructor
In the constructor you can see how to initialize the index buffer object (m_ibo), which is not a pointer.
Following is the code of both the constructor and the destructor:
ExtendedOpenGLWidget::ExtendedOpenGLWidget(QWidget *parent):QOpenGLWidget(parent),
m_vbo(QOpenGLBuffer::VertexBuffer),
m_ibo(QOpenGLBuffer::IndexBuffer){
}
ExtendedOpenGLWidget::~ExtendedOpenGLWidget(){
makeCurrent();
m_vao.destroy();
m_vbo.destroy();
m_texture->destroy();
doneCurrent();
delete m_vertexShader;
delete m_fragmentShader;
delete m_texture;
}
Initializing
The function initializeGL defines the vertex array object, buffers, texture, shaders and shader program. In addition it creates and image, draws the text on it using the class QPainter. A texture is then instantiated using the image and created. Following is the code of the functiom:
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()){
m_vao.bind();
}
m_vbo.create();
m_vbo.bind();
m_vbo.allocate(vertices,sizeof(vertices));
m_ibo.create();
m_ibo.bind();
m_ibo.allocate(vertex_indices,sizeof(vertex_indices));
// Create an Image
QImage img(400,200,QImage::Format_RGBX8888);
QPainter painter(&img);
painter.fillRect(0,0,200,200,QColor(0,0,255,255));
painter.setPen(QColor(255,255,0,255));
QFont font=painter.font();
font.setPixelSize(48);
painter.setFont(font);
painter.drawText(5,75,"Hello!");
painter.end(); m_texture=new QOpenGLTexture(img);
m_texture->create();
m_vertexShader=new QOpenGLShader(QOpenGLShader::Vertex);
m_vertexShader->compileSourceFile("../vertex_shader.txt");
m_fragmentShader=new QOpenGLShader(QOpenGLShader::Fragment);
m_fragmentShader->compileSourceFile("../fragment_shader.txt");
m_program.addShader(m_vertexShader);
m_program.addShader(m_fragmentShader);
m_program.link();
m_program.bind();
}
Painting
The painting is done by the method ‘paintGL‘. In the example, the function draws the textured surface by calling ‘glDrawElemts‘. The function ‘glDrawElements‘ uses an index buffer with the indices to the vertices, so you won’t have to define a vertex twice. Associating the texture with a variable in the shaders – using a function such as ‘setUniformLocation‘ or ‘setAttributeBuffer‘ – is not require, so a function to do it does not exists.
Following is the function’s code
void ExtendedOpenGLWidget::paintGL(){
QMatrix4x4 orthoMat;
orthoMat.ortho(QRectF(0,0,aspectRatio,1));
QMatrix4x4 perspective;
perspective.perspective(verticalAngle,aspectRatio, .2,10);
m_program.setAttributeBuffer("vertex_coords",GL_FLOAT,0,3,sizeof(vertex));
m_program.setAttributeBuffer("texture_coords",GL_FLOAT,sizeof(vertices[0].vertex_coords),2,sizeof(vertex));
m_program.enableAttributeArray("vertex_coords");
m_program.enableAttributeArray("texture_coords");
m_program.setUniformValue("ortho",orthoMat);
m_program.setUniformValue("perspective",perspective);
m_vao.release();
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
m_vao.bind();
m_texture->bind();
m_program.bind();
glDrawElements(GL_TRIANGLES,6,GL_UNSIGNED_SHORT,0);
m_vao.release();
m_program.disableAttributeArray("vertexcoords");
m_program.disableAttributeArray("texture_coords");
}
Resizing
The method ‘resizeGL‘ is an event handler called when the OpenGL widget is resized. In the example it changes the aspect ratio of the widget. Following is the code:
void ExtendedOpenGLWidget::resizeGL(int w, int h){
aspectRatio=(float)w/h;
}
The Slot
The function ‘changedVerticalAngle‘ is a user-defined slot to be connected with the value-change event. It changes the vertical angle of the perspective transform, and calls update.The function ‘update‘ is a slot that accepts no arguments, and its role is to add a repaint event to the event loop. Using ‘repaint‘ might be risky if called by a function called by ‘paintGL‘ because the result of using it may cause endless recursion. So, better avoid using ‘repaint‘.
Following is the code:
void ExtendedOpenGLWidget::changeVerticalAngle(int value){
verticalAngle=value;
update();
}
The Main Function
The important part of the main function is that it connects signals sent by the spin box to the OpenGL widget:
int main(int argc, char *argv[]){
QApplication app(argc, argv);;
QMainWindow mainWindow;
Ui_MainWindow mainObject;
mainObject.setupUi(&mainWindow);
QObject::connect(mainObject.verticalAngleField,
SIGNAL(valueChanged(int)),
mainObject.imageWindow,
SLOT(changeVerticalAngle(int)));
mainObject.imageWindow->changeVerticalAngle(mainObject.verticalAngleField->value());
mainWindow.show();
return app.exec();
}
Now, to check if your image has been added, change the vertical angle to 0. A perspective matrix generated with a vertical angle zero is an identity matrix.