Sur cette page

Arborescence de scènes - RHI sous QML

Explique comment effectuer un rendu directement avec QRhi dans une scène Qt Quick.

« Squircle » animé, rendu avec QRhi dans une scène Qt Quick avec superposition de texte

Introduction

L'exemple « RHI Under QML » montre comment une application peut utiliser les signaux ` QQuickWindow::beforeRendering()` et ` QQuickWindow::beforeRenderPassRecording()` pour afficher du contenu personnalisé basé sur ` QRhi` dans une scène ` Qt Quick `.

Pour les applications qui souhaitent afficher du contenu QRhi par-dessus la scène Qt Quick, utilisez QQuickWindow::beforeRendering() pour charger des données dans des tampons et connectez-vous au signal QQuickWindow::afterRenderPassRecording().

Dans cet exemple, nous verrons également comment il est possible d’exposer au QML des valeurs qui influencent le rendu basé sur QRhi. Nous animons la valeur seuil à l’aide d’un NumberAnimation dans le fichier QML, et cette valeur de type float est ensuite transmise dans un tampon uniforme au shader de fragment.

Cet exemple est équivalent à bien des égards aux exemples « OpenGL sous QML », « Direct3D 11 sous QML », « Metal sous QML » et « Vulkan sous QML ». Ces exemples affichent le même contenu en utilisant directement une API 3D. Cet exemple, en revanche, est entièrement multiplateforme et portable, car il prend en charge de manière native toutes les API 3D prises en charge par QRhi (telles que OpenGL, Vulkan, Metal, Direct 3D 11 et 12).

Remarque : cet exemple illustre des fonctionnalités avancées de bas niveau permettant un rendu 3D portable et multiplateforme, tout en s'appuyant sur des API dont la compatibilité n'est pas entièrement garantie par le module Qt GUI. Pour pouvoir utiliser les API d'QRhi, l'application est liée à Qt::GuiPrivate et inclut <rhi/qrhi.h>.

L'ajout d'un rendu personnalisé en tant que sous-couche ou superposition est l'une des trois façons d'intégrer un rendu 2D/3D personnalisé dans une scène Qt Quick. Les deux autres options consistent à effectuer le rendu « en ligne » avec le rendu propre à la scène Qt Quick à l’aide de QSGRenderNode, ou à générer un passage de rendu entièrement distinct ciblant une cible de rendu dédiée (une texture), puis à faire en sorte qu’un élément de la scène affiche cette texture. Reportez-vous aux exemples « Scene Graph - RHI Texture Item » et « Scene Graph - Custom QSGRenderNode » concernant ces approches.

Concepts fondamentaux

Qt Quick Le signal `beforeRendering()` est émis au début de chaque image, avant que le graphe de scène ne commence son rendu ; ainsi, tous les appels de dessin ` QRhi ` effectués en réponse à ce signal s’empileront sous les éléments du rendu par scène. Cependant, deux points sont importants ici : les commandes « QRhi » propres à l’application doivent être enregistrées dans le même tampon de commandes que celui utilisé par le graphe de scène, et de plus, ces commandes doivent appartenir au même passage de rendu. La fonction `beforeRendering()` n’est pas suffisante à elle seule, car elle est émise au début de l’image, avant le début de l’enregistrement d’un passage de rendu via ` QRhiCommandBuffer::beginPass()`. En se connectant également à `beforeRenderPassRecording()`, les commandes propres à l’application et le rendu propre au graphe de scène se retrouveront dans le bon ordre :

Démonstration

Le rendu personnalisé est encapsulé dans une classe personnalisée ` QQuickItem`. ` RhiSquircle ` dérive de ` QQuickItem` et est exposé à QML (notez l’ QML_ELEMENT). La scène QML instancie ` RhiSquircle`. Notez toutefois qu’il ne s’agit pas d’un élément visuel : le drapeau ` QQuickItem::ItemHasContents ` n’est pas activé. Ainsi, la position et la taille de l’élément n’ont aucune importance et il ne réimplémente pas ` updatePaintNode()`.

