How to do it...

The main ingredient here is the depth value, which determines which shapes should appear in front of or behind the other shapes. The primitive shape that is positioned behind another surface (with a shallower depth than another shape) will not be rendered (or will be partially rendered). OpenGL provides a simple way to achieve this:

  1. Let's continue our project from the previous 2D example. Enable depth testing by adding glEnable(GL_DEPTH_TEST) to the initializeGL() function in renderwindow.cpp:
void RenderWindow::initializeGL()
{
openGLFunctions = openGLContext->functions();
glEnable(GL_DEPTH_TEST);
  1. We will change our vertices array into something longer, which is the vertex information of a 3D cube shape. We can remove the colors array for now since you are not supplying the color information to the shader this time. We can also remove the vbo_colors VBO for the same reason:
GLfloat vertices[] = {
-1.0f,-1.0f,-1.0f,1.0f,-1.0f,-1.0f,-1.0f,-1.0f, 1.0f,
1.0f,-1.0f,-1.0f,1.0f,-1.0f, 1.0f,-1.0f,-1.0f, 1.0f,
-1.0f, 1.0f,-1.0f,-1.0f, 1.0f, 1.0f,1.0f, 1.0f,-1.0f,
1.0f, 1.0f,-1.0f,-1.0f, 1.0f, 1.0f,1.0f, 1.0f, 1.0f,
-1.0f,-1.0f, 1.0f,1.0f,-1.0f, 1.0f,-1.0f, 1.0f, 1.0f,
1.0f,-1.0f, 1.0f,1.0f, 1.0f, 1.0f,-1.0f, 1.0f, 1.0f,
-1.0f,-1.0f,-1.0f,-1.0f, 1.0f,-1.0f,1.0f,-1.0f,-1.0f,
1.0f,-1.0f,-1.0f,-1.0f, 1.0f,-1.0f,1.0f, 1.0f,-1.0f,
-1.0f,-1.0f, 1.0f,-1.0f, 1.0f,-1.0f,-1.0f,-1.0f,-1.0f,
-1.0f,-1.0f, 1.0f,-1.0f, 1.0f, 1.0f,-1.0f, 1.0f,-1.0f,
1.0f,-1.0f, 1.0f,1.0f,-1.0f,-1.0f,1.0f, 1.0f,-1.0f,
1.0f,-1.0f, 1.0f,1.0f, 1.0f,-1.0f,1.0f, 1.0f, 1.0f
};
  1. In the paintEvent() function, we must add GL_DEPTH_BUFFER_BIT to the glClear() function since we enabled depth checking in initializeGL() in the previous step:
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
  1. After that, we need to send a matrix information to the shader called Model-View-Projection (MVPso that the GPU knows how render the 3D shapes on a 2D screen. The MVP matrix is the result of multiplication between the projection matrix, view matrix, and model matrix. The multiplication order is very important so that you get the correct result:
QMatrix4x4 matrixMVP;
QMatrix4x4 model, view, projection;
model.translate(0, 1, 0);
model.rotate(45, 0, 1, 0);
view.lookAt(QVector3D(5, 5, 5), QVector3D(0, 0, 0), QVector3D(0, 1, 0));
projection.perspective(60.0f, ((float)this->width()/(float)this->height()), 0.1f, 100.0f);
matrixMVP = projection * view * model;
shaderProgram->setUniformValue("matrix", matrixMVP);
  1. Change the last value in glDrawArrays() to 36 since we now have 36 triangles in the cube shape:
glDrawArrays(GL_TRIANGLES, 0, 36);
  1. We have to go back to our shader code and change some parts of it, as highlighted in the following code:
static const char *vertexShaderSource =
"#version 330 core\n"
"layout(location = 0) in vec3 posAttr;\n"
"uniform mat4 matrix;\n"
"out vec3 fragPos;\n"
"void main() {\n"
"fragPos = posAttr;\n"
"gl_Position = matrix * vec4(posAttr, 1.0); }";

static const char *fragmentShaderSource =
"#version 330 core\n"
"in vec3 fragPos;\n"
"out vec4 col;\n"
"void main() {\n"
"col = vec4(fragPos, 1.0); }";
  1. If you build and run the project now, you should see a colorful cube appear on the screen. We use the same vertices array for the color, which gives this colorful result:

  1. Even though the result looks pretty good, if we want to really show off the 3D effect, it would be by animating the cube. To do that, first, we need to open up renderwindow.h and add the following variables to it. Note that you are allowed to initialize variables in the header file in modern C++ standard, which was not the case back in the older C++ standard:
QTime* time;
int currentTime = 0;
int oldTime = 0;
float deltaTime = 0;
float rotation = 0;
  1. Open up renderwindow.cpp and add the following highlighted code to the class constructor:
openGLContext = new QOpenGLContext();
openGLContext->setFormat(format);
openGLContext->create();
openGLContext->makeCurrent(this);

time = new QTime();
time->start();
  1. After that, add the following highlighted code to the top of your paintEvent() function. deltaTime is the value of the elapsed time of each frame, which is used to make animation speed consistent, regardless of frame rate performance:
void RenderWindow::paintEvent(QPaintEvent *event) {
Q_UNUSED(event);
// Delta time for each frame
currentTime = time->elapsed();
deltaTime = (float)(currentTime - oldTime) / 1000.0f;
oldTime = currentTime;
  1. Add the following highlighted code on top of your MVP matrix code and apply the rotation variable to the rotate() function, like so:
rotation += deltaTime * 50;

QMatrix4x4 matrixMVP;
QMatrix4x4 model, view, projection;
model.translate(0, 1, 0);
model.rotate(rotation, 0, 1, 0);
  1. Call the update() function at the end of the paintEvent() function so that paintEvent() will be called again and again at the end of each draw call. Since we are changing the rotation value in the paintEvent() function, we can give the viewer the illusion of a rotating cube:
    glDrawArrays(GL_TRIANGLES, 0, 36);

shaderProgram->release();
vao->release();

this->update();
}
  1. If you compile and run the program now, you should see a spinning cube in your render window!