Using modern OpenGL ES features with QOpenGLFramebufferObject in Qt 5.6

QOpenGLFramebufferObject is a handy OpenGL helper class in Qt 5. It conveniently hides the differences between OpenGL and OpenGL ES, enables easy usage of packed depth-stencil attachments, multisample renderbuffers, and some more exotic formats like RGB10. As a follow up to our previous post about OpenGL ES 3 enhancements in Qt 5.6, we are now going to take a look at an old and a new feature of this class.

Qt 5.6 introduces support for multiple color attachments, to enable techniques requiring multiple render targets. Even though support for this is available since OpenGL 2.0, OpenGL ES was missing it up until version 3.0. Now that mobile and embedded devices with GLES 3.0 support are becoming widely available, it is time for QOpenGLFramebufferObject to follow suit and add some basic support for MRT.

Our other topic is multisample rendering to framebuffers. This has been available for some time in Qt 5, but was usually limited to desktop platforms as OpenGL ES does not have this as a core feature before version 3.0. We will now go through the basic usage and look at some common pitfalls.

Multisample framebuffers

By default our framebuffer will have a texture as its sole color attachment. QOpenGLFramebufferObject takes care of creating this texture, applications can then query it via textures() or can even take ownership of and detaching it from the FBO by calling takeTexture().

What if multisampling is desired? Requesting it is simple by using the appropriate constructor, typically like this:


    QOpenGLFramebufferObjectFormat format;
    format.setAttachment(QOpenGLFramebufferObject::CombinedDepthStencil);
    format.setSamples(4);
    fbo = new QOpenGLFramebufferObject(width, height, format);

Here 4 samples per pixel are requested, together with depth and stencil attachments, as the default is to have color only. The rest of the defaults in QOpenGLFramebufferObjectFormat matches the needs of most applications so there is rarely a need to change them.

Now, the obvious question:

What if support is not available?

When support for multisample framebuffers is not available, either because the application is running on an OpenGL ES 2.0 system or the necessary extensions are missing, QOpenGLFramebufferObject falls silently back to the non-multisample path. To check the actual number of samples, query the QOpenGLFramebufferObjectFormat via format() and check samples(). Once the framebuffer is created, the returned format contains the actual, not the requested, number of samples. This will typically be 0, 4, 8, etc. and may not match the requested value even when multisampling is supported since the requests are often rounded up to the next supported value.


    ...
    fbo = new QOpenGLFramebufferObject(width, height, format);
    if (fbo->isValid()) {
        if (fbo->format().samples() > 0) {
            // we got a framebuffer backed by a multisample renderbuffer
        } else {
            // we got a non-multisample framebuffer, backed by a texture
        }
    }

What happens under the hood?

QOpenGLFramebufferObject's multisample support is based on GL_EXT_framebuffer_multisample and GL_EXT_framebuffer_blit. The good thing here is that glRenderbufferStorageMultisample and glBlitFramebuffer are available in both OpenGL 3.0 and OpenGL ES 3.0, meaning that relying on multisample framebuffers is now feasible in cross-platform applications and code bases targeting desktop, mobile, or even embedded systems.

As usual, had Qt not cared for older OpenGL and OpenGL ES versions, there would have been some other options available, like using multisample textures via GL_ARB_texture_multisample or GL_ARB_texture_storage_multisample instead of multisample renderbuffers. This would then require OpenGL 3.2 (OpenGL 4.3 or OpenGL ES 3.1 in case of the latter). It has some benefits, like avoiding the explicit blit call for resolving the samples. It may be added as an option to QOpenGLFramebufferObject in some future Qt release, let's see.

A note for iOS: older iOS devices, supporting OpenGL ES 2.0 only, have multisample framebuffer support via GL_APPLE_framebuffer_multisample. However, API-wise this extension is not fully compatible with the combination of EXT_framebuffer_multisample+blit, so adding support for it is less appealing. Now that newer devices come with OpenGL ES 3.0 support, it is less relevant since the standard approach will work just fine.

So what can I do with that renderbuffer?

In most cases applications will either want a texture for the FBO's color attachment or do a blit to some other framebuffer (or the default framebuffer associated with the current surface). In the multisample case there is no texture, so calling texture() or similar is futile. To get a texture we will need a non-multisample framebuffer to which the multisample one's contents is blitted via glBlitFramebuffer.

Fortunately QOpenGLFramebufferObject provides some helpers for this too:


    static bool hasOpenGLFramebufferBlit();
    static void blitFramebuffer(QOpenGLFramebufferObject *target, const QRect &targetRect,
                                QOpenGLFramebufferObject *source, const QRect &sourceRect,
                                GLbitfield buffers,
                                GLenum filter,
                                int readColorAttachmentIndex,
                                int drawColorAttachmentIndex);

Combined with some helpful overloads, this makes resolving the samples pretty easy:


    ...
    GLuint texture;
    QScopedPointer tmp;
    if (fbo->format().samples() > 0) {
        tmp.reset(new QOpenGLFramebufferObject(fbo->size()));
        QOpenGLFramebufferObject::blitFramebuffer(tmp.data(), fbo);
        texture = tmp->texture();
    } else {
        texture = fbo->texture();
    }
    ...

Checking for the availability of glBlitFramebuffer via hasOpenGLFramebufferBlit() is not really necessary because the multisample extension requires the blit one, and, on top of that, QOpenGLFramebufferObject checks for the presence of both and will never take the multisample path without either of them. Therefore checking format().samples(), as shown above, is sufficient. It is worth noting that creating a new FBO (like tmp above) over and over again should be avoided in production code. Instead, create it once, together with the multisample one, and reuse it. Note also that having depth or stencil attachments for tmp is not necessary.

And now on to the shiny new stuff.

Multiple render targets

In Qt 5.6 QOpenGLFramebufferObject is no longer limited to a single texture or renderbuffer attached to the GL_COLOR_ATTACHMENT0 attachment point. The default behavior does not change, constructing an instance will behave like in earlier versions, with a single texture or renderbuffer. However, we now have a new function called addColorAttachment():


void addColorAttachment(const QSize &size, GLenum internalFormat = 0);

Its usage is pretty intuitive: as the name suggests, it will create a new texture or renderbuffer and attach it to the next color attachment point. For example, to create a framebuffer object with 3 color attachments in addition to depth and stencil:


fbo = new QOpenGLFramebufferObject(size, QOpenGLFramebufferObject::CombinedDepthStencil);
fbo->addColorAttachment(size);
fbo->addColorAttachment(size);

The sizes and internal formats can differ. The default value of 0 for internalFormat leads to choosing a commonly suitable default (like GL_RGBA8). As for the sizes, the main rule is to keep in mind that rendering will get limited to the area that fits all attachments.

After this we will have a color attachment at GL_COLOR_ATTACHMENT0, 1 and 2. To specify which ones are written to from the fragment shader, use glDrawBuffers. As introduced in the previous post, QOpenGLExtraFunctions is of great help for invoking OpenGL ES 3.x functions:


QOpenGLExtraFunctions *f = QOpenGLContext::currentContext()->extraFunctions();
GLenum bufs[] = { GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1, GL_COLOR_ATTACHMENT2 };
f->glDrawBuffers(3, bufs);

We are all set. All we need now is a fragment shader writing to the different attachments:


...
layout(location = 0) out vec4 diffuse;
layout(location = 1) out vec4 position;
layout(location = 2) out vec4 normals;
...

The rest is up to the applications. QOpenGLFramebufferObject no longer prevents them from doing deferred shading and lighting, or other MRT-based techniques.

To check at runtime if multiple render targets are supported, call hasOpenGLFeature() with MultipleRenderTargets. If the return value is false, calling addColorAttachment() is futile.

Finally, now that there can be more than one texture, some of the existing QOpenGLFramebufferObject functions get either a new overload or a slightly differently named alternative. They need no further explanation, I believe:


QVector textures() const;
GLuint takeTexture(int colorAttachmentIndex);
QImage toImage(bool flipped, int colorAttachmentIndex) const;

That's all for now, happy hacking!


Blog Topics:

Comments