class RhiSquircle : public QQuickItem
{
    Q_OBJECT
    Q_PROPERTY(qreal t READ t WRITE setT NOTIFY tChanged)
    QML_ELEMENT

public:
    RhiSquircle();

    qreal t() const { return m_t; }
    void setT(qreal t);

signals:
    void tChanged();

public slots:
    void sync();
    void cleanup();

private slots:
    void handleWindowChanged(QQuickWindow *win);

private:
    void releaseResources() override;

    qreal m_t = 0;
    SquircleRenderer *m_renderer = nullptr;
};

En revanche, lorsque l’élément est associé à un ` QQuickWindow`, il se connecte au signal ` QQuickWindow::beforeSynchronizing()`. L’utilisation de ` Qt::DirectConnection ` est importante, car ce signal est émis sur le thread de rendu ` Qt Quick `, s’il existe. Nous voulons que le slot connecté soit appelé sur ce même thread.

RhiSquircle::RhiSquircle()
{
    connect(this, &QQuickItem::windowChanged, this, &RhiSquircle::handleWindowChanged);
}

void RhiSquircle::handleWindowChanged(QQuickWindow *win)
{
    if (win) {
        connect(win, &QQuickWindow::beforeSynchronizing, this, &RhiSquircle::sync, Qt::DirectConnection);
        connect(win, &QQuickWindow::sceneGraphInvalidated, this, &RhiSquircle::cleanup, Qt::DirectConnection);
        // Ensure we start with cleared to black. The squircle's blend mode relies on this.
        win->setColor(Qt::black);
    }
}

Au cours de la phase de synchronisation du graphe de scène, l’infrastructure de rendu est créée, si ce n’est pas déjà fait, et les données pertinentes pour le rendu sont synchronisées, c’est-à-dire copiées depuis l’élément RhiSquircle, qui réside sur le thread principal, vers l’objet SquircleRenderer qui réside sur le thread de rendu. (S’il n’y a pas de thread de rendu, les deux objets résident alors sur le thread principal.) L’accès aux données est sécurisé car le thread principal est bloqué pendant que le thread de rendu exécute sa phase de synchronisation. Consultez la page « Qt Quick Scene Graph » pour plus d’informations sur le modèle de threading et de rendu du graphe de scène.

Outre la valeur de ` t`, le pointeur ` QQuickWindow ` associé est également copié. Bien que ` SquircleRenderer ` puisse interroger ` window()` sur l’élément ` RhiSquircle ` même lorsqu’il s’exécute sur le thread de rendu, cela n’est, en théorie, pas entièrement sûr. D’où la création d’une copie.

Lors de la configuration de l’ SquircleRenderer, des connexions sont établies avec les fonctions beforeRendering() et beforeRenderPassRecording(), qui sont essentielles pour pouvoir agir et injecter les commandes de rendu 3D personnalisées de l’application au moment opportun.

void RhiSquircle::sync()
{
    // This function is invoked on the render thread, if there is one.

    if (!m_renderer) {
        m_renderer = new SquircleRenderer;
        // Initializing resources is done before starting to record the
        // renderpass, regardless of wanting an underlay or overlay.
        connect(window(), &QQuickWindow::beforeRendering, m_renderer, &SquircleRenderer::frameStart, Qt::DirectConnection);
        // Here we want an underlay and therefore connect to
        // beforeRenderPassRecording. Changing to afterRenderPassRecording
        // would render the squircle on top (overlay).
        connect(window(), &QQuickWindow::beforeRenderPassRecording, m_renderer, &SquircleRenderer::mainPassRecordingStart, Qt::DirectConnection);
    }
    m_renderer->setT(m_t);
    m_renderer->setWindow(window());
}

Lorsque l’événement beforeRendering() est déclenché, les ressources QRhi nécessaires à notre rendu personnalisé, telles que QRhiBuffer, QRhiGraphicsPipeline et les objets associés, sont créées si ce n’est pas déjà fait.

