Exemple de widget RHI Cube
QRhi Montre comment effectuer le rendu d'un cube texturé et l'intégrer à QPainter et aux widgets, en utilisant l'API 3D et la couche d'abstraction du langage d'ombrage de Qt 3D.

Capture d'écran de l'exemple de widget Cube RHI
Cet exemple s'appuie sur l'exemple simple de widget RHI. Alors que l'exemple simple est intentionnellement minimal et aussi compact que possible, ne rendant qu'un seul triangle sans widgets supplémentaires dans la fenêtre, cette application démontre qu'il est possible d'obtenir des résultats avec des widgets différents dans la fenêtre, ce qui n'est pas le cas ici :
- La présence de divers widgets dans la fenêtre, dont certains contrôlent des données consommées par la sous-classe QRhiWidget.
- Au lieu de demander continuellement des mises à jour, l'application QRhiWidget ne met à jour le contenu de sa texture de soutien que lorsque certaines données connexes changent.
- Le cube est texturé à l'aide d'un QRhiTexture dont le contenu provient d'un QImage qui contient un rendu logiciel effectué à l'aide de QPainter.
- Le contenu de QRhiWidget can be read back est enregistré dans un fichier image (par exemple, un fichier PNG).
- Anti-crénelage 4x multi-échantillon can be toggled au moment de l'exécution. La sous-classe QRhiWidget est préparée pour gérer correctement le changement du nombre d'échantillons.
- Forcer un explicitly specified backing texture size peut être activé dynamiquement et contrôlé par un curseur entre 16x16 et 512x512 pixels.
- La sous-classe QRhiWidget gère correctement le changement de QRhi. On peut le voir en action lorsqu'on rend le widget de premier niveau (pas de parent ; devient une fenêtre séparée) et qu'on le replace ensuite dans la hiérarchie des enfants de la fenêtre principale.
- Plus important encore, certains widgets, même en semi-transparence, peuvent être placés au-dessus de QRhiWidget, ce qui prouve qu'il est possible d'empiler et de mélanger correctement les éléments. C'est un cas où QRhiWidget est supérieur à l'intégration d'une fenêtre native, c'est-à-dire une QWindow basée sur QRhi et utilisant QWidget::createWindowContainer(), parce qu'elle permet l'empilement et l'écrêtage de la même manière que n'importe quelle QWidget ordinaire, créée par logiciel, alors que l'intégration d'une fenêtre native peut, selon la plate-forme, avoir diverses limitations, par exemple, il peut souvent être difficile ou inefficace de placer des contrôles supplémentaires par-dessus.
Dans la réimplémentation de initialize(), la première chose à faire est de vérifier si la dernière version de QRhi avec laquelle nous avons travaillé est toujours à jour et si le nombre d'échantillons (pour l'anticrénelage multi-échantillon) a changé. Le premier point est important parce que toutes les ressources graphiques doivent être libérées lorsque QRhi change, alors qu'avec un nombre d'échantillons changeant dynamiquement, un problème similaire se pose spécifiquement pour les objets QRhiGraphicsPipeline comme ceux qui enregistrent le nombre d'échantillons. Pour simplifier, l'application gère tous ces changements de la même manière, en réinitialisant sa structure scene à une structure par défaut, ce qui permet de libérer toutes les ressources graphiques. Toutes les ressources sont ensuite recréées.
Lorsque la taille de la texture d'appui (donc la taille de la cible de rendu) change, aucune action spéciale n'est nécessaire, mais un signal est émis par commodité, juste pour que main() puisse repositionner l'étiquette d'incrustation. Le nom de l'API 3D est également exposé par le biais d'un signal en interrogeant QRhi::backendName() chaque fois que l'adresse QRhi change.
L'implémentation doit être consciente que l'anticrénelage multi-échantillon implique que colorTexture() est nullptr, alors que msaaColorBuffer() est valide. C'est le contraire lorsque le MSAA n'est pas utilisé. La raison de la différenciation et de l'utilisation de types différents (QRhiTexture, QRhiRenderBuffer) est de permettre l'utilisation de MSAA avec des API graphiques 3D qui ne prennent pas en charge les textures multi-échantillons, mais qui prennent en charge les renderbuffers multi-échantillons. OpenGL ES 3.0 en est un exemple.
Pour vérifier la taille des pixels et le nombre d'échantillons à jour, une solution pratique et compacte est d'interroger QRhiRenderTarget, car de cette façon il n'est pas nécessaire de vérifier si colorTexture() et msaaColorBuffer() sont valides.
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 explicite. 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 en utilisant une projection en perspective. La vue n'est qu'une simple traduction pour l'instant.
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 de l'écriture du tampon uniforme prend également en compte la rotation fournie par l'utilisateur, générant ainsi la matrice finale de projection de la vue du modèle.
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 du site QRhiTexture qui est échantillonné dans le fragment shader lors du rendu du cube est assez simple, même s'il s'y passe beaucoup de choses : tout d'abord, un dessin basé sur QPainter est généré dans un site QImage. Il utilise le texte fourni par l'utilisateur. Ensuite, les données de pixels côté CPU sont téléchargées dans une texture (plus précisément, l'opération de téléchargement est enregistrée sur un QRhiResourceUpdateBatch, qui est ensuite soumis plus tard dans 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 d'uniformes contenant uniquement une matrice 4x4 (16 flottants).
La texture qui contient le dessin généré par QPainter a une taille de 512x512. Notez que toutes les tailles (tailles de texture, fenêtres, ciseaux, régions de téléchargement de texture, etc.) sont toujours exprimées en pixels lorsque vous travaillez avec QRhi. Pour échantillonner cette texture dans le shader, une adresse sampler object est nécessaire (indépendamment du fait que les applications basées sur QRhi utiliseront généralement des échantillonneurs d'images combinés dans le code du shader GLSL, qui peuvent ensuite être transposés en objets de texture et d'échantillonnage séparés avec certains langages d'ombrage, ou peuvent rester un objet de texture-échantillonneur combiné avec d'autres, ce qui signifie qu'il peut ne pas y avoir réellement d'objet d'échantillonneur natif sous le capot au moment de l'exécution, en fonction de l'API 3D, mais tout cela est transparent pour l'application).
Le nuanceur de sommets lit le tampon uniforme au point de liaison 0, ce qui signifie que scene.ubuf est exposé à ce point de liaison. Le nuanceur de fragments échantillonne une texture fournie au point de liaison 1, une paire combinée texture-échantillonneur est donc spécifiée pour cet emplacement de liaison.
Le site QRhiGraphicsPipeline permet le test/écriture de la profondeur et élimine les backfaces. Il s'appuie également sur un certain nombre de valeurs par défaut, par exemple la fonction de comparaison de profondeur est par défaut Less, ce qui nous convient, et le mode de la face avant est le sens inverse des aiguilles d'une montre, ce qui est également bon en l'état et n'a donc pas besoin d'être défini à nouveau.
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 le site QSlider contrôlant la rotation a fourni une nouvelle valeur ou si le site 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, une seule passe de rendu avec un seul appel de dessin est enregistrée. 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 vertex, l'une pour les positions (x, y, z) et l'autre pour les UV (u, v), avec un décalage de départ correspondant à 36 paires de flotteurs 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. main() se connecte au signal valueChanged de QSlider. Lorsqu'il est émis, le lamda connecté appelle setCubeRotation() sur le ExampleRhiWidget. Ici, si la valeur est différente de la précédente, elle est stockée et un drapeau "dirty" est activé. Ensuite, et c'est le plus important, update() est appelé sur le ExampleRhiWidget. C'est ce qui déclenche le rendu d'une nouvelle image dans la texture d'arrière-plan de QRhiWidget. Sans cela, le contenu du ExampleRhiWidget ne serait pas mis à jour lorsque l'on fait glisser le 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(); }
Voir aussi 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.