Sur cette page

Scene Graph - RHI sous QML

Montre comment effectuer un rendu directement avec QRhi sous une scène Qt Quick.

Introduction

L'exemple RHI Under QML montre comment une application peut utiliser les signaux QQuickWindow::beforeRendering() et QQuickWindow::beforeRenderPassRecording() pour dessiner un contenu personnalisé basé sur QRhi sous une scène Qt Quick.

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

Dans cet exemple, nous verrons également comment il est possible d'avoir des valeurs exposées à QML qui affectent le rendu basé sur QRhi. Nous animons la valeur seuil à l'aide d'un NumberAnimation dans le fichier QML et cette valeur flottante est ensuite transmise dans un tampon uniforme au fragment shader.

L'exemple est équivalent dans la plupart des cas aux exemples OpenGL sous QML, Direct3D 11 sous QML, Metal sous QML et Vulkan sous QML. Ces exemples rendent le même contenu en utilisant directement une API 3D. Cet exemple, en revanche, est totalement multiplateforme et portable, car il prend en charge de manière inhérente le fonctionnement de toutes les API 3D prises en charge par QRhi (telles que OpenGL, Vulkan, Metal, Direct 3D 11 et 12).

Remarque : cet exemple démontre une fonctionnalité avancée de bas niveau effectuant un rendu 3D portable et multiplateforme, tout en s'appuyant sur des API avec une garantie de compatibilité limitée du module Qt GUI. Pour pouvoir utiliser les API de QRhi, l'application renvoie à Qt::GuiPrivate et inclut <rhi/qrhi.h>.

L'ajout d'un rendu personnalisé en tant que sous-couche/surcouche 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 propre rendu de la scène Qt Quick à l'aide de QSGRenderNode, ou à générer une passe de rendu entièrement distincte ciblant une cible de rendu dédiée (une texture), puis à faire en sorte qu'un élément de la scène affiche la texture. Reportez-vous aux exemples Graphique de la scène - Elément de texture RHI et Graphique de la scène - QSGRenderNode personnalisé concernant ces approches.

Concepts de base

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 Qt Quick. Cependant, deux signaux sont importants ici : les commandes QRhi de l'application doivent être enregistrées dans le même tampon de commande que celui utilisé par le graphe de scène et, de plus, les commandes doivent appartenir à la même passe de rendu. beforeRendering() n'est pas suffisant en soi, car il est émis au début de l'image, avant de commencer à enregistrer une passe de rendu via QRhiCommandBuffer::beginPass(). En se connectant également à beforeRenderPassRecording(), les commandes de l'application et le rendu du graphe de scène finiront dans le bon ordre :

Visite guidée

Le rendu personnalisé est encapsulé dans un QQuickItem personnalisé. RhiSquircle dérive de QQuickItem et est exposé à QML (notez le QML_ELEMENT). La scène QML instancie RhiSquircle. Notez cependant qu'il ne s'agit pas d'un élément visuel : l'indicateur QQuickItem::ItemHasContents n'est pas activé. La position et la taille de l'élément n'ont donc 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;
};

Au lieu de cela, 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 y en a un. Nous voulons que le slot connecté soit invoqué 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);
    }
}

Dans la phase de synchronisation du graphe de scène, l'infrastructure de rendu est créée, si ce n'est pas encore fait, et les données pertinentes pour le rendu sont synchronisées, c'est-à-dire copiées de l'élément RhiSquircle, qui vit sur le thread principal, à l'objet SquircleRenderer qui vit sur le thread de rendu. (S'il n'y a pas de thread de rendu, les deux objets vivent sur le thread principal) L'accès aux données est sûr car le thread principal est bloqué pendant que le thread de rendu exécute sa phase de synchronisation. Voir 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 opère sur le thread de rendu, cela n'est, en théorie, pas totalement sûr. D'où la nécessité de faire une copie.

Lors de la configuration de SquircleRenderer, des connexions sont établies avec beforeRendering() et beforeRenderPassRecording(), qui sont la clé 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 beforeRendering() est émis, les ressources QRhi nécessaires à notre rendu personnalisé, telles que QRhiBuffer, QRhiGraphicsPipeline et les objets connexes, sont créées si ce n'est pas encore 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(). La mémoire tampon des sommets ne change pas de contenu une fois que l'ensemble initial de sommets y a été téléchargé. La mémoire tampon uniforme est cependant une mémoire tampon dynamic, comme c'est généralement le cas pour ce type de mémoire tampon. Son contenu, du moins certaines régions, est mis à jour à chaque image. D'où l'appel inconditionnel à updateDynamicBuffer() pour le décalage 0 et une taille d'octet de 4 (qui est sizeof(float) puisque le type C++ float correspond au type 32 bits de GLSL float). Ce qui est stocké à cette position est la valeur de t, qui est mise à jour à chaque image, c'est-à-dire à chaque invocation de frameStart().