Les données contenues dans les tampons sont mises à jour (plus précisément, les opérations de mise à jour des données sont mises en file d’attente) à l’aide de ` QRhiResourceUpdateBatch ` et ` QRhiCommandBuffer::resourceUpdate()`. Le tampon de sommets ne modifie pas son contenu une fois que l’ensemble initial de sommets y a été chargé. Le tampon d’uniformes, en revanche, est un tampon « dynamic », comme c’est généralement le cas pour ce type de tampons. Son contenu, du moins certaines de ses régions, est mis à jour à chaque image. D’où l’appel inconditionnel à ` updateDynamicBuffer()` pour un décalage de 0 et une taille d’octet de 4 (ce qui correspond à ` sizeof(float) `, puisque le type C++ ` float ` correspond justement au type GLSL 32 bits ` float`). La valeur stockée à cette position est celle de ` t`, et celle-ci est mise à jour à chaque image, c'est-à-dire à chaque appel de `frameStart()`.

Il existe une valeur flottante supplémentaire dans le tampon, à partir du décalage 4. Elle sert à prendre en compte les différences entre les systèmes de coordonnées des API 3D : lorsque ` isYUpInNDC()` renvoie ` false`, ce qui est notamment le cas avec Vulkan, la valeur est fixée à -1,0, ce qui entraîne l’inversion de la valeur Y dans le vecteur à deux composantes qui est transmis (avec interpolation) au shader de fragment à partir duquel la couleur est calculée. De cette manière, le rendu à l’écran est identique (c’est-à-dire que le coin supérieur gauche est tiré vers le vert, et le coin inférieur gauche vers le rouge), quelle que soit l’API 3D utilisée. Cette valeur n’est mise à jour qu’une seule fois dans le tampon d’uniformes, de la même manière que dans le tampon de sommets. Cela met en évidence un problème auquel le code de rendu de bas niveau, qui vise à être portable, est souvent confronté : les différences entre les systèmes de coordonnées normalisées du périphérique (NDC) et celles des images et des tampons de trame. Par exemple, les NDC utilisent un système où l’origine se trouve en bas à gauche partout, sauf dans Vulkan. À l’inverse, les tampons d’image utilisent un système où l’origine se trouve en haut à gauche partout, sauf dans OpenGL. Les moteurs de rendu classiques fonctionnant avec une projection en perspective peuvent souvent ignorer ce problème en s'appuyant simplement sur ` QRhi::clipSpaceCorrMatrix()`, une matrice pouvant être multipliée par la matrice de projection, qui applique à la fois une inversion de l'axe Y lorsque cela est nécessaire, et tient compte du fait que la profondeur de l'espace de découpe est de -1..1 avec OpenGL, mais de 0..1 partout ailleurs. Cependant, dans certains cas, comme dans cet exemple, cette approche n’est pas applicable. L’application et la logique du shader doivent alors effectuer les ajustements nécessaires des positions des sommets et des coordonnées UV, en fonction des résultats des requêtes QRhi::isYUpInNDC() et QRhi::isYUpInFramebuffer().

Pour accéder aux objets ` QRhi ` et ` QRhiSwapChain ` utilisés par ` Qt Quick `, il suffit de les interroger via ` QQuickWindow`. Notez que cela suppose que ` QQuickWindow ` soit une fenêtre normale affichée à l’écran. Si elle utilisait plutôt ` QQuickRenderControl `, par exemple pour effectuer un rendu hors écran dans une texture, l’interrogation de la chaîne d’échange serait erronée puisqu’il n’y a alors pas de chaîne d’échange.

Comme le signal est émis après que ` Qt Quick ` a appelé ` QRhi::beginFrame()`, il est déjà possible d'interroger le tampon de commandes et la cible de rendu à partir de la swapchain. C'est ce qui permet d'émettre facilement un ` QRhiCommandBuffer::resourceUpdate()` sur l'objet renvoyé par ` QRhiSwapChain::currentFrameCommandBuffer()`. Lors de la création d'un pipeline graphique, une ` QRhiRenderPassDescriptor ` peut être récupérée à partir du ` QRhiRenderTarget ` renvoyé par ` QRhiSwapChain::currentFrameRenderTarget()`. (notez que cela signifie que le pipeline graphique construit ici ne convient qu’au rendu vers la swapchain, ou au mieux vers une autre cible de rendu compatible avec celle-ci ; il est probable que si l’on souhaitait effectuer un rendu vers une texture, un QRhiRenderPassDescriptor différent, et donc un pipeline graphique différent, serait nécessaire, car les formats de la texture et de la swapchain peuvent différer)

