Einfaches RHI Widget Beispiel
Zeigt, wie ein Dreieck mit QRhi, der 3D-API von Qt und der Abstraktionsschicht der Schattierungssprache gerendert wird.
Screenshot des einfachen RHI Widget Beispiels
Dieses Beispiel ist in vielerlei Hinsicht das Gegenstück zum RHI Window Example in der QWidget Welt. Die Unterklasse QRhiWidget in dieser Anwendung rendert ein einzelnes Dreieck und verwendet eine einfache Grafikpipeline mit einfachen Vertex- und Fragment-Shadern. Im Gegensatz zur einfachen QWindow-basierten Anwendung muss sich dieses Beispiel nicht um Details auf niedrigerer Ebene kümmern, wie z. B. das Einrichten des Fensters und der QRhi oder den Umgang mit Swapchain- und Fensterereignissen, da dies hier vom QWidget Framework übernommen wird. Die Instanz der Unterklasse QRhiWidget wird zu einer QVBoxLayout hinzugefügt. Um das Beispiel minimal und kompakt zu halten, werden keine weiteren Widgets oder 3D-Inhalte eingeführt.
Sobald eine Instanz von ExampleRhiWidget
, einer Unterklasse von QRhiWidget, der untergeordneten Hierarchie eines Widgets der obersten Ebene hinzugefügt wird, wird das entsprechende Fenster automatisch zu einem Direct 3D-, Vulkan-, Metal- oder OpenGL-gerenderten Fenster. Der QPainter-gerenderte Widget-Inhalt, d.h. alles, was kein QRhiWidget, QOpenGLWidget oder QQuickWidget ist, wird dann in eine Textur hochgeladen, während die genannten speziellen Widgets jeweils in eine Textur rendern. Der resultierende Satz von textures wird durch den Backingstore des Widgets der obersten Ebene zusammengesetzt.
Aufbau und main()
Die Funktion main()
ist recht einfach. Das Top-Level-Widget ist standardmäßig auf eine Größe von 720p eingestellt (diese Größe ist in logischen Einheiten, die tatsächliche Pixelgröße kann je nach scale factor unterschiedlich sein). Das Fenster ist größenveränderbar. QRhiWidget macht es einfach, Unterklassen zu implementieren, die korrekt mit der Größenveränderung des Widgets aufgrund von Änderungen der Fenstergröße oder des Layouts umgehen.
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(); }
Die Unterklasse QRhiWidget implementiert die beiden Virtuals: initialize() und render(). initialize() wird mindestens einmal vor render() aufgerufen, wird aber auch bei einer Reihe von wichtigen Änderungen aufgerufen, z. B. wenn die Hintergrundtextur des Widgets aufgrund einer sich ändernden Größe des Widgets neu erstellt wird, wenn sich die Rendering-Zielparameter ändern oder wenn das Widget aufgrund des Wechsels zu einem neuen Top-Level-Fenster zu einem neuen QRhi wechselt.
Hinweis: Im Gegensatz zu QOpenGLWidget's Legacy initializeGL
- resizeGL
- paintGL
Modell, gibt es nur zwei Virtuals in QRhiWidget. Der Grund dafür ist, dass es mehr spezielle Ereignisse gibt, die möglicherweise berücksichtigt werden müssen als nur die Größenänderung, z.B. beim Reparenting zu einem anderen Top-Level-Fenster. (robuste QOpenGLWidget Implementierungen mussten dies durch zusätzliche Buchhaltung bewältigen, z.B. durch die Verfolgung der zugehörigen QOpenGLContext Lebensdauer, was bedeutet, dass die drei Virtuals nicht wirklich ausreichend waren) Ein einfacheres Paar von initialize
- render
, wo initialize
bei wichtigen Änderungen erneut aufgerufen wird, ist dafür besser geeignet.
Die QRhi Instanz ist nicht im Besitz des Widgets. Sie wird in initialize()
from the base class abgefragt. Die Speicherung als Mitglied ermöglicht es, Änderungen zu erkennen, wenn initialize()
erneut aufgerufen wird. Grafikressourcen wie die Vertex- und Uniform-Buffer oder die Grafikpipeline stehen jedoch unter der Kontrolle von 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; };
Damit die Anweisung #include <rhi/qrhi.h>
funktioniert, muss die Anwendung auf GuiPrivate
(oder gui-private
mit qmake) verweisen. Siehe QRhi für weitere Details über das Kompatibilitätsversprechen der QRhi Familie von APIs.
CMakeLists.txt
target_link_libraries(simplerhiwidget PRIVATE Qt6::Core Qt6::Gui Qt6::GuiPrivate Qt6::Widgets )
Rendering-Einrichtung
In examplewidget.cpp
verwendet die Widget-Implementierung eine Hilfsfunktion, um ein QShader Objekt aus einer .qsb
Datei zu laden. Diese Anwendung liefert vorkonditionierte .qsb
Dateien, die über das Qt Resource System in die ausführbare Datei eingebettet sind. Aufgrund von Modulabhängigkeiten (und weil qmake immer noch unterstützt wird), verwendet dieses Beispiel nicht die praktische CMake-Funktion qt_add_shaders()
, sondern wird mit den .qsb
Dateien als Teil des Quellbaums geliefert. Für Anwendungen in der realen Welt wird empfohlen, dies zu vermeiden und stattdessen die CMake-Integrationsfunktionen des Moduls Qt Shader Tools zu verwenden (qt_add_shaders
). Ungeachtet des Ansatzes ist das Laden der gebündelten/generierten .qsb
Dateien im C++-Code gleich.
static QShader getShader(const QString &name) { QFile f(name); return f.open(QIODevice::ReadOnly) ? QShader::fromSerialized(f.readAll()) : QShader(); }
Schauen wir uns die initialize()-Implementierung an. Zunächst wird das Objekt QRhi abgefragt und für die spätere Verwendung gespeichert, auch um einen Vergleich bei künftigen Aufrufen der Funktion zu ermöglichen. Wenn eine Nichtübereinstimmung vorliegt (z. B. wenn das Widget zwischen Fenstern verschoben wird), wird die Wiederherstellung der Grafikressourcen, die neu erstellt werden müssen, durch das Zerstören und Löschen eines geeigneten Objekts ausgelöst, in diesem Fall des m_pipeline
. Das Beispiel demonstriert nicht aktiv das Reparenting zwischen Fenstern, ist aber darauf vorbereitet. Es ist auch darauf vorbereitet, mit einer sich ändernden Widgetgröße umzugehen, die bei einer Größenänderung des Fensters auftreten kann. Dies erfordert keine besondere Behandlung, da initialize()
jedes Mal aufgerufen wird, wenn dies geschieht, und die Abfrage von renderTarget()->pixelSize()
oder colorTexture()->pixelSize()
immer die neueste, aktuelle Größe in Pixeln liefert. Worauf dieses Beispiel nicht vorbereitet ist, ist das Ändern von Texturformaten und multisample settings, da es immer nur die Standardeinstellungen verwendet (RGBA8 und kein Multisample-Antialiasing).
void ExampleRhiWidget::initialize(QRhiCommandBuffer *cb) { if (m_rhi != rhi()) { m_pipeline.reset(); m_rhi = rhi(); }
Wenn die Grafikressourcen (neu) erstellt werden müssen, tut initialize()
dies mit ganz typischem QRhi-basiertem Code. Ein einzelner Scheitelpunktpuffer mit den verschachtelten Positions-Farb-Scheitelpunktdaten ist ausreichend, während die Modellansichts-Projektionsmatrix über einen einheitlichen Puffer von 64 Byte (16 Fließkommazahlen) ausgegeben wird. Der Uniform-Buffer ist die einzige für den Shader sichtbare Ressource und wird nur im Vertex-Shader verwendet. Die Grafikpipeline stützt sich auf viele Standardeinstellungen (z. B. Tiefentest aus, Überblenden deaktiviert, Farbschreiben aktiviert, Flächen-Culling deaktiviert, Standardtopologie der Dreiecke usw.) Das Layout der Scheitelpunktdaten ist x
, y
, r
, g
, b
, daher beträgt der Stride 5 Floats, während das zweite Scheitelpunkt-Eingangsattribut (die Farbe) einen Offset von 2 Floats hat (Überspringen von x
und y
). Jede Grafikpipeline muss mit einer QRhiRenderPassDescriptor verknüpft werden. Diese kann von der QRhiRenderTarget, die von der Basisklasse verwaltet wird, abgerufen werden.
Hinweis: Dieses Beispiel stützt sich auf die Vorgabe von QRhiWidget für autoRenderTarget, die auf true
gesetzt ist. Deshalb muss das Rendering-Ziel nicht verwaltet werden, sondern kann das vorhandene Ziel einfach durch den Aufruf von renderTarget() abgefragt werden.
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); }
Schließlich wird die Projektionsmatrix berechnet. Diese hängt von der Widgetgröße ab und wird daher bei jedem Aufruf der Funktionen bedingungslos durchgeführt.
Hinweis: Jegliche Größen- und Viewport-Berechnungen sollten sich immer nur auf die Pixelgröße stützen, die von der als Farbpuffer dienenden Ressource abgefragt wird, da dies das eigentliche Rendering-Ziel ist. Vermeiden Sie die manuelle Berechnung von Größen, Ansichtsfenstern, Scheren usw. auf der Grundlage der von QWidget gemeldeten Größe oder des Gerätepixelverhältnisses.
Hinweis: Die Projektionsmatrix enthält die correction matrix von QRhi, um 3D-API-Unterschiede in normalisierten Gerätekoordinaten zu berücksichtigen. (z.B. Y nach unten vs. Y nach oben)
Eine Verschiebung von -4
wird nur angewandt, um sicherzustellen, dass das Dreieck mit z
Werten von 0 sichtbar sein wird.
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); }
Rendering
Das Widget zeichnet einen einzigen Rendering-Durchgang auf, der einen einzigen Zeichenaufruf enthält.
Die im Initialisierungsschritt berechnete Ansichtsprojektionsmatrix wird mit der Modellmatrix kombiniert, die in diesem Fall eine einfache Drehung ist. Die resultierende Matrix wird dann in den einheitlichen Puffer geschrieben. Beachten Sie, dass resourceUpdates
an beginPass() übergeben wird, was eine Abkürzung ist, um resourceUpdate() nicht manuell aufrufen zu müssen.
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());
Im Renderpass wird ein einzelner Zeichenaufruf mit 3 Vertices aufgezeichnet. Die im Initialisierungsschritt erstellte Grafikpipeline wird an den Befehlspuffer gebunden, und das Ansichtsfenster wird so eingestellt, dass es das gesamte Widget abdeckt. Um den einheitlichen Puffer für den (Scheitelpunkt-)Shader sichtbar zu machen, wird setShaderResources() ohne Argument aufgerufen, was bedeutet, dass die m_srb
verwendet wird, da diese bei der Erstellung der Pipeline mit der Pipeline verbunden wurde. In komplexeren Renderern ist es nicht unüblich, ein anderes QRhiShaderResourceBindings Objekt zu übergeben, solange dieses layout-compatible mit dem bei der Pipeline-Erstellung angegebenen Objekt übereinstimmt. Es gibt keinen Indexpuffer und eine einzelne Scheitelpunktpufferbindung (das einzelne Element in vbufBinding
bezieht sich auf den einzelnen Eintrag in der Bindungsliste des QRhiVertexInputLayout, der bei der Erstellung der Pipeline angegeben wurde).
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();
Sobald der Render-Durchgang aufgezeichnet ist, wird update() aufgerufen. Dies fordert einen neuen Frame an und wird verwendet, um sicherzustellen, dass das Widget kontinuierlich aktualisiert wird und das Dreieck rotierend erscheint. Der Rendering-Thread (in diesem Fall der Hauptthread) wird standardmäßig durch die Darstellungsrate gedrosselt. In diesem Beispiel gibt es kein richtiges Animationssystem, so dass die Drehung in jedem Frame zunimmt, was bedeutet, dass sich das Dreieck auf Displays mit unterschiedlichen Bildwiederholraten unterschiedlich schnell dreht.
update(); }
Siehe auch QRhi, Cube RHI Widget Beispiel, und RHI Window Beispiel.
© 2025 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.