Exemple de widget RHI simple
Montre comment effectuer le rendu d'un triangle à l'aide de QRhi, l'API 3D de Qt et la couche d'abstraction du langage d'ombrage.

Capture d'écran de l'exemple de widget RHI simple
Cet exemple est, à bien des égards, le pendant de l'exemple de fenêtre RHI dans le monde QWidget. La sous-classe QRhiWidget de cette application effectue le rendu d'un seul triangle, en utilisant un pipeline graphique simple avec des nuanceurs de sommets et de fragments basiques. Contrairement à l'application simple basée sur QWindow, cet exemple n'a pas besoin de se préoccuper des détails de bas niveau, tels que la configuration de la fenêtre et de QRhi, ou la gestion de la chaîne d'échange et des événements de la fenêtre, car cela est pris en charge par le cadre QWidget. L'instance de la sous-classe QRhiWidget est ajoutée à une QVBoxLayout. Pour que l'exemple reste minimal et compact, aucun autre widget ou contenu 3D n'est introduit.
Une fois qu'une instance de ExampleRhiWidget, une sous-classe de QRhiWidget, est ajoutée à la hiérarchie des enfants d'un widget de premier niveau, la fenêtre correspondante devient automatiquement une fenêtre avec rendu Direct 3D, Vulkan, Metal ou OpenGL. Le contenu du widget rendu par QPainter, c'est-à-dire tout ce qui n'est pas QRhiWidget, QOpenGLWidget ou QQuickWidget, est ensuite téléchargé dans une texture, tandis que les widgets spéciaux mentionnés effectuent chacun un rendu dans une texture. L'ensemble de textures qui en résulte est composé par le backingstore du widget de niveau supérieur.
Structure et main()
La fonction main() est assez simple. Par défaut, le widget de premier niveau a une taille de 720p (cette taille est exprimée en unités logiques, la taille réelle des pixels peut être différente, en fonction de scale factor. La fenêtre est redimensionnable. QRhiWidget simplifie la mise en œuvre de sous-classes qui gèrent correctement le redimensionnement du widget en raison de la taille de la fenêtre ou des modifications de la disposition.
int main(int argc, char **argv) { QApplication app(argc, argv); ExampleRhiWidget *rhiWidget = new ExampleRhiWidget; QVBoxLayout *layout = new QVBoxLayout; layout->addWidget(rhiWidget); QWidget w; w.setLayout(layout); w.resize(1280, 720); w.show(); return app.exec(); }
La sous-classe QRhiWidget réimplante les deux virtuels : initialize() et render(). initialize() est appelé au moins une fois avant render(), mais il est également invoqué lors d'un certain nombre de changements importants, comme lorsque la texture d'arrière-plan du widget est recréée en raison d'un changement de taille du widget, lorsque les paramètres de la cible de rendu changent ou lorsque le widget passe à un nouveau QRhi en raison d'un déplacement vers une nouvelle fenêtre de niveau supérieur.
Remarque : contrairement au modèle QOpenGLWidget's legacy initializeGL - resizeGL - paintGL, il n'y a que deux virtuels dans QRhiWidget. Cela s'explique par le fait qu'il y a plus d'événements spéciaux à prendre en compte que le simple redimensionnement, par exemple lors de la répartition sur une autre fenêtre de niveau supérieur. (Les implémentations robustes de QOpenGLWidget ont dû gérer cela en effectuant une comptabilité supplémentaire, par exemple en suivant la durée de vie associée de QOpenGLContext, ce qui signifie que les trois virtuels n'étaient pas réellement suffisants). Une paire plus simple de initialize - render, où initialize est ré-invoqué lors de changements importants, est mieux adaptée.
L'instance QRhi n'appartient pas au widget. Elle sera interrogée dans initialize() from the base class . Le fait de la stocker en tant que membre permet de reconnaître les changements lorsque initialize() est à nouveau invoqué. Les ressources graphiques, telles que les tampons de sommets et d'uniformes, ou le pipeline graphique sont toutefois sous le contrôle de ExampleRhiWidget.
#include <QRhiWidget> #include <rhi/qrhi.h> class ExampleRhiWidget : public QRhiWidget { public: ExampleRhiWidget(QWidget *parent = nullptr) : QRhiWidget(parent) { } void initialize(QRhiCommandBuffer *cb) override; void render(QRhiCommandBuffer *cb) override; private: QRhi *m_rhi = nullptr; std::unique_ptr<QRhiBuffer> m_vbuf; std::unique_ptr<QRhiBuffer> m_ubuf; std::unique_ptr<QRhiShaderResourceBindings> m_srb; std::unique_ptr<QRhiGraphicsPipeline> m_pipeline; QMatrix4x4 m_viewProjection; float m_rotation = 0.0f; };
Pour que l'instruction #include <rhi/qrhi.h> fonctionne, l'application doit être liée à GuiPrivate (ou gui-private avec qmake). Voir QRhi pour plus de détails sur la promesse de compatibilité de la famille d'API QRhi.
CMakeLists.txt
target_link_libraries(simplerhiwidget PRIVATE
Qt6::Core
Qt6::Gui
Qt6::GuiPrivate
Qt6::Widgets
)Configuration du rendu
Dans examplewidget.cpp, l'implémentation du widget utilise une fonction d'aide pour charger un objet QShader à partir d'un fichier .qsb. Cette application envoie des fichiers .qsb pré-conditionnés intégrés dans l'exécutable via le système de ressources Qt. En raison des dépendances entre les modules (et du fait que qmake est toujours pris en charge), cet exemple n'utilise pas la fonction CMake pratique qt_add_shaders(), mais fournit plutôt les fichiers .qsb dans l'arborescence des sources. Les applications réelles sont encouragées à éviter cela et à utiliser plutôt les fonctions d'intégration CMake du module Qt Shader Tools (qt_add_shaders). Quelle que soit l'approche, dans le code C++, le chargement des fichiers .qsb regroupés/générés est le même.
static QShader getShader(const QString &name) { QFile f(name); return f.open(QIODevice::ReadOnly) ? QShader::fromSerialized(f.readAll()) : QShader(); }
Examinons l'implémentation de la fonction initialize(). Tout d'abord, l'objet QRhi est interrogé et stocké en vue d'une utilisation ultérieure, mais aussi pour permettre une comparaison lors des invocations futures de la fonction. En cas de non-concordance (par exemple, lorsque le widget est déplacé d'une fenêtre à l'autre), la recréation des ressources graphiques à recréer est déclenchée par la destruction et la nullité d'un objet approprié, en l'occurrence l'objet m_pipeline. L'exemple ne démontre pas activement le reparentage entre fenêtres, mais il est prêt à le gérer. Il est également prêt à gérer un changement de taille de widget qui peut se produire lors du redimensionnement de la fenêtre. Cela ne nécessite pas de traitement particulier puisque initialize() est invoqué à chaque fois que cela se produit, et que l'interrogation de renderTarget()->pixelSize() ou colorTexture()->pixelSize() donne toujours la dernière taille à jour en pixels. Ce à quoi cet exemple n'est pas préparé, c'est à la modification de color buffer formats et multisample settings puisqu'il n'utilise jamais que les valeurs par défaut (RGBA8 et pas d'anticrénelage multi-échantillon).
void ExampleRhiWidget::initialize(QRhiCommandBuffer *cb) { if (m_rhi != rhi()) { m_pipeline.reset(); m_rhi = rhi(); }
Lorsque les ressources graphiques doivent être (re)créées, initialize() le fait à l'aide d'un code typique basé sur QRhi. Un seul tampon de vertex avec la position entrelacée - les données de vertex de couleur est suffisant, tandis que la matrice de projection de vue de modèle est exposée via un tampon uniforme de 64 octets (16 flottants). Le tampon uniforme est la seule ressource visible par le shader, et il n'est utilisé que dans le shader de vertex. Le pipeline graphique repose sur de nombreux paramètres par défaut (par exemple, test de profondeur désactivé, mélange désactivé, écriture des couleurs activée, élimination des faces désactivée, topologie par défaut des triangles, etc.) La disposition des données de sommet est la suivante : x, y, r, g, b, d'où un intervalle de 5 flottants, tandis que le deuxième attribut d'entrée du sommet (la couleur) a un décalage de 2 flottants (en sautant x et y). Chaque pipeline graphique doit être associé à un QRhiRenderPassDescriptor qui peut être récupéré à partir du QRhiRenderTarget géré par la classe de base.
Remarque : cet exemple s'appuie sur la valeur par défaut de QRhiWidget( autoRenderTarget ) fixée à true. C'est pourquoi il n'a pas besoin de gérer la cible de rendu, mais peut simplement interroger la cible existante en appelant renderTarget().
if (!m_pipeline) { m_vbuf.reset(m_rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(vertexData))); m_vbuf->create(); m_ubuf.reset(m_rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, 64)); m_ubuf->create(); m_srb.reset(m_rhi->newShaderResourceBindings()); m_srb->setBindings({ QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::VertexStage, m_ubuf.get()), }); m_srb->create(); m_pipeline.reset(m_rhi->newGraphicsPipeline()); m_pipeline->setShaderStages({ { QRhiShaderStage::Vertex, getShader(QLatin1String(":/shader_assets/color.vert.qsb")) }, { QRhiShaderStage::Fragment, getShader(QLatin1String(":/shader_assets/color.frag.qsb")) } }); QRhiVertexInputLayout inputLayout; inputLayout.setBindings({ { 5 * sizeof(float) } }); inputLayout.setAttributes({ { 0, 0, QRhiVertexInputAttribute::Float2, 0 }, { 0, 1, QRhiVertexInputAttribute::Float3, 2 * sizeof(float) } }); m_pipeline->setVertexInputLayout(inputLayout); m_pipeline->setShaderResourceBindings(m_srb.get()); m_pipeline->setRenderPassDescriptor(renderTarget()->renderPassDescriptor()); m_pipeline->create(); QRhiResourceUpdateBatch *resourceUpdates = m_rhi->nextResourceUpdateBatch(); resourceUpdates->uploadStaticBuffer(m_vbuf.get(), vertexData); cb->resourceUpdate(resourceUpdates); }
Enfin, la matrice de projection est calculée. Cela dépend de la taille du widget et est donc effectué de manière inconditionnelle dans chaque invocation des fonctions.
Note : Les calculs de taille et de fenêtre ne doivent jamais se baser sur la taille des pixels demandée à la ressource servant de tampon de couleur, puisqu'il s'agit de la cible de rendu réelle. Évitez de calculer manuellement les tailles, les fenêtres, les ciseaux, etc. sur la base de la taille rapportée par QWidget ou du rapport entre les pixels du périphérique.
Remarque : la matrice de projection inclut le site correction matrix de QRhi afin de tenir compte des différences entre les API 3D en ce qui concerne les coordonnées normalisées de l'appareil. (par exemple, Y vers le bas par rapport à Y vers le haut).
Une translation de -4 est appliquée pour s'assurer que le triangle dont les valeurs z sont égales à 0 sera visible.
const QSize outputSize = renderTarget()->pixelSize(); m_viewProjection = m_rhi->clipSpaceCorrMatrix(); m_viewProjection.perspective(45.0f, outputSize.width() / (float) outputSize.height(), 0.01f, 1000.0f); m_viewProjection.translate(0, 0, -4); }
Rendu
Le widget enregistre une seule passe de rendu, qui contient un seul appel de dessin.
La matrice de projection de la vue calculée dans l'étape d'initialisation est combinée avec la matrice du modèle, qui dans ce cas est une simple rotation. La matrice résultante est ensuite écrite dans le tampon uniforme. Notez que resourceUpdates est passé à beginPass(), ce qui est un raccourci pour ne pas avoir à invoquer resourceUpdate() manuellement.
void ExampleRhiWidget::render(QRhiCommandBuffer *cb) { QRhiResourceUpdateBatch *resourceUpdates = m_rhi->nextResourceUpdateBatch(); m_rotation += 1.0f; QMatrix4x4 modelViewProjection = m_viewProjection; modelViewProjection.rotate(m_rotation, 0, 1, 0); resourceUpdates->updateDynamicBuffer(m_ubuf.get(), 0, 64, modelViewProjection.constData());
Dans la passe de rendu, un seul appel de dessin avec 3 sommets est enregistré. Le pipeline graphique créé lors de l'étape d'initialisation est lié au tampon de commande, et la fenêtre de visualisation est définie de manière à couvrir l'ensemble du widget. Pour rendre le tampon uniforme visible au nuanceur (de sommets), setShaderResources() est appelé sans argument, ce qui signifie que l'on utilise m_srb puisque celui-ci a été associé au pipeline au moment de sa création. Dans les moteurs de rendu plus complexes, il n'est pas inhabituel de passer un objet QRhiShaderResourceBindings différent, à condition qu'il soit layout-compatible avec celui qui a été donné au moment de la création du pipeline. Il n'y a pas de tampon d'index, et il y a une seule liaison de tampon de vertex (l'élément unique dans vbufBinding se réfère à l'entrée unique dans la liste de liaison de QRhiVertexInputLayout qui a été spécifiée lors de la création du pipeline).
const QColor clearColor = QColor::fromRgbF(0.4f, 0.7f, 0.0f, 1.0f); cb->beginPass(renderTarget(), clearColor, { 1.0f, 0 }, resourceUpdates); cb->setGraphicsPipeline(m_pipeline.get()); const QSize outputSize = renderTarget()->pixelSize(); cb->setViewport(QRhiViewport(0, 0, outputSize.width(), outputSize.height())); cb->setShaderResources(); const QRhiCommandBuffer::VertexInput vbufBinding(m_vbuf.get(), 0); cb->setVertexInput(0, 1, &vbufBinding); cb->draw(3); cb->endPass();
Une fois que la passe de rendu est enregistrée, update() est appelé. Il s'agit d'une demande de nouvelle image, utilisée pour garantir que le widget est continuellement mis à jour et que le triangle apparaît en rotation. Le thread de rendu (le thread principal dans ce cas) est ralenti par le taux de présentation par défaut. Il n'y a pas de système d'animation propre dans cet exemple, et donc la rotation augmentera à chaque image, ce qui signifie que le triangle tournera à des vitesses différentes sur des écrans ayant des taux de rafraîchissement différents.
update(); }
Voir aussi QRhi, Cube RHI Widget Example, et RHI Window Example.
© 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.