Il y a une valeur flottante supplémentaire dans la mémoire tampon, à partir du décalage 4. Elle est utilisée pour tenir compte des différences de système de coordonnées des API 3D : lorsque isYUpInNDC() renvoie false, ce qui est le cas avec Vulkan en particulier, la valeur est fixée à -1,0, ce qui conduit à inverser la valeur Y dans le vecteur à 2 composantes qui est transmis (avec interpolation) au nuanceur de fragments sur la base duquel la couleur est calculée. De cette manière, la sortie à l'écran est identique (c'est-à-dire que le coin supérieur gauche est verdâtre, le coin inférieur gauche est rougeâtre), quelle que soit l'API 3D utilisée. Cette valeur n'est mise à jour qu'une seule fois dans le tampon uniforme, de la même manière que dans le tampon de sommets. Cela met en évidence un problème auquel les codes de rendu de bas niveau qui visent à être portables doivent souvent faire face : les différences de système de coordonnées dans les coordonnées normalisées du périphérique (NDC) et dans les images et les tampons d'images. Par exemple, les NDC utilisent un système d'origine en bas à gauche partout sauf dans Vulkan. Alors que les framebuffers utilisent un système d'origine en haut à gauche partout sauf pour OpenGL. Les moteurs de rendu typiques qui travaillent avec une projection en perspective peuvent souvent ignorer ce problème en s'appuyant commodément sur QRhi::clipSpaceCorrMatrix(), qui est une matrice qui peut être multipliée dans la matrice de projection, et applique à la fois une inversion Y lorsque cela est nécessaire, et répond également au fait que la profondeur de l'espace clip fonctionne sur -1..1 avec OpenGL mais sur 0..1 partout ailleurs. Cependant, dans certains cas, comme dans cet exemple, cela n'est pas applicable. L'application et la logique du shader doivent plutôt effectuer l'ajustement nécessaire des positions des vertex et des UV en fonction 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 à partir de QQuickWindow. Notez que cela suppose que QQuickWindow est une fenêtre régulière à l'écran. S'il utilise QQuickRenderControl à la place, par exemple pour effectuer un rendu hors écran dans une texture, l'interrogation de la chaîne de permutation serait erronée puisqu'il n'y a pas de chaîne de permutation à ce moment-là.

En raison du signal émis après que Qt Quick ait appelé QRhi::beginFrame(), il est déjà possible d'interroger le tampon de commande et la cible de rendu à partir de la chaîne de permutation. C'est ce qui permet d'émettre commodément un QRhiCommandBuffer::resourceUpdate() sur l'objet renvoyé par QRhiSwapChain::currentFrameCommandBuffer(). Lors de la création d'un pipeline graphique, un QRhiRenderPassDescriptor peut être récupéré à partir du QRhiRenderTarget renvoyé par QRhiSwapChain::currentFrameRenderTarget(). (Notez que cela signifie que le pipeline graphique construit ici n'est adapté qu'au rendu vers la swapchain, ou au mieux vers une autre cible de rendu qui est compatible avec elle ; il est probable que si nous voulions effectuer un rendu vers une texture, alors un QRhiRenderPassDescriptor différent, et donc un pipeline graphique différent, serait nécessaire puisque les formats de la texture et de la swapchain peuvent différer).

void SquircleRenderer::frameStart() { // Cette fonction est invoqué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 flottantsm_uniformBuffer.reset(rhi->newBuffer(QRhiBuffer::Dynamique, 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::TargetBlend blend ; 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, sur QQuickWindow::beforeRenderPassRecording(), un appel à dessiner une bande triangulaire à 4 sommets est enregistré. En pratique, cet exemple dessine simplement un quadrilatère et calcule les couleurs des pixels en utilisant la logique des nuanceurs 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. La chose importante à garder à l'esprit est que tout ce qui est enregistré sur le site QRhiCommandBuffer récupéré sur le site swapchain de la fenêtre est effectivement ajouté avant le rendu du graphe de scène Qt Quick au sein de la passe de rendu principale.

Remarque : cela signifie que si le tampon de profondeur est utilisé pour tester la profondeur et écrire les valeurs de profondeur, le contenu de Qt Quick peut être affecté par les valeurs écrites dans le tampon de profondeur. Voir 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 relatives à la gestion des primitives opaques et mélangées en alpha.

Pour obtenir la taille de la fenêtre en pixels, QRhiRenderTarget::pixelSize() est utilisé. C'est pratique car l'exemple n'a pas besoin de calculer la taille de la fenêtre par d'autres moyens et n'a pas à se préoccuper de l'application de high DPI scale factor, s'il y en a une.

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 vertex et fragment shaders passent par le pipeline de conditionnement standard des shaders QRhi. Initialement écrits en GLSL compatible avec Vulkan, ils sont compilés en SPIR-V, puis transposés dans d'autres langages d'ombrage par les outils de Qt. Lorsque CMake est utilisé, l'exemple s'appuie sur la commande qt_add_shaders qui rend simple et pratique le regroupement des shaders avec l'application et l'exécution du traitement nécessaire au moment de la construction. Voir Qt Shader Tools Build System Integration pour plus de détails.

Spécifier BASE permet de supprimer le préfixe ../shared, tandis que PREFIX ajoute le préfixe /scenegraph/rhiunderqml prévu. Le chemin 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 au moment de la compilation, et les répertorie dans le fichier qrc. Cette approche n'est cependant pas recommandée pour les nouvelles applications qui utilisent CMake comme système de compilation.

Exemple de projet @ code.qt.io

Voir aussi Scene Graph - RHI Texture Item et Scene Graph - Custom QSGRenderNode.

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