Exemple de widget « Cube RHI »

Ce exemple montre comment afficher un cube texturé et l'intégrer à QPainter et à des widgets, à l'aide d'QRhi, l'API Qt 3D et la couche d'abstraction du langage de shaders.

Application présentant des superpositions de rendu complexes

Capture d'écran de l'exemple de widget Cube RHI

Cet exemple s'appuie sur l'exemple de widget RHI simple. Alors que l'exemple simple est volontairement minimaliste et aussi compact que possible, ne rendant qu'un seul triangle sans widget supplémentaire dans la fenêtre, cette application illustre :

  • La présence de divers widgets dans la fenêtre, dont certains contrôlent des données utilisées par la sous-classe QRhiWidget.
  • Au lieu de demander des mises à jour en continu, l’ QRhiWidget ne met ici à jour le contenu de sa texture de fond que lorsque certaines données associées changent.
  • Le cube est texturé à l’aide d’un « QRhiTexture » dont le contenu provient d’un « QImage » contenant un rendu logiciel effectué à l’aide de « QPainter ».
  • Le contenu de l’ QRhiWidget can be read back est enregistré dans un fichier image (par exemple, un fichier PNG).
  • can be toggled avec anticrénelage multi-échantillonnage 4x à l’exécution. La sous-classe QRhiWidget est conçue pour gérer correctement le nombre d’échantillons variable.
  • L'explicitly specified backing texture size peut être activée ou désactivée de manière dynamique et contrôlée à l'aide d'un curseur allant de 16x16 à 512x512 pixels.
  • La sous-classe QRhiWidget gère correctement les variations de l’ QRhi. Ce comportement est visible lorsque l’on place le widget au niveau supérieur (sans parent ; il devient une fenêtre distincte), puis qu’on le réintègre dans la hiérarchie des enfants de la fenêtre principale.
  • Plus important encore, certains widgets, même semi-transparents, peuvent être placés par-dessus l’ QRhiWidget, ce qui prouve qu’un empilement et un mélange corrects sont possibles. C’est un cas où l’ QRhiWidget est supérieure à l’intégration d’une fenêtre native, c’est-à-dire une QWindow basée sur QRhi utilisant QWidget::createWindowContainer(), car elle permet l’empilement et le découpage de la même manière que n’importe quel élément QWidget ordinaire rendu par logiciel, alors que l’intégration d’une fenêtre native peut, selon la plateforme, présenter diverses limitations ; par exemple, il est souvent difficile ou inefficace d’y superposer des contrôles supplémentaires.

Dans la réimplémentation de initialize(), la première chose à faire est de vérifier si l’ QRhi avec laquelle nous avons travaillé en dernier est toujours à jour, et si le nombre d’échantillons (pour l’anticrénelage multi-échantillons) a changé. Le premier point est important car toutes les ressources graphiques doivent être libérées lorsque l’ QRhi change, tandis qu’avec un nombre d’échantillons évoluant dynamiquement, un problème similaire se pose spécifiquement pour les objets de type « QRhiGraphicsPipeline », car ceux-ci intègrent le nombre d’échantillons de manière fixe. Par souci de simplicité, l’application gère tous ces changements de la même manière, en réinitialisant sa structure ` scene ` à une structure construite par défaut, ce qui a pour effet pratique de libérer toutes les ressources graphiques. Toutes les ressources sont ensuite recréées.

Lorsque la taille de la texture de support (et donc la taille de la cible de rendu) change, aucune action particulière n’est nécessaire, mais un signal est émis par commodité, simplement pour que la fonction main() puisse repositionner l’étiquette de superposition. Le nom de l’API 3D est également exposé via un signal en interrogeant la fonction QRhi::backendName() chaque fois que la fonction QRhi change.

L’implémentation doit tenir compte du fait que l’anticrénelage multi-échantillonnage implique que ` colorTexture()` renvoie ` nullptr`, tandis que ` msaaColorBuffer()` est valide. C’est l’inverse de ce qui se passe lorsque le MSAA n’est pas utilisé. La raison pour laquelle on fait la distinction et utilise des types différents (QRhiTexture, QRhiRenderBuffer) est de permettre l’utilisation du MSAA avec des API graphiques 3D qui ne prennent pas en charge les textures multi-échantillons, mais qui prennent en charge les tampons de rendu multi-échantillons. OpenGL ES 3.0 en est un exemple.

Pour vérifier la taille actuelle des pixels et le nombre d’échantillons, une solution pratique et concise consiste à interroger l’ QRhiRenderTarget, car cela évite d’avoir à vérifier si c’est colorTexture() ou msaaColorBuffer() qui est valide.

