Qt5 OpenGL Part 2: 3D Rendering 23


In the last tutorial, we covered how to render a single triangle with color attributes. In this tutorial we’re going to extend upon that knowledge by rendering a batch of triangles in the form of a cube. This is going to require another helper class, and minor changes to a few parts of the code.

But in the end, it will be a big step up from our “Hello, OpenGL!” triangle from the previous tutorial.

The Transformation Pipeline

In Linear Algebra, there is a subset of Matrices known as Transformation Matrices. These matrices are such that they can be multiplied by a vector to produce a resultant vector that has been transformed in a meaningful geometric way by the matrix input (hence the name).

In graphics programming, we use these matrices all the time to move points around in space, and eventually draw them on screen. The last tutorial we did, there were no transformations invovled; we simply plotted points from [-1.0f, 1.0f], and OpenGL figured out the rest. As you can imagine, to draw 3D shapes, we probably need to do a little more.

Enter the Transformation Pipeline:

The transformation pipeline.

The transformation pipeline.

The Transformation Pipeline outlines the steps we can take to move ourselves into and out-of certain vector spaces. The easiest way to think of this is that all points in consideration are relative to whatever space we’re in. For example, if we’re in Object Space, all points are relative to the Object. If we’re in World Space, all points are relative to the World. etc. The most common transformations you move through are boxed-out above, but it’s far from an exhaustive list. If you can image that points can be relative to something, it can be a space.

OpenGL is expecting us to transform our points around through several spaces, and then pass it something that’s in View Space (sometimes known as Projection Space), which is what we were originally plotting points in back in the previous tutorial as [-1.0f, 1.0f]. We can freely move forward and backwards through the spaces, and for some rendering techniques, will need to. I feel that this kind of information is better learned through practice. We will be using Qt’s implementation of Vector and Matrix classes, but in the future I recommend you actually implement the transformations to understand them better.


 

3D Rendering

Okay, instead of spending time explaining OpenGL in detail (a task which has been done by many others in far more detail) – we’d better jump back into our implementation.

1. Create a Transform3D class!

There is a QTransform class. If you look for it, you can see that it’s there. This class represents a 2D transformation matrix, and can be used in OpenGL if all you’re doing is a 2D application. However, we want to be rendering in 3D.

In 3D, this becomes problematic because there are many ways to represent intermediate transformation data. In the end we want a matrix, sure – but we also want it to be relatively easy to manipulate on the CPU-side.

In fact, a 3D transformation is just complicated enough that we can end up over-thinking it, and just not producing anything usable or maintainable. So for now, I’m going to call “Premature Optimization” and make a simple Transform3D class which does just what we need, and nothing more.

Create a new class named Transform3D, and give it the following interface in Transform3D.h:

This looks scary, but just like the Vertex class, it doesn’t really have too much functionality – many functions are there for convenience. Really the only functionality is: translate, rotate, scale, grow, and toMatrix.

You might also notice that not all of the functionality is here. That’s because there is a part of this class that I want to keep out of the header. That also happens to be where the more interesting parts of the implementation are.

So in our Transform3D.cpp file:

Here, the Transformation functions all make sense – they simply modify the transformation, and then mark it as dirty. Things get a little interesting in the toMatrix() function.

Imagining that there could be objects at rest, we don’t have to recreate the Transformation Matrix every time. This matrix that we’re building is a matrix that will bring points which are in Object Space, to points which are within World Space – or points representing the object having moved around the world.

If you don’t have sufficient knowledge of Linear Algebra, I will at least mention that the order in which the matrix is formed is important. Note that it resets, applies translation, applies rotation, and then applies scale. Depending on how a matrix is laid out (row-major versus column-major) this could be reversed, but do be aware that the order in which we create these matrices matters greatly.

The next three functions introduce another part of Qt, which are the Debug and Data streams. We’ve used QDebug a little, this is what gets called when you write code like:  qDebug() << someVariable; it’s solely for printing debug data.

QDataStream is a stream which allows us to read/write binary data. It’s going to piggyback off the QDataStream functions provided for QVector3D and QQuaternion, so not much is needed in terms of implementation. This may become useful if we decide to save a world’s state later.

