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.8
f, -
0.8
f, 0.0
f,
0.8
f, -
0.8
f, 0.0
f,
0.0
f, 0.8
f, 0.0
f
}
;
static
const
float
vertexColors[] =
{
1.0
f, 0.0
f, 0.0
f,
0.0
f, 1.0
f, 0.0
f,
0.0
f, 0.0
f, 1.0
f
}
;
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.8
f, -
0.8
f, 0.0
f,
0.8
f, -
0.8
f, 0.0
f,
0.0
f, 0.8
f, 0.0
f
}
;
static
const
float
vertexColors[] =
{
1.0
f, 0.0
f, 0.0
f,
0.0
f, 1.0
f, 0.0
f,
0.0
f, 0.0
f, 1.0
f
}
;
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.