void SquircleRenderer::frameStart()
{
    // Cette fonction est appelée sur le thread de rendu, s'il y en a un.

    QRhi*rhi = m_window->rhi();
    if (!rhi) {
        qWarning("QQuickWindow is not using QRhi for rendering");
       return;
    }
    QRhiSwapChain*swapChain = m_window->swapChain();
    if (!swapChain) {
        qWarning("No QRhiSwapChain?");
       return;
    }
    QRhiResourceUpdateBatch*resourceUpdates = rhi->nextResourceUpdateBatch();

    if (!m_pipeline) {
        m_vertexShader = getShader(QLatin1String(":/scenegraph/rhiunderqml/squircle_rhi.vert.qsb"));
        if (!m_vertexShader.isValid())
            qWarning("Failed to load vertex shader; rendering will be incorrect");

        m_fragmentShader = getShader(QLatin1String(":/scenegraph/rhiunderqml/squircle_rhi.frag.qsb"));
        if (!m_fragmentShader.isValid())
            qWarning("Failed to load fragment shader; rendering will be incorrect");

        m_vertexBuffer.reset(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(vertices)));
        m_vertexBuffer->create();
        resourceUpdates->uploadStaticBuffer(m_vertexBuffer.get(), vertices);

        const quint32 UBUF_SIZE = 4 + 4; // 2 flottants
        m_uniformBuffer.reset(rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, UBUF_SIZE));
        m_uniformBuffer->create();

        float yDir = rhi->isYUpInNDC() ? 1.0f:-1.0f;
        resourceUpdates->updateDynamicBuffer(m_uniformBuffer.get(), 4, 4, &yDir);

        m_srb.reset(rhi->newShaderResourceBindings());
        const auto visibleToAll = QRhiShaderResourceBinding::VertexStage | QRhiShaderResourceBinding::FragmentStage;
        m_srb->setBindings({
            QRhiShaderResourceBinding::uniformBuffer(0, visibleToAll, m_uniformBuffer.get())
        });
        m_srb->create();

        QRhiVertexInputLayout inputLayout;
        inputLayout.setBindings({
            { 2 * sizeof(float) }
        });
        inputLayout.setAttributes({
            { 0, 0, QRhiVertexInputAttribute::Float2, 0 }
        });

        m_pipeline.reset(rhi->newGraphicsPipeline());
        m_pipeline->setTopology(QRhiGraphicsPipeline::TriangleStrip);
        QRhiGraphicsPipeline::TargetBlendblend;
        blend.enable = true;
        blend.srcColor = QRhiGraphicsPipeline::SrcAlpha;
        blend.srcAlpha = QRhiGraphicsPipeline::SrcAlpha;
        blend.dstColor = QRhiGraphicsPipeline::One;
        blend.dstAlpha = QRhiGraphicsPipeline::One;
        m_pipeline->setTargetBlends({ blend });
        m_pipeline->setShaderStages({
            { QRhiShaderStage::Vertex, m_vertexShader },
            { QRhiShaderStage::Fragment, m_fragmentShader }
        });
        m_pipeline->setVertexInputLayout(inputLayout);
        m_pipeline->setShaderResourceBindings(m_srb.get());
        m_pipeline->setRenderPassDescriptor(swapChain->currentFrameRenderTarget()->renderPassDescriptor());
        m_pipeline->create();
    }

   float t = m_t;
    resourceUpdates->updateDynamicBuffer(m_uniformBuffer.get(), 0, 4, &t);

    swapChain->currentFrameCommandBuffer()->resourceUpdate(resourceUpdates);
}

Enfin, lors de l’appel à ` QQuickWindow::beforeRenderPassRecording()`, un appel de dessin pour une bande de triangles comportant 4 sommets est enregistré. Dans la pratique, cet exemple se contente de dessiner un quadrilatère et de calculer les couleurs des pixels à l’aide de la logique des shaders de fragment, mais les applications sont libres de réaliser des dessins plus complexes : la création de plusieurs pipelines graphiques et l’enregistrement de plusieurs appels de dessin sont tout à fait possibles. Il est important de garder à l’esprit que tout ce qui est enregistré sur l’ QRhiCommandBuffer récupérée via l’ swapchain de la fenêtre est effectivement préfixé avant le rendu propre au graphe de scène Qt Quick au sein du passage de rendu principal.

Remarque : cela signifie que si l’utilisation d’un tampon de profondeur avec test de profondeur et écriture des valeurs de profondeur est impliquée, le contenu de Qt Quick peut être affecté par les valeurs écrites dans le tampon de profondeur. Consultez la section « Qt Quick Scene Graph Default Renderer » pour plus de détails sur le moteur de rendu du graphe de scène, en particulier les sections consacrées à la gestion des primitives opaques et des primitives avec mélange alpha.

Pour obtenir la taille de la fenêtre en pixels, on utilise la fonction ` QRhiRenderTarget::pixelSize()`. Cela est pratique car, de cette manière, l’exemple n’a pas besoin de calculer la taille de la fenêtre d’affichage par d’autres moyens et n’a pas à se soucier d’appliquer l’` high DPI scale factor`, le cas échéant.

void SquircleRenderer::mainPassRecordingStart()
{
    // This function is invoked on the render thread, if there is one.

    QRhi *rhi = m_window->rhi();
    QRhiSwapChain *swapChain = m_window->swapChain();
    if (!rhi || !swapChain)
        return;

    const QSize outputPixelSize = swapChain->currentFrameRenderTarget()->pixelSize();
    QRhiCommandBuffer *cb = m_window->swapChain()->currentFrameCommandBuffer();
    cb->setViewport({ 0.0f, 0.0f, float(outputPixelSize.width()), float(outputPixelSize.height()) });
    cb->setGraphicsPipeline(m_pipeline.get());
    cb->setShaderResources();
    const QRhiCommandBuffer::VertexInput vbufBinding(m_vertexBuffer.get(), 0);
    cb->setVertexInput(0, 1, &vbufBinding);
    cb->draw(4);
}

Les shaders de sommets et de fragments passent par le pipeline standard de conditionnement des shaders « QRhi ». Initialement écrits en GLSL compatible Vulkan, ils sont compilés en SPIR-V, puis transpilés vers d’autres langages de shaders à l’aide des outils de Qt. Lorsqu’on utilise CMake, l’exemple s’appuie sur la commande ` qt_add_shaders `, qui permet de regrouper facilement et commodément les shaders avec l’application et d’effectuer le traitement nécessaire au moment de la compilation. Voir ` qt_add_shaders()` pour plus de détails.

La spécification de ` BASE ` permet de supprimer le préfixe ` ../shared `, tandis que ` PREFIX ` ajoute le préfixe ` /scenegraph/rhiunderqml ` souhaité. Le chemin d'accès final est donc ` :/scenegraph/rhiunderqml/squircle_rhi.vert.qsb`.

qt_add_shaders(rhiunderqml "rhiunderqml_shaders"
    PRECOMPILE
    OPTIMIZED
    PREFIX
        /scenegraph/rhiunderqml
    BASE
        ../shared
    FILES
        ../shared/squircle_rhi.vert
        ../shared/squircle_rhi.frag
)

Pour prendre en charge qmake, l’exemple fournit toujours les fichiers .qsb qui seraient normalement générés lors de la compilation, et les répertorie dans le fichier qrc. Cette approche n’est toutefois pas recommandée pour les nouvelles applications utilisant CMake comme système de compilation.

Exemple de projet sur code.qt.io

Voir également «Graphique de scène - Élément de texture RHI » et « Graphique de scène - QSGRenderNode personnalisé ».

© 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.