Note: I did not include these functions in Vertex because at first I wasn’t sure if we would use them – but Vertex is not a complete class yet, so we may add it yet.

2. Modify Window (again)

The Window class is our playground – It’s kind of like our main for OpenGL. So it will be common for us to have to modify it.

First we have to modify the window.h:

We are simply including some more files, and declaring some more variables. We’re also going to do something only once during construction of the Window; so we need a default constructor. It’s not OpenGL-related, so I want to keep it out of initializeGL(). Honestly it doesn’t even belong here, but we’re working our way up to a maintainable framework. Similar train of thought is there for the overriding of the update() SLOT. Updating logic will go here.

And then, we need to change window.cpp:

The first change is going to be to the geometry we’re drawing. Instead of a single triangle, we’re going to draw a cube. This is kind of a pain, because it means a little more thinking to create the geometry than what a single triangle needed. I’ve opted for a simple approach of defining a bunch of Vertex vertices, and placing them to form the triangles we need to form a cube.

So replace the old sg_vertexes code, with this:

My naming convention is Vertex_???, where ??? will be one of {FB}{TB}{LR} for Front/Back, Top/Bottom, Left/Right respectively. Here, the winding of the vertices is important because we don’t want to draw Vertex information that winds clockwise – that would be inefficient.

Next, we need to add our default constructor:

We’re simply starting the object moved back 5 units, nothing major.

While we’re at it, we need to change initializeGL():

Our first change is to connect() the SIGNAL frameSwapped() with the SLOT update(). For fun results, you can try to run the application without this connect, and you’ll notice that the only time paintGL() is called is when you resize the window. By default, OpenGL is used to create rich tools within Qt – not for animation. We need to ask OpenGL to repaint if we want to animate something.

Note: I noticed that this causes the window to lag when dragging/resizing the window. However, this is the intended usage of the frameSwapped() SIGNAL, so I believe this to be a bug for the time being. I will submit it to Qt-team for review.

Many tutorials talk about doing this with QTimer, which is possible. However, it’s more common in modern graphics programming to schedule a redraw immediately after VSync, and by default Qt5 starts up with VSync enabled.

After that, we need to tell OpenGL that we will only draw faces which wind counter-clockwise with glEnable(GL_CULL_FACE). By default, a Front Face is defined as a face which winds counter-clockwise, alternatively the Back Face winds the opposite direction. It is also possible to tell OpenGL to cull only front faces, only back faces, or both. This can be changed if need be, but I’d recommend keeping the default settings unless you know what you’re doing.

The last change is a little more temporary. We are caching the location of the shader uniforms “modelToWorld” and “worldToView” via uniformLocation(). (As mentioned in previous tutorials, this can fail! We will look at some ways of error handling in the future.)

You may have noticed that for attributes, we don’t need to query the location, we simply pass a 0 and a 1 into the shader. This is because within the shader we define the location with layout(location = N)  where N is some integer number. Uniform layout locations were not added until OpenGL 4.3, which we are not targeting – so we cannot use them.

Next up is to finally add some code to our resizeGL() function:

The perspective matrix is our CameraToView matrix, and it is defined as such:

Perspective Matrix

This matrix will take points in CameraSpace, and essentially normalize them based on the camera’s view to fit within the camera’s view of [-1.0, 1.0]. This is why we can call it a Perspective Matrix or a View Matrix – just pick one. The math behind this is a little confusing, and past what this tutorial covers, so we wont really talk about it much here. Luckily, QMatrix4x4 already implements this functionality.

There are going to be a few changes to paintGL():

The calls to setUniformValue() allow us to update the value of worldToView and modelToWorld.

Realistically, we don’t need to update worldToView every frame, but it should be avoided to update any OpenGL state in the resizeGL() function. So for simplicity’s sake, we will update them here.

And finally, we need to update() the scene:

We will rotate the current m_transform. This will allow our cube to rotate by 1.0f degrees every call, about the axis formed by the vector <0.4f, 0.3f, 0.3f>.

3. Update shaders/simple.vert

The last step is to make a few changes to our Vertex Shader:

The only changes here are that we have two uniform matrices, and that we need to transform the original vertex position to view space.

That’s it! Now we have a simple application, with a relatively easy Transform3D interface. Try playing with translating the m_transform every paintGL(), or call grow() to make it grow (or shrink if a negative value is provided) every frame.

Our final application should look like this:

A Rotating 3D Cube! Woah!

A Rotating 3D Cube! Woah!

Summary

In this tutorial, we learned about the following;

  • What the Transformation Pipeline is.
  • How to create a Transform3D class which represents an object’s position, orientation, and size.
  • How to pass uniform information into our shader program.
  • Steps involved in displaying a 3D cube.

View Code on GitHub

Cheers!


Update (2016-08-15)

Bonus: Changing the QOpenGLWindow to a QOpenGLWidget

Hey everyone!

I’m glad that many of my readers are learning the basics of OpenGL and Qt through my tutorials, I’m so happy that others find this information useful! :) One thing you might notice in the comments is that people seem to be having some trouble converting the project to a widget type. So I decided to do a quick once-over the code to see what you might have to change in order to turn this into a Widget of some sort.

Note that I won’t be talking about making a particularly advanced or efficient widget. The point of the QtOpenGL Tutorial Series was to re-use as much of the modern QtOpenGL code as possible, only recreating logic where absolutely necessary. A transformation from QOpenGLWindow to QOpenGLWidget is a fairly straightforward one, and totally supported by the Qt framework without additional classes.

There are really only three important changes:

  1. Instead of including the Qt gui library, we will need to include the Qt widgets library.
  2. Change all references of QOpenGLWindow to QOpenGLWidget (and rename classes appropriately).
  3. Update the main.cpp to construct a QMainWindow, and attach the new Widget as a central widget.

Here is a small overview of those changes in code:
I’ve only highlighted the important bits.

Note: Obviously you must update all references of QOpenGLWindow to QOpenGLWidget in the source file as well!

After making those changes, you should have a perfectly working application with a central widget displaying the spinning cube like in our example!

Hope this helped those of you who were having trouble converting the widgets. If you want to do something a little more low-level (construct from a QWidget) you’ll have a bit more work to do. I haven’t ever done this work, though it should be possible – I would start by looking at how QOpenGLWidget is implemented.

Cheers!


Leave a Reply to Trent Cancel reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.

23 thoughts on “Qt5 OpenGL Part 2: 3D Rendering

  • Florian Dennerlein

    Thanks for this great tutorial. I faced a problem when using QOpenGLWidgets instead of QOpenGLWindows. They should behave the same, but despite calling paintGL regularly, the scene doesn’t show any updates. However, the transformation matrices change with each iteration.

    Do you have an idea, what could be the problem in this scenario?`(Using Microsoft Visual Studio 2010 and Qt5.4 with Qt add in).

    • Trent Post author

      The most probable issue: I believe QOpenGLWidget may rely on a call at the end of your paintGL() function to the base function QOpenGLWidget::paintGL(); For Widgets, I believe this schedules the FBO swap so that it can draw to the actual window.

      I recall running into this issue when switching over to Widgets, which you inevitably must. I really wanted to keep things lightweight and only use the core QOpenGLWindow class, but there is some functionality (eg. Touch Events) which are only available to widgets.

      Give that a shot and let me know if it helps. Also, providing source might help me further debug.

        • Michael Liao

          I got a same issue that nothing came out. Not even glClear is called. I set the background as (0.2f, 0.2f, 0.2f, 1.0f) but only a black screen showed up.

          • Trent Post author

            Dang, I never replied to this ;_; Thought I did, anyways – I hope you figured it out! Otherwise, I’m going to do a dry-run converting the code above from Window to Widget and document any issues I run into and how I fixed them. I kind of do some hand-wavy stuff when it comes to that transition.

          • Trent Post author

            Hey Michael, please see the changes to the article above. At the end of the post I describe the change from QOpenGLWindow to QOpenGLWidget. Perhaps you’re simply changing to a QWidget? That would require more plumbing than we covered in this tutorial.

          • Michael Liao

            I think I have got it, I just have to promote my opengl widget to my custom QOpenGLWidget class~
            Thanks for this detailed tutorial :), just have another question. In OpenGL 4.5 it seems unnecessary to establish a VAO before loading attributes, why is it required in 3.3?

        • Thomas Dullien

          How did you end up fixing it? :) I am currently following the tutorial, and also switching over to Widgets, and running into the exact same issue…

          • Trent Post author

            It’s been a while since I’ve looked at this code – I can maybe take a look at this code, and try to switch it over to a Widget tomorrow – and then document any trouble I have switching the code over.

          • Trent Post author

            Hey Thomas, please see the changes to the article above. At the end of the post I describe the change from QOpenGLWindow to QOpenGLWidget. Perhaps you’re simply changing to a QWidget? That would require more plumbing than we covered in this tutorial.

          • vmsr2

            HI
            I’m trying to convert to QtOpegGL widget and am facing exactly the same problem described above after having followed the tutorial instructions (which are great by the way).
            Could you tell me how you fixed the problem – all I see is a blank screen (the transformation matrices do change with each iteration.)

            Many thanks

    • Trent Post author

      Hey plc66,

      Glad this could be of help! :)
      The idea of integrating with a wonderful UI framework like Qt for general-purpose VR would be amazing, I look forward to seeing where it goes.

      • plc66

        Hello Trent,

        I just realized that the paintGL event I described is totally wrong. I used blindly both sources without understanding too much what I was doing.
        Part of the problem is that the example app given by OSVR is still using fixed function pipeline (openGL2.2), whereas your tutos are based on more recent shaders techniques (openGL3.3).
        OSVR client kit returns a double [16] array for perspective and view matrices, and I need to convert said array to QMatrix4x4 format. Elegantly if possible. Will update and keep you in the loop when I get there.

        Cheers and thanks again,

        plc66

  • Carsten F

    Hi,

    thanks for the tutorial. I was working on a OpenGL project using glew and glfw. But since they are very limited for window changes, i switched to Qt now. But nothing is displayed for me. I tried to run your example code and the object is displayed :D But the animation does not work. So i think my problem is maybe that my frame is never updated.
    If you are still following the comments here, it would be awesome of you if you could contact me in my thread on the Qt board: https://forum.qt.io/topic/74250/opengl-nothing-displayed

    • Trent Post author

      Hey Carsten,

      Animation will only happen if you “connect” the frameSwapped signal to the update slot. What this means is every time a frame is swapped from the backbuffer, the update function will again be called. Note that the function names are case-sensitive. I did a quick Ctrl+F for “connect” in your MainWindow file in your project and didn’t find anything.

      you can tell that is the issue if you attempt to resize the window and during resize events the animations occur.

      Hope that helps!

  • WCK

    Firstly, thank you for your guidance,I made some changes ,I just use the OpenGLWidget (i mean draw it on ui_mainwindow),then promoted it to my own class,so here has a problem. connect can not connect the signal “frameswapped()” and SLOT”update()”, I dont know how to deal this ,but i find a new way, add “update()” into paintGL(),then delete the recursive calls of update().and the cube rotate.cheers!(I’m not good at English,but thank you! then I can keep going ,the next course of yours.)

  • Alberto

    Thanks for this amazing tutorial Trent!
    I was having trouble to add more objects to the scene when activating the “GL_DEPTH_TEST” and that problem finally lies on that the perspective projection matrix must have a “nearClipPlane” and “farClipPlane” values different from 0 and always positive. Otherwise, it does’t works for Z testing. Just to write it down and make others know this little shortcoming.

    I give a link to the gluPerspective documentation for better explanation and reference.
    https://www.khronos.org/registry/OpenGL-Refpages/gl2.1/xhtml/gluPerspective.xml

    Many thanks again.
    Regards.

    • Eric

      DepthTest doesnot work after setting as following:
      //glEnable(GL_CULL_FACE);
      glEnable(GL_DEPTH_TEST);
      m_projection.perspective(45.0f, width / float(height), 0.1f, 100.0f);
      glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

      Where is the matter?

  • JR

    Great Tutorial…after years away from GL, this quickly clears the cobwebs. Regarding the issue with no display from the widget implementation, it appears Qt doesn’t like the widget setting the format. Set QSurfaceFormat::setDefaultFormat(format); prior to creating the widget in main, and it should function.