Using FBOs instead of pBuffers in Qt 5

In Qt 4 lifetime it was possible to accelerate QPainter drawing with OpenGL by using the QGLPixelBuffer class: it offered a nice and quick way of creating a drawable surface, rendering to it (using ordinary QPainter methods) and grabbing the final result as a QImage.

In Qt 5 QGLPixelBuffer is still there, but it has been deprecated in favour of Framebuffer Objects, wrapped in Qt by the QOpenGLFramebufferObject class. However, QOpenGLFramebufferObject is not a QPaintDevice, therefore we can’t use a QPainter directly on it.

Enter QOpenGLPaintDevice

QOpenGLPaintDevice is the glue we’re looking for. Creating a QOpenGLPaintDevice on an active OpenGL context allows us to paint using QPainter on it. Let’s have a look at some code.

#include <QtCore>
#include <QtGui>
#include <QtWidgets>

QImage createImageWithFBO()
{
    QSurfaceFormat format;
    format.setMajorVersion(3);
    format.setMinorVersion(3);

    QWindow window;
    window.setSurfaceType(QWindow::OpenGLSurface);
    window.setFormat(format);
    window.create();

    QOpenGLContext context;
    context.setFormat(format);
    if (!context.create())
        qFatal("Cannot create the requested OpenGL context!");
    context.makeCurrent(&window);

    const QRect drawRect(0, 0, 400, 400);
    const QSize drawRectSize = drawRect.size();

    QOpenGLFramebufferObjectFormat fboFormat;
    fboFormat.setSamples(16);
    fboFormat.setAttachment(QOpenGLFramebufferObject::CombinedDepthStencil);

    QOpenGLFramebufferObject fbo(drawRectSize, fboFormat);
    fbo.bind();

    QOpenGLPaintDevice device(drawRectSize);
    QPainter painter;
    painter.begin(&device);
    painter.setRenderHints(QPainter::Antialiasing | QPainter::HighQualityAntialiasing);

    painter.fillRect(drawRect, Qt::blue);

    painter.drawTiledPixmap(drawRect, QPixmap(":/qt-project.org/qmessagebox/images/qtlogo-64.png"));

    painter.setPen(QPen(Qt::green, 5));
    painter.setBrush(Qt::red);
    painter.drawEllipse(0, 100, 400, 200);
    painter.drawEllipse(100, 0, 200, 400);

    painter.setPen(QPen(Qt::white, 0));
    QFont font;
    font.setPointSize(24);
    painter.setFont(font);
    painter.drawText(drawRect, "Hello FBO", QTextOption(Qt::AlignCenter));

    painter.end();

    fbo.release();
    return fbo.toImage();
}

int main(int argc, char **argv)
{
    QApplication app(argc, argv);

    QImage targetImage = createImageWithFBO();

    QLabel label;
    label.setPixmap(QPixmap::fromImage(targetImage));
    label.show();
    return app.exec();
}

Analysis

This small piece of code creates a QImage by painting on a temporary Frame Buffer Object using QPainter, then displays the resulting image using a QLabel. Let’s analyze what the createImageWithFBO method does.

    QSurfaceFormat format;
    format.setMajorVersion(3);
    format.setMinorVersion(3);

    QWindow window;
    window.setSurfaceType(QWindow::OpenGLSurface);
    window.setFormat(format);
    window.create();

    QOpenGLContext context;
    context.setFormat(format);
    if (!context.create())
        qFatal("Cannot create the requested OpenGL context!");
    context.makeCurrent(&window);

First of all, we create a QWindow (of type OpenGL) and a QOpenGLContext, and make the context current on that window. Both of them will try to use a OpenGL 3.3 (note that f.i. Mac OS X as of now supports up to 3.2).

Using QWindow as the context’s target surface is a workaround because, as of Qt 5.0, there are no other QSurface types. Since we need a valid surface to make a context current, we just create an invisible window (we won’t show it) without any specific size, and that is sufficient to be able to use an OpenGL context on it.

    const QRect drawRect(0, 0, 400, 400);
    const QSize drawRectSize = drawRect.size();

These variables will hold our target rectangle/size to draw on.

    QOpenGLFramebufferObjectFormat fboFormat;
    fboFormat.setSamples(16);
    fboFormat.setAttachment(QOpenGLFramebufferObject::CombinedDepthStencil);

    QOpenGLFramebufferObject fbo(drawRectSize, fboFormat);
    fbo.bind();

Here we’re creating a QOpenGLFramebufferObject with the target size. Before doing that, we also specify its format: we want multisampling to be active on the FBO, so we get antialiased primitives, so we set 16 samples per pixel; moreover, we ask Qt to attach a depth and a stencil buffers to the frame buffer object. We’re not going to use any of them explicitely; but the Qt paint engine might (actually, there are glStencil calls in the paint engine), so we want the FBO to have them.

Last, we bind the QOpenGLFramebufferObject object. This will redirect all painting operations on it.

    QOpenGLPaintDevice device(drawRectSize);
    QPainter painter;
    painter.begin(&device);
    painter.setRenderHints(QPainter::Antialiasing | QPainter::HighQualityAntialiasing);

    painter.fillRect(drawRect, Qt::blue);

    painter.drawTiledPixmap(drawRect, QPixmap(":/qt-project.org/qmessagebox/images/qtlogo-64.png"));

    painter.setPen(QPen(Qt::green, 5));
    painter.setBrush(Qt::red);
    painter.drawEllipse(0, 100, 400, 200);
    painter.drawEllipse(100, 0, 200, 400);

    painter.setPen(QPen(Qt::white, 0));
    QFont font;
    font.setPointSize(24);
    painter.setFont(font);
    painter.drawText(drawRect, "Hello FBO", QTextOption(Qt::AlignCenter));

    painter.end();

We then proceed to create a QOpenGLPaintDevice and painting on it using the ordinary QPainter calls. Note that we explicitely have to end() the painting, so we know that all painting commands have been flushed (and done) after that call, and our FBO holds the results.

    fbo.release();
    return fbo.toImage();

And these lines complete the magic: we unbind the FBO, returning to the default framebuffer, and grab its contents as a QImage using the toImage() method.

The result is:
Hello FBO

Injecting raw OpenGL calls

We can build on top this simple example and also perform pure OpenGL drawing while painting with QPainter. It suffices to add the following lines in the above snippet, while the painter is active:

    painter.beginNativePainting();
    nativePainting();
    painter.endNativePainting();

The pair of begin/endNativePainting calls are needed to reset the OpenGL state to a “clean” one before we perform our own painting. The nativePainting() function is:

void nativePainting()
{
    static const float vertexPositions[] = {
        -0.8f, -0.8f, 0.0f,
         0.8f, -0.8f, 0.0f,
         0.0f,  0.8f, 0.0f
    };

    static const float vertexColors[] = {
        1.0f, 0.0f, 0.0f,
        0.0f, 1.0f, 0.0f,
        0.0f, 0.0f, 1.0f
    };

    QOpenGLBuffer vertexPositionBuffer(QOpenGLBuffer::VertexBuffer);
    vertexPositionBuffer.create();
    vertexPositionBuffer.setUsagePattern(QOpenGLBuffer::StaticDraw);
    vertexPositionBuffer.bind();
    vertexPositionBuffer.allocate(vertexPositions, 9 * sizeof(float));

    QOpenGLBuffer vertexColorBuffer(QOpenGLBuffer::VertexBuffer);
    vertexColorBuffer.create();
    vertexColorBuffer.setUsagePattern(QOpenGLBuffer::StaticDraw);
    vertexColorBuffer.bind();
    vertexColorBuffer.allocate(vertexColors, 9 * sizeof(float));

    QOpenGLShaderProgram program;
    program.addShaderFromSourceCode(QOpenGLShader::Vertex,
                                    "#version 330\n"
                                    "in vec3 position;\n"
                                    "in vec3 color;\n"
                                    "out vec3 fragColor;\n"
                                    "void main() {\n"
                                    "    fragColor = color;\n"
                                    "    gl_Position = vec4(position, 1.0);\n"
                                    "}\n"
                                    );
    program.addShaderFromSourceCode(QOpenGLShader::Fragment,
                                    "#version 330\n"
                                    "in vec3 fragColor;\n"
                                    "out vec4 color;\n"
                                    "void main() {\n"
                                    "    color = vec4(fragColor, 1.0);\n"
                                    "}\n"
                                    );
    program.link();
    program.bind();

    vertexPositionBuffer.bind();
    program.enableAttributeArray("position");
    program.setAttributeBuffer("position", GL_FLOAT, 0, 3);

    vertexColorBuffer.bind();
    program.enableAttributeArray("color");
    program.setAttributeBuffer("color", GL_FLOAT, 0, 3);

    glDrawArrays(GL_TRIANGLES, 0, 3);
}

Which is a very verbose (but modern) way to draw a three-coloured triangle in the middle of our image. I won’t analyze the snippet in detail, as it’s quite readable: in the first part we’re creating a couple of vertex buffer objects, one to hold the vertex positions (already in clip space) and another to hold the vertex colours (in RGB).

We then create the required vertex and fragment shaders, which are trivial — the vertex shader just outputs the (normalized) position in input, and passes the colour to the fragment shader; the fragment shader simply outputs the fragment colour it got as input.

The program is then linked, and the buffers created at the beginning are then set as attribute buffers.

We’re then ready to draw our triangle, with a simple glDrawArrays call. The final result is:

Hello FBO with OpenGL

And the complete snippet:

#include <QtCore>
#include <QtGui>
#include <QtWidgets>

void nativePainting()
{
    static const float vertexPositions[] = {
        -0.8f, -0.8f, 0.0f,
         0.8f, -0.8f, 0.0f,
         0.0f,  0.8f, 0.0f
    };

    static const float vertexColors[] = {
        1.0f, 0.0f, 0.0f,
        0.0f, 1.0f, 0.0f,
        0.0f, 0.0f, 1.0f
    };

    QOpenGLBuffer vertexPositionBuffer(QOpenGLBuffer::VertexBuffer);
    vertexPositionBuffer.create();
    vertexPositionBuffer.setUsagePattern(QOpenGLBuffer::StaticDraw);
    vertexPositionBuffer.bind();
    vertexPositionBuffer.allocate(vertexPositions, 9 * sizeof(float));

    QOpenGLBuffer vertexColorBuffer(QOpenGLBuffer::VertexBuffer);
    vertexColorBuffer.create();
    vertexColorBuffer.setUsagePattern(QOpenGLBuffer::StaticDraw);
    vertexColorBuffer.bind();
    vertexColorBuffer.allocate(vertexColors, 9 * sizeof(float));

    QOpenGLShaderProgram program;
    program.addShaderFromSourceCode(QOpenGLShader::Vertex,
                                    "#version 330\n"
                                    "in vec3 position;\n"
                                    "in vec3 color;\n"
                                    "out vec3 fragColor;\n"
                                    "void main() {\n"
                                    "    fragColor = color;\n"
                                    "    gl_Position = vec4(position, 1.0);\n"
                                    "}\n"
                                    );
    program.addShaderFromSourceCode(QOpenGLShader::Fragment,
                                    "#version 330\n"
                                    "in vec3 fragColor;\n"
                                    "out vec4 color;\n"
                                    "void main() {\n"
                                    "    color = vec4(fragColor, 1.0);\n"
                                    "}\n"
                                    );
    program.link();
    program.bind();

    vertexPositionBuffer.bind();
    program.enableAttributeArray("position");
    program.setAttributeBuffer("position", GL_FLOAT, 0, 3);

    vertexColorBuffer.bind();
    program.enableAttributeArray("color");
    program.setAttributeBuffer("color", GL_FLOAT, 0, 3);

    glDrawArrays(GL_TRIANGLES, 0, 3);
}

QImage createImageWithFBO()
{
    QSurfaceFormat format;
    format.setMajorVersion(3);
    format.setMinorVersion(3);

    QWindow window;
    window.setSurfaceType(QWindow::OpenGLSurface);
    window.setFormat(format);
    window.create();

    QOpenGLContext context;
    context.setFormat(format);
    if (!context.create())
        qFatal("Cannot create the requested OpenGL context!");
    context.makeCurrent(&window);

    const QRect drawRect(0, 0, 400, 400);
    const QSize drawRectSize = drawRect.size();

    QOpenGLFramebufferObjectFormat fboFormat;
    fboFormat.setSamples(16);
    fboFormat.setAttachment(QOpenGLFramebufferObject::CombinedDepthStencil);

    QOpenGLFramebufferObject fbo(drawRectSize, fboFormat);
    fbo.bind();

    QOpenGLPaintDevice device(drawRectSize);
    QPainter painter;
    painter.begin(&device);
    painter.setRenderHints(QPainter::Antialiasing | QPainter::HighQualityAntialiasing);

    painter.fillRect(drawRect, Qt::blue);

    painter.drawTiledPixmap(drawRect, QPixmap(":/qt-project.org/qmessagebox/images/qtlogo-64.png"));

    painter.setPen(QPen(Qt::green, 5));
    painter.setBrush(Qt::red);
    painter.drawEllipse(0, 100, 400, 200);
    painter.drawEllipse(100, 0, 200, 400);

    painter.beginNativePainting();
    nativePainting();
    painter.endNativePainting();

    painter.setPen(QPen(Qt::white, 0));
    QFont font;
    font.setPointSize(24);
    painter.setFont(font);
    painter.drawText(drawRect, "Hello FBO", QTextOption(Qt::AlignCenter));

    painter.end();

    fbo.release();
    return fbo.toImage();
}

int main(int argc, char **argv)
{
    QApplication app(argc, argv);

    QImage targetImage = createImageWithFBO();

    QLabel label;
    label.setPixmap(QPixmap::fromImage(targetImage));
    label.show();
    return app.exec();
}

Dynamic textures

With some little more efforts we can also duplicate the dynamic texture feature of QGLPixelBuffer: QOpenGLFramebufferObject offers the texture() method, which returns the ID of the texture bound to the color buffer. This means that we can draw using QPainter and texture arbitrary objects in our OpenGL scene using the results. But that would require a full blog post on its own…

Happy hacking!

KDAB

The work for this blog post has been kindly sponsored by KDAB, the Qt experts.

Advertisements