Exemple simple de widget RHI
Montre comment afficher un triangle à l’aide d’ QRhi, l’API Qt 3D et la couche d’abstraction du langage de shaders.

Capture d'écran de l'exemple de widget RHI simple
Cet exemple est, à bien des égards, l'équivalent de l'exemple « RHI Window » dans l'univers d'QWidget. La sous-classe ` QRhiWidget ` de cette application affiche un seul triangle, à l’aide d’un pipeline graphique simple avec des shaders de sommets et de fragments de base. Contrairement à une application classique basée sur ` QWindow`, cet exemple ne nécessite pas de se soucier des détails de bas niveau, tels que la configuration de la fenêtre et de l’` QRhi`, ni de la gestion de la chaîne d’échange (swapchain) et des événements de fenêtre, car tout cela est pris en charge par le framework ` QWidget ` ici. L’instance de la sous-classe ` QRhiWidget ` est ajoutée à un ` QVBoxLayout`. Afin de garder cet exemple simple et concis, aucun autre widget ni contenu 3D n’est introduit.
Dès qu’une instance d’ ExampleRhiWidget, une sous-classe de QRhiWidget, est ajoutée à la hiérarchie des enfants d’un widget de niveau supérieur, la fenêtre correspondante devient automatiquement une fenêtre rendue en Direct 3D, Vulkan, Metal ou OpenGL. Le contenu du widget rendu par QPainter, c’est-à-dire tout ce qui n’est pas un QRhiWidget, QOpenGLWidget ou QQuickWidget, est ensuite chargé dans une texture, tandis que les widgets spéciaux mentionnés sont chacun rendus dans une texture. L’ensemble résultant de textures est composite par le backingstore du widget de niveau supérieur.
Structure et fonction main()
La fonction ` main() ` est assez simple. Le widget de niveau supérieur a par défaut une taille de 720p (cette taille est exprimée en unités logiques ; la taille réelle en pixels peut varier en fonction de ` scale factor`). La fenêtre est redimensionnable. ` QRhiWidget ` facilite l’implémentation de sous-classes qui gèrent correctement le redimensionnement du widget en fonction des changements de taille de la fenêtre ou de la mise en page.
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éimplémente les deux méthodes virtuelles : initialize() et render(). La méthode `initialize()` est appelée au moins une fois avant `render()`, mais elle est également invoquée lors d'un certain nombre de changements importants, par exemple lorsque la texture de fond 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 à une nouvelle ` QRhi ` suite à son déplacement vers une nouvelle fenêtre de niveau supérieur.
Remarque : contrairement à l’ancien modèle « initializeGL - resizeGL - paintGL » d’ QOpenGLWidget, il n’y a que deux méthodes virtuelles dans QRhiWidget. Cela s’explique par le fait qu’il existe davantage d’événements particuliers nécessitant une gestion que le simple redimensionnement, par exemple lors d’un changement de fenêtre de niveau supérieur. (Les implémentations robustes de ` QOpenGLWidget ` devaient gérer cela en effectuant un suivi supplémentaire, par exemple en suivant la durée de vie de l’` QOpenGLContext ` associé, ce qui signifie que les trois méthodes virtuelles n’étaient en réalité pas suffisantes.) Une paire plus simple, composée de ` initialize ` et ` render`, où ` initialize ` est réappelé lors de changements importants, est mieux adaptée à cette situation.
L’instance QRhi n’appartient pas au widget. Elle sera interrogée dans initialize() from the base class . Son stockage en tant que membre permet de détecter les modifications lorsque initialize() est réappelé. Les ressources graphiques, telles que les tampons de sommets et 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). Consultez 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 est fournie avec des fichiers ` .qsb ` pré-conditionnés, intégrés à l'exécutable via le système de ressources Qt. En raison des dépendances entre modules (et de la prise en charge toujours active de qmake), cet exemple n'utilise pas la fonction CMake pratique ` qt_add_shaders()`, mais est plutôt fourni avec les fichiers ` .qsb ` intégrés à l'arborescence des sources. Dans les applications réelles, il est recommandé d’éviter cette approche et d’utiliser plutôt les fonctionnalités d’intégration CMake du module Qt Shader Tools (qt_add_shaders). Quelle que soit l’approche choisie, le chargement des fichiers .qsb intégrés ou générés s’effectue de la même manière dans le code C++.
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 la comparaison lors des futurs appels de la fonction. En cas de divergence (par exemple lorsque le widget est déplacé d’une fenêtre à l’autre), la recréation des ressources graphiques nécessaires est déclenchée en détruisant et en remettant à zéro un objet approprié, en l’occurrence l’ m_pipeline. L’exemple ne montre pas activement le changement de parent entre les fenêtres, mais il est prêt à le gérer. Il est également prêt à gérer un changement de taille du widget pouvant survenir lors du redimensionnement de la fenêtre. Cela ne nécessite aucune gestion particulière, puisque ` initialize() ` est appelé à chaque fois que cela se produit ; ainsi, l'interrogation de ` renderTarget()->pixelSize() ` ou ` colorTexture()->pixelSize() ` renvoie toujours la taille la plus récente et à 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 utilise toujours et uniquement les valeurs par défaut (RGBA8 et pas d’anticrénelage multisample).
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() s'en charge à l'aide d'un code assez classique basé sur l'QRhi. Un seul tampon de sommets contenant les données de sommets entrelacées (position-couleur) suffit, tandis que la matrice de projection modèle-vue 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 sommet. Le pipeline graphique repose sur de nombreux paramètres par défaut (par exemple, test de profondeur désactivé, fusion désactivée, écriture de couleur activée, culling des faces désactivé, topologie par défaut des triangles, etc.) La structure des données de sommets est la suivante : x, y, r, g, b. Le pas est donc de 5 flottants, tandis que le deuxième attribut d’entrée du sommet (la couleur) présente un décalage de 2 flottants (en sautant x et y). Chaque pipeline graphique doit être associé à un QRhiRenderPassDescriptor. Celui-ci peut être récupéré à partir de l’ QRhiRenderTarget géré par la classe de base.
Remarque : cet exemple repose sur la valeur par défaut de l’ QRhiWidget, à savoir autoRenderTarget, définie sur true. C’est pourquoi il n’a pas besoin de gérer la cible de rendu, mais peut simplement interroger celle qui existe 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. Celle-ci dépend de la taille du widget et est donc calculée systématiquement à chaque appel des fonctions.
Remarque : tous les calculs de taille et de fenêtre d’affichage doivent toujours se baser uniquement sur la taille en pixels interrogée à partir de la ressource servant de tampon de couleur, car il s’agit de la cible de rendu réelle. Évitez de calculer manuellement les tailles, les fenêtres d’affichage, les ciseaux, etc. en vous basant sur la taille indiquée par l’ QWidget ou le rapport de pixels de l’appareil.
Remarque : la matrice de projection inclut l’ correction matrix de QRhi afin de tenir compte des différences entre les API 3D en matière de coordonnées normalisées de l’appareil (par exemple, Y vers le bas vs Y vers le haut).
Une translation de l’ -4 est appliquée uniquement pour s’assurer que le triangle dont les valeurs d’ z sont égales à 0 soit 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 un seul passage de rendu, qui contient un seul appel de dessin.
La matrice de projection de vue calculée lors de l’initialisation est combinée à la matrice de modèle, qui, dans ce cas précis, correspond à une simple rotation. La matrice résultante est ensuite écrite dans le tampon d’uniformes. Notez que ` resourceUpdates ` est transmis à ` beginPass()`, ce qui constitue un raccourci permettant d’éviter d’appeler manuellement ` resourceUpdate()`.
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 le passage 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 d’affichage est configurée pour couvrir l’intégralité du widget. Pour rendre le tampon d’uniformes visible par le shader (de sommets), la fonction setShaderResources() est appelée sans argument, ce qui implique l’utilisation de l’objet m_srb, puisque celui-ci a été associé au pipeline lors de sa création. Dans les moteurs de rendu plus complexes, il n’est pas rare de passer un objet ` QRhiShaderResourceBindings ` différent, à condition qu’il soit en layout-compatible avec celui fourni lors de la création du pipeline. Il n’y a pas de tampon d’index, et il n’y a qu’une seule liaison de tampon de sommets (l’élément unique dans ` vbufBinding ` fait référence à l’entrée unique dans la liste de liaisons de l’objet ` 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 la passe de rendu enregistrée, la fonction update() est appelée. Cela demande une nouvelle image et sert à garantir que le widget se mette à jour en continu, de sorte que le triangle apparaisse en rotation. Le thread de rendu (le thread principal dans ce cas) est limité par la fréquence de rafraîchissement par défaut. Cet exemple ne comporte pas de système d'animation à proprement parler ; la rotation s'accélère donc à chaque image, ce qui signifie que le triangle tournera à des vitesses différentes selon les écrans et leurs fréquences de rafraîchissement.
update();
}Voir également QRhi, Exemple de widget Cube RHI 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.