Remplacer les pbuffers par des frame buffer objects dans Qt 5

Giuseppe D'Angelo

Traducteur

Alexandre Laurent

L'utilisation des frame buffer objects peut accélérer le rendu de QPainter. Toutefois, Qt 5 ne le permet pas directement. Giuseppe D'angelo nous explique comment utiliser QPainter pour dessiner sur un frame buffer object.

Commentez

- Titre : Remplacer les pbuffers par des frame buffer objects dans Qt 5

- Auteur : Giuseppe D'Angelo

- Traducteur : Alexandre Laurent

- Parution : 21 février 2013

- Mise à jour : 22 février 2013

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2013 Giuseppe D'Angelo. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Utilisez les frame buffer objects au lieu des pbuffers dans Qt 5

- II. Dans QOpenGLPaintDevice

- III. Analyse

- IV. Injection des appels bruts OpenGL

- V. Textures dynamiques

- VI. Remerciements

I - Utilisez les frame buffer objects au lieu des pbuffers dans Qt 5

Durant le cycle de vie de Qt 4, il était possible d'accélérer le rendu de QPainter avec OpenGL en utilisant la classe QGLPixelBuffer : elle offrait une méthode simple et rapide pour créer des surfaces utilisables pour le dessin, dessiner sur celles-ci (avec les méthodes de QPainter) et récupérer le résultat final dans une QImage.

Dans Qt 5, QGLPixelBuffer est toujours d'actualité, mais la classe a été dépréciée en faveur des Frame Buffer Objects (FBO), implémentés dans Qt avec la classe QOpenGLFramebufferObject. Toutefois, QOpenGLFramebufferObject n'est pas un QPaintDevice et vous ne pouvez donc plus directement utiliser QPainter dessus.

II - Dans QOpenGLPaintDevice

QOpenGLPaintDevice est le raccord que nous cherchons. La création d'un QOpenGLPaintDevice au-dessus d'un contexte OpenGL permet d'y dessiner avec QPainter. Regardons le 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();
}

III - Analyse

Ce morceau de code crée une QImage en dessinant sur un FBO temporaire avec QPainter, puis affiche l'image avec un QLabel. Analysons ce que createImageWithFBO fait.

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);

Tout d'abord, nous créons une QWindow (de type OpenGL) et un QOpenGLContext et le définissons comme contexte courant pour cette fenêtre. Chacun d'eux essayera d'utiliser OpenGL 3.3 (notez que maintenant, Mac OS X possède le support d'OpenGL 3.2).

L'utilisation de la QWindow comme surface cible du contexte est une solution de contournement, car dans Qt 5.0, il n'y a pas d'autre type de surface. Comme il faut une surface valide pour rendre le contexte courant, nous créons une fenêtre invisible (nous ne la montrons pas) sans taille spécifique et c'est assez pour utiliser un contexte OpenGL sur celle-ci.

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

Ces variables contiennent la taille du rectangle cible de dessin.

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

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

Ici, nous créons un QopenGLFramebufferObject avec la taille cible. Avant de le faire, nous spécifions son format : nous voulons activer le multiéchantillonage sur le FBO pour avoir des primitives antialiasées, nous définissons donc seize échantillons par pixel. De plus, nous demandons à Qt d'attacher un tampon de profondeur et un stencil buffer au FBO. Nous n'allons pas les utiliser explicitement, mais le moteur de rendu de Qt le fera (en fait, le moteur de rendu appelle glStencil), nous souhaitons donc que notre FBO les ait.

Finalement, nous lions le QOpenGLFramebufferObject. Cela permettra de rediriger toutes les opérations de dessin sur ce dernier.

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();

Ensuite, nous créons un QOpenGLPaintDevice et dessinons sur celui-ci avec les classiques appels de QPainter. Notez que nous devons arrêter explicitement le rendu, avec la fonction end() afin de s'assurer que toutes les commandes de rendu ont été effectuées après cet appel et que notre FBO contient bien le résultat.

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

Ces lignes concluent le tout : nous délions le FBO, retournant le tampon d'image par défaut et récupérons son contenu dans une QImage avec la méthode toImage().

Le résultat est le suivant :

IV - Injection des appels bruts OpenGL

Nous pouvons construire ajouter une autre couche à cet exemple simple et effectuer des opérations de dessin purement OpenGL tout en dessinant avec QPainter. Il suffit d'ajouter les lignes suivantes au code précédent, lorsque le painter est actif :

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

La paire de fonctions begin/endNativePainting est nécessaire pour remettre l'état d'OpenGL à un état « propre » avant d'effectuer notre rendu. La fonction nativePainting() est la suivante :

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);
}

C'est une méthode très verbeuse (mais moderne) pour dessiner un triangle tricolore au centre de notre image. Je n'analyserai pas le code en détail, il est facilement compréhensible : dans la première partie, nous créons un couple de vertex buffer objects, un pour contenir les positions des vertex (directement dans l'espace de clip) et le second pour contenir les couleurs des vertex (en RVB).

Nous créons le vertex shader et le fragment shader nécessaire, ce qui est trivial. Le vertex shader retourne au fragment shader la position (normalisée) passée en entrée ainsi que la couleur. Le fragment shader retourne simplement la couleur du fragment qu'il reçoit en entrée.

Ensuite, le programme est lié et les tampons sont créés au début puis définis comme tampons attributs du shader.

Nous sommes prêt pour dessiner notre triangle à l'aide d'un appel à glDrawArrays(). Le résultat est le suivant :

Voici le code complet :

#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();
}

V - Textures dynamiques

Avec quelques efforts supplémentaires nous pouvons aussi reproduire la fonctionnalité de textures dynamiques de QGLPixelBuffer : QOpenGLFramebufferObject offre une méthode texture(), qui retourne l'identifiant de la texture attachée au tampon de couleur. Cela signifie que nous pouvons utiliser QPainter et la texture arbitrairement dans notre scène OpenGL avec les résultats, mais cela nécessiterait un billet de blog complet juste pour cette fonctionnalité…

Joyeux bidouillage !

VI - Remerciements

Le travail de cet article a été sponsorisé par KDAB, les experts Qt.

Merci à Giuseppe D'Angelo de nous avoir autorisés à traduire cet article de son blog.

Merci dourouc05 pour ses corrections et ClaudeLELOUP pour sa relecture orthographique.