Outils d'utilisateurs

Outils du Site


qt_opengl_-_realiser_un_rendu_offscreen

Qt OpenGL - Réaliser un rendu offscreen

Sommaire

OpenGL dispose d'un mécanisme permettant de rediriger le rendu d'une scène 3D dans un buffer au lieu de l'afficher directement à l'écran. L'intérêt principal est de pouvoir profiter de l'accélération matérielle pour générer une image (ou pour récupérer le résultat d'un calcul effectué sur un processeur graphique sous la forme d'une image).

Nous allons présenter les pBuffer et les Frame Buffer Object (FBO) à travers un exemple simple : nous allons dessiner dans le Fragment Shader des cercles concentriques de différentes couleurs.

Utilisation de QGLPixelBuffer

La classe QGLPixelBuffer permet de manipuler les pBuffer avec Qt.

QGLPixelBuffer m_pbuffer;

L'initialisation prend en paramètre la taille du buffer et le format utilisé. Le format correspond au format d'affichage de contexte OpenGL (il permet notamment l'activation du double buffering). La fonction format permet de récupérer le QGLFormat du contexte openGL courant.

    heightmapWidget::heightmapWidget(QWidget *parent) :
    m_pbuffer(QSize(512, 512), format(), this)
{

Cependant, dans notre cas, nous souhaitons créer un Pixel Buffer Object avec une taille identique au Frame Buffer Object, pour pouvoir comparer la différence de performance. Pour cela, on va créer Pixel Buffer Object sur le tas :

    // QGLPixelBuffer* m_pbuffer;
    m_pbuffer = new QGLPixelBuffer(QSize(512, 512), format(), this)
{

Nous allons maintenant générer une nouvelle texture dans laquelle sera stockée l'image produite. L'appel à la méthode generateDynamicTexture permet de générer une texture OpenGL de même taille que celle du pBuffer. L'identifiant de la texture est récupéré sous forme d'entier non signé dans la variable m_pbuffer_location.

La méthode bindToDynamicTexture permet de lier le contenu du pBuffer à une texture : dès que le pBuffer sera modifié, la texture liée sera mise à jour automatiquement. Malheureusement, cette fonctionnalité n'est pas disponible sur toutes les plateformes, notamment Linux (serveur X11). Cette fonction retourne un boolean indiquant si cette fonctionnalité est prise en charge par le système. Si ce n'est pas le cas, il faudra effectuer manuellement la mise à jour de la texture après modification.

    m_pbuffer_location = m_pbuffer->generateDynamicTexture();
    has_pbuffer =  m_pbuffer->bindToDynamicTexture(m_pbuffer_location);

La génération de la texture sera réalisée dans un Fragment Shader. Nous créons donc un Program Shader chargé d'appliquer ce Fragment Shader sur chaque pixel de notre texture de rendu. Nous passons les dimensions de la texture pour permettre de centrer notre rendu.

    m_program_pbuffer.addShaderFromSourceFile(QGLShader::Fragment, ":/pbuffer.glsl");
    m_program_pbuffer.link();
    m_program_pbuffer.bind();
    m_program_pbuffer.setUniformValue("pbuffer_size", QVector2D(m_pbuffer->width(), m_pbuffer->height()));
    m_program_pbuffer.release();

Le code du shader n'introduit pas de nouveau concept, il produit simplement des cercles concentriques à l'aide la fonction GLSL sin avec une couleur dépendant de la position du pixel par rapport au centre.

// Dans pbuffer.glsl
 
#version 130
 
uniform vec2 pbuffer_size;
out vec4 color;
 
void main(void)
{
    vec2 FragCoord = vec2(gl_FragCoord.x / pbuffer_size.x - 0.5, gl_FragCoord.y / pbuffer_size.y - 0.5);
    float radius = sqrt(FragCoord.x * FragCoord.x + FragCoord.y * FragCoord.y);
    float gray = sin(radius * 200.0);
 
    color = vec4(FragCoord.x * gray, FragCoord.y * gray, 0.0, 1.0);
}

Il ne reste plus qu'à modifier la fonction de rendu paintGL pour ajouter le rendu off-screen avant le code du rendu à l'écran. L'appel à la méthode makeCurrent permet d'utiliser le contexte OpenGL du pBuffer : l'image générée par notre Fragment Shader sera donc stockée dans le pBuffer et non à l'écran.

void HeightmapWidget::paintGL()
{
    m_pbuffer->makeCurrent();
    m_program_pbuffer.bind();

Nous traçons simplement un carré dans lequel sera dessinée la texture. Pour des raisons de simplicité, nous utilisons ici les primitives glVertex. Dans le cas où la liaison dynamique de texture n'est pas supportée, nous recopions manuellement le contenu du Vertex Buffer Object dans la texture avec la méthode updateDynamixTexture.

    glClear(GL_COLOR_BUFFER_BIT);
    glBegin(GL_QUADS);
        glVertex2f(-1.0, -1.0);
        glVertex2f(-1.0, 1.0);
        glVertex2f(1.0, 1.0);
        glVertex2f(1.0, -1.0);
    glEnd();
 
    if (!has_pbuffer)
        m_pbuffer->updateDynamicTexture(m_pbuffer_location);
 
    m_program_pbuffer.release();

On réactive le contexte OpenGL du widget avec makeCurrent. La texture est ensuite liée au contexte à l'aide la fonction OpenGL glBindTexture.

    makeCurrent();
    glBindTexture(GL_TEXTURE_2D, m_pbuffer_location);

Voici le rendu final de notre exemple :

Utilisation de QGLFrameBufferObject

Les Frame Buffer Object permettent également de réaliser du rendu off-screen et de générer des images à l'aide d'OpenGL. De plus, ils présentent un certain nombre d'avantages par rapport aux Pixel Buffer Object, par exemple :

des performances légèrement plus élevées en général, dues à l'absence de changement de contexte OpenGL ; les Frame Buffer Object sont une extension OpenGL pure, ne dépendant pas de bibliothèque spécifique au système utilisé, ce qui les rend plus portables que les Pixel Buffer Object. Les Frame Buffer Object sont gérés dans Qt à l'aide de la classe QGLFrameBufferObject. À la différence des Pixel Buffer Object, il est nécessaire d'avoir un contexte OpenGL actif au moment de l'initialisation. Il n'est donc pas possible de les initialiser dans la liste d'initialisation dans le constructeur :

class HeightmapWidget : public QGLWidget
{
private:
    QGLFramebufferObject m_fbo;
public:
    HeightmapWidget(QWidget *parent) :
        QGLWidget(parent),
        m_fbo(512,512) // erreur : pas de contexte OpenGL valide
    {
    }
};

De plus, l'opérateur d'affectation étant privé, nous ne pouvons pas créer notre objet QGLFrameBufferObject dans la pile :

class HeightmapWidget : public QGLWidget
{
private:
    QGLFramebufferObject m_fbo;
public:
    HeightmapWidget(QWidget *parent) :
        QGLWidget(parent)
    {
        makeCurrent(); // OK : un contexte OpenGL est actif
        m_fbo = QGLFramebufferObject(512, 512); // erreur : l'affectation est impossible
    }
};

Il est donc indispensable de créer notre objet dans le tas sous forme de pointeur. On prendra les précautions nécessaires pour une gestion correcte de la mémoire, en n'oubliant pas d'appeler delete dans le destructeur.

class HeightmapWidget : public QGLWidget
{
private:
    QGLFramebufferObject* m_fbo;
public:
    HeightmapWidget(QWidget *parent) :
        QGLWidget(parent)
    {
        makeCurrent(); // OK : un contexte OpenGL est actif
        m_fbo = new QGLFramebufferObject(512, 512); // OK : on affecte un pointeur
    }
    ~HeightmapWidget()
    {
        if (m_fbo) delete m_fbo;
    }
};

Le Program Shader étant similaire à celui du chapitre précédent (les couleurs sont changées, pour voir la différence avec le Pixel Buffer Object), nous ne détaillerons que le code du rendu. L'utilisation des Frame Buffer Object ne requiert pas de changement de contexte OpenGL : nous appelons directement les méthodes bind et release pour activer le rendu off-screen.

void HeightmapWidget::paintGL()
{
    m_program_fbo.bind();
    m_fbo->bind();
    ...
    m_fbo->release();
    m_program_fbo.release();

La texture générée est ensuite liée au contexte OpenGL pour l'affichage du terrain :

    glBindTexture(GL_TEXTURE_2D, m_fbo>texture());

Le résultat final :

Dessiner dans une texture dynamique avec QPainter

Les classes QGLPixelBuffer et QGLFramebufferObject héritent de la classe QPaintDevice, il est donc possible de dessiner dedans directement avec QPainter.

Pour cela, on crée un QPainter avec le QGLPixelBuffer comme paramètre et on peut l'utiliser dans la foulée. Lorsque l'on a fini d'utiliser QPainter, on réactive le contexte du QGLWidget et on active la texture dynamique avec la fonction glBindTexture :

    QPainter pbuffer_painter(m_pbuffer);
    // On dessine avec le QPainter
    pbuffer_painter.end();
    makeCurrent();
    glBindTexture(GL_TEXTURE_2D, m_pbuffer_location);

Le code est similaire pour le Frame Buffer Object : l'identifiant GLuint de la texture gérée par le Frame Buffer Object est obtenu avec la fonction texture. De plus, comme QPainter modifie le contexte OpenGL courant, il est nécessaire de restaurer les paramètres du contexte après utilisation de QPainter. En revanche, il n'est pas nécessaire d'appeler la fonction makeCurrent puisque Frame Buffer Object travaille avec le même contexte OpenGL que le widget. Dans notre cas, puisque la plupart des paramètres du contexte sont envoyés directement dans le shader, il faut simplement rétablir les dimensions de la vue :

    glViewport(0, 0, width(), height());

Cette technique pourra être utilisée, par exemple, pour afficher un texte défilant sur un panneau lumineux d'une scène représentant une rue ou afficher les images d'une télévision.

qt_opengl_-_realiser_un_rendu_offscreen.txt · Dernière modification: 2014/12/11 17:19 par gbdivers