void ExampleRhiWidget::initialize(QRhiCommandBuffer *)
{
    if (m_rhi != rhi()) {
        m_rhi = rhi();
        scene = {};
        emit rhiChanged(QString::fromUtf8(m_rhi->backendName()));
    }
    if (m_pixelSize != renderTarget()->pixelSize()) {
        m_pixelSize = renderTarget()->pixelSize();
        emit resized();
    }
    if (m_sampleCount != renderTarget()->sampleCount()) {
        m_sampleCount = renderTarget()->sampleCount();
        scene = {};
    }

Le reste est assez intuitif. Les tampons et les pipelines sont (re)créés si nécessaire. Le contenu de la texture utilisée pour texturer le maillage du cube est mis à jour. La scène est rendue à l’aide d’une projection en perspective. La vue se limite pour l’instant à une simple translation.

    if (!scene.vbuf) {
        initScene();
        updateCubeTexture();
    }

    scene.mvp = m_rhi->clipSpaceCorrMatrix();
    scene.mvp.perspective(45.0f, m_pixelSize.width() / (float) m_pixelSize.height(), 0.01f, 1000.0f);
    scene.mvp.translate(0, 0, -4);
    updateMvp();
}

La fonction qui effectue la mise en file d’attente effective de l’écriture dans le tampon uniforme prend également en compte la rotation fournie par l’utilisateur, générant ainsi la matrice finale de projection modèle-vue.

void ExampleRhiWidget::updateMvp()
{
    QMatrix4x4 mvp = scene.mvp * QMatrix4x4(QQuaternion::fromEulerAngles(QVector3D(30, itemData.cubeRotation, 0)).toRotationMatrix());
    if (!scene.resourceUpdates)
        scene.resourceUpdates = m_rhi->nextResourceUpdateBatch();
    scene.resourceUpdates->updateDynamicBuffer(scene.ubuf.get(), 0, 64, mvp.constData());
}

La mise à jour de l’ QRhiTexture, qui est échantillonné dans le shader de fragment lors du rendu du cube, est assez simple, même si de nombreux processus s’y déroulent : tout d’abord, un dessin basé sur une fonction « QPainter » est généré au sein d’un « QImage ». Celui-ci utilise le texte fourni par l’utilisateur. Ensuite, les données de pixels côté CPU sont chargées dans une texture (plus précisément, l’opération de chargement est enregistrée dans une QRhiResourceUpdateBatch, qui est ensuite soumise plus tard dans la fonction render()).

void ExampleRhiWidget::updateCubeTexture()
{
    QImage image(CUBE_TEX_SIZE, QImage::Format_RGBA8888);
    const QRect r(QPoint(0, 0), CUBE_TEX_SIZE);
    QPainter p(&image);
    p.fillRect(r, QGradient::DeepBlue);
    QFont font;
    font.setPointSize(24);
    p.setFont(font);
    p.drawText(r, itemData.cubeText);
    p.end();

    if (!scene.resourceUpdates)
        scene.resourceUpdates = m_rhi->nextResourceUpdateBatch();
    scene.resourceUpdates->uploadTexture(scene.cubeTex.get(), image);
}

L’initialisation des ressources graphiques est simple. Il n’y a qu’un tampon de sommets, pas de tampon d’index, et un tampon uniforme ne contenant qu’une matrice 4x4 (16 flottants).

La texture contenant le dessin généré par l’ QPainter a une taille de 512x512. Notez que toutes les tailles (tailles de texture, fenêtres d’affichage, zones de découpe, zones de chargement de texture, etc.) sont toujours exprimées en pixels lorsque l’on travaille avec QRhi. Pour échantillonner cette texture dans le shader, un échantillonneur de texture ( sampler object ) est nécessaire (même si les applications basées sur QRhi utilisent généralement des échantillonneurs d’images combinés dans le code du shader GLSL, qui peuvent ensuite être transpilés en objets de texture et d’échantillonneur distincts avec certains langages de shading, ou rester un objet combiné texture-échantillonneur avec d’autres ; cela signifie qu’il se peut qu’il n’y ait en réalité pas d’objet échantillonneur natif en arrière-plan au moment de l’exécution, selon l’API 3D, mais tout cela est transparent pour l’application)

Le shader de sommets lit à partir du tampon uniforme au point de liaison 0 ; par conséquent, ` scene.ubuf ` est exposé à cet emplacement de liaison. Le shader de fragments échantillonne une texture fournie au point de liaison 1 ; par conséquent, une paire combinée texture-échantillonneur est spécifiée pour cet emplacement de liaison.

QRhiGraphicsPipeline permet le test et l’écriture de profondeur, et effectue le culling des faces arrière. Il s’appuie également sur un certain nombre de valeurs par défaut : par exemple, la fonction de comparaison de profondeur est définie par défaut sur ` Less`, ce qui nous convient, et le mode de face avant est antihoraire, ce qui est également satisfaisant tel quel et ne nécessite donc pas d’être redéfini.

    scene.vbuf.reset(m_rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(cube)));
    scene.vbuf->create();

    scene.resourceUpdates = m_rhi->nextResourceUpdateBatch();
    scene.resourceUpdates->uploadStaticBuffer(scene.vbuf.get(), cube);

    scene.ubuf.reset(m_rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, 64));
    scene.ubuf->create();

    scene.cubeTex.reset(m_rhi->newTexture(QRhiTexture::RGBA8, CUBE_TEX_SIZE));
    scene.cubeTex->create();

    scene.sampler.reset(m_rhi->newSampler(QRhiSampler::Linear, QRhiSampler::Linear, QRhiSampler::None,
                                               QRhiSampler::ClampToEdge, QRhiSampler::ClampToEdge));
    scene.sampler->create();

    scene.srb.reset(m_rhi->newShaderResourceBindings());
    scene.srb->setBindings({
        QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::VertexStage, scene.ubuf.get()),
        QRhiShaderResourceBinding::sampledTexture(1, QRhiShaderResourceBinding::FragmentStage, scene.cubeTex.get(), scene.sampler.get())
    });
    scene.srb->create();

    scene.ps.reset(m_rhi->newGraphicsPipeline());
    scene.ps->setDepthTest(true);
    scene.ps->setDepthWrite(true);
    scene.ps->setCullMode(QRhiGraphicsPipeline::Back);
    scene.ps->setShaderStages({
        { QRhiShaderStage::Vertex, getShader(QLatin1String(":/shader_assets/texture.vert.qsb")) },
        { QRhiShaderStage::Fragment, getShader(QLatin1String(":/shader_assets/texture.frag.qsb")) }
    });
    QRhiVertexInputLayout inputLayout;
    // The cube is provided as non-interleaved sets of positions, UVs, normals.
    // Normals are not interesting here, only need the positions and UVs.
    inputLayout.setBindings({
        { 3 * sizeof(float) },
        { 2 * sizeof(float) }
    });
    inputLayout.setAttributes({
        { 0, 0, QRhiVertexInputAttribute::Float3, 0 },
        { 1, 1, QRhiVertexInputAttribute::Float2, 0 }
    });
    scene.ps->setSampleCount(m_sampleCount);
    scene.ps->setVertexInputLayout(inputLayout);
    scene.ps->setShaderResourceBindings(scene.srb.get());
    scene.ps->setRenderPassDescriptor(renderTarget()->renderPassDescriptor());
    scene.ps->create();

Dans la réimplémentation de ` render()`, les données fournies par l’utilisateur sont d’abord vérifiées. Si la ` QSlider ` contrôlant la rotation a fourni une nouvelle valeur, ou si la ` QTextEdit ` contenant le texte du cube a modifié son texte, les ressources graphiques dont le contenu dépend de ces données sont mises à jour.

Ensuite, un seul passage de rendu avec un seul appel de dessin est enregistré. Les données du maillage du cube sont fournies dans un format non entrelacé, d’où la nécessité de deux liaisons d’entrée de sommets : l’une correspond aux positions (x, y, z) et l’autre aux coordonnées UV (u, v), avec un décalage de départ correspondant à 36 paires de flottants x-y-z.

void ExampleRhiWidget::render(QRhiCommandBuffer *cb)
{
    if (itemData.cubeRotationDirty) {
        itemData.cubeRotationDirty = false;
        updateMvp();
    }

    if (itemData.cubeTextDirty) {
        itemData.cubeTextDirty = false;
        updateCubeTexture();
    }

    QRhiResourceUpdateBatch *resourceUpdates = scene.resourceUpdates;
    if (resourceUpdates)
        scene.resourceUpdates = nullptr;

    const QColor clearColor = QColor::fromRgbF(0.4f, 0.7f, 0.0f, 1.0f);
    cb->beginPass(renderTarget(), clearColor, { 1.0f, 0 }, resourceUpdates);

    cb->setGraphicsPipeline(scene.ps.get());
    cb->setViewport(QRhiViewport(0, 0, m_pixelSize.width(), m_pixelSize.height()));
    cb->setShaderResources();
    const QRhiCommandBuffer::VertexInput vbufBindings[] = {
        { scene.vbuf.get(), 0 },
        { scene.vbuf.get(), quint32(36 * 3 * sizeof(float)) }
    };
    cb->setVertexInput(0, 2, vbufBindings);
    cb->draw(36);

    cb->endPass();
}

Comment les données fournies par l’utilisateur sont-elles envoyées ? Prenons l’exemple de la rotation. La fonction `main()` se connecte au signal ` valueChanged ` de l’ QSlider. Lorsque ce signal est émis, la fonction lambda connectée appelle `setCubeRotation()` sur l’ExampleRhiWidget. À ce stade, si la valeur est différente de la précédente, elle est stockée et un indicateur de modification est activé. Ensuite, et c’est le plus important, la fonction update() est appelée sur l’ExampleRhiWidget. C’est ce qui déclenche le rendu d’une nouvelle image dans la texture de fond de l’ QRhiWidget. Sans cela, le contenu de l’ExampleRhiWidget ne serait pas mis à jour lors du déplacement du curseur.

    void setCubeTextureText(const QString &s)
    {
        if (itemData.cubeText == s)
            return;
        itemData.cubeText = s;
        itemData.cubeTextDirty = true;
        update();
    }

    void setCubeRotation(float r)
    {
        if (itemData.cubeRotation == r)
            return;
        itemData.cubeRotation = r;
        itemData.cubeRotationDirty = true;
        update();
    }

Exemple de projet sur code.qt.io

Voir également QRhi, Exemple de widget RHI simple et Exemple de fenêtre RHI.

© 2026 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.