Schaubild - RHI unter QML
Zeigt, wie man direkt mit QRhi unter einer Qt Quick Szene rendert.
Einführung
Das RHI Under QML-Beispiel zeigt, wie eine Anwendung die Signale QQuickWindow::beforeRendering() und QQuickWindow::beforeRenderPassRecording() nutzen kann, um benutzerdefinierten QRhi-basierten Inhalt unter einer Qt Quick -Szene zu zeichnen.
Anwendungen, die QRhi Inhalte über der Qt Quick Szene darstellen möchten, können dies tun, indem sie sich mit den Signalen QQuickWindow::afterRendering() und QQuickWindow::afterRenderPassRecording() verbinden.
In diesem Beispiel werden wir auch sehen, wie es möglich ist, Werte zu haben, die QML ausgesetzt sind und das QRhi-basierte Rendering beeinflussen. Wir animieren den Schwellenwert mit einem NumberAnimation in der QML-Datei und dieser Float-Wert wird dann in einem einheitlichen Puffer an den Fragment-Shader weitergegeben.
Das Beispiel ist in den meisten Punkten äquivalent zu den Beispielen OpenGL unter QML, Direct3D 11 unter QML, Metal unter QML und Vulkan unter QML. Diese Beispiele rendern denselben Inhalt durch direkte Verwendung einer 3D-API. Dieses Beispiel hingegen ist vollständig plattformübergreifend und portabel, da es von Haus aus den Betrieb mit allen von QRhi unterstützten 3D-APIs (wie OpenGL, Vulkan, Metal, Direct 3D 11 und 12) unterstützt.
Hinweis: Dieses Beispiel demonstriert eine fortgeschrittene Low-Level-Funktionalität, die portables, plattformübergreifendes 3D-Rendering durchführt und dabei auf APIs mit begrenzter Kompatibilitätsgarantie des Qt Gui-Moduls zurückgreift. Um die APIs von QRhi nutzen zu können, verlinkt die Anwendung auf Qt::GuiPrivate
und schließt <rhi/qrhi.h>
ein.
Das Hinzufügen von benutzerdefiniertem Rendering als Underlay/Overlay ist eine der drei Möglichkeiten, benutzerdefiniertes 2D/3D-Rendering in eine Qt Quick Szene zu integrieren. Die beiden anderen Optionen sind, das Rendering "inline" mit dem eigenen Rendering der Szene Qt Quick unter Verwendung von QSGRenderNode durchzuführen oder einen separaten Rendering-Durchgang zu erzeugen, der ein spezielles Rendering-Ziel (eine Textur) anvisiert, und dann ein Element in der Szene die Textur anzeigen zu lassen. Siehe die Beispiele Scene Graph - RHI Texture Item und Scene Graph - Custom QSGRenderNode für diese Ansätze.
Kernkonzepte
Das Signal beforeRendering() wird zu Beginn eines jeden Frames ausgegeben, bevor der Szenegraph mit dem Rendering beginnt. Daher werden alle QRhi Zeichenaufrufe, die als Reaktion auf dieses Signal erfolgen, unter den Qt Quick Elementen gestapelt. Es gibt jedoch zwei Signale, die hier von Bedeutung sind: Die eigenen QRhi Befehle der Anwendung sollten im selben Befehlspuffer aufgezeichnet werden, der auch vom Szenengraphen verwendet wird, und außerdem sollten die Befehle zum selben Rendering-Pass gehören. beforeRendering() allein reicht dafür nicht aus, da es zu Beginn des Frames ausgegeben wird, bevor die Aufzeichnung eines Rendering-Passes über QRhiCommandBuffer::beginPass() beginnt. Indem man sich auch mit beforeRenderPassRecording() verbindet, werden die eigenen Befehle der Anwendung und das eigene Rendering des Szenegraphen in die richtige Reihenfolge gebracht:
- Die Renderschleife des Szenegraphen ruft QRhi::beginFrame() auf.
- QQuickWindow::beforeRendering() wird ausgegeben - die Anwendung bereitet die Ressourcen für ihr eigenes Rendering vor
- Der Szenegraph ruft QRhiCommandBuffer::beginPass()
- QQuickWindow::beforeRenderPassRecording() wird emittiert - die Anwendung zeichnet Zeichenaufrufe auf
- Der Szenegraph zeichnet Zeichenaufrufe auf
Exkursion
Das benutzerdefinierte Rendering ist in einer benutzerdefinierten QQuickItem gekapselt. RhiSquircle
leitet sich von QQuickItem ab und ist QML ausgesetzt (beachten Sie das QML_ELEMENT
). Die QML-Szene instanziiert RhiSquircle
. Beachten Sie jedoch, dass es sich hierbei nicht um ein visuelles Element handelt: Das Flag QQuickItem::ItemHasContents ist nicht gesetzt. Daher sind Position und Größe des Elements nicht relevant, und es wird nicht updatePaintNode() reimplementiert.
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; };
Wenn das Element mit einem QQuickWindow verknüpft wird, wird es stattdessen mit dem Signal QQuickWindow::beforeSynchronizing() verbunden. Die Verwendung von Qt::DirectConnection ist wichtig, da dieses Signal auf dem Qt Quick Render-Thread ausgegeben wird, falls es einen gibt. Wir möchten, dass der verbundene Slot auf demselben Thread aufgerufen wird.
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); } }
In der Synchronisierungsphase des Szenengraphen wird die Rendering-Infrastruktur erstellt, falls dies noch nicht geschehen ist, und die für das Rendering relevanten Daten werden synchronisiert, d. h. von dem Element RhiSquircle
, das sich auf dem Haupt-Thread befindet, auf das Objekt SquircleRenderer
kopiert, das sich auf dem Render-Thread befindet. (wenn es keinen Render-Thread gibt, leben beide Objekte auf dem Haupt-Thread) Der Zugriff auf die Daten ist sicher, da der Haupt-Thread blockiert ist, während der Render-Thread seine Synchronisierungsphase durchführt. Weitere Informationen über das Threading- und Rendering-Modell des Szenegraphs finden Sie unter Qt Quick Scene Graph.
Zusätzlich zu dem Wert von t
wird auch der zugehörige Zeiger QQuickWindow kopiert. Zwar könnte SquircleRenderer
window () das Element RhiSquircle
auch dann abfragen, wenn es auf dem Render-Thread läuft, doch ist dies theoretisch nicht völlig sicher. Daher wird eine Kopie erstellt.
Beim Einrichten von SquircleRenderer
werden Verbindungen zu beforeRendering() und beforeRenderPassRecording() hergestellt, die der Schlüssel sind, um die benutzerdefinierten 3D-Rendering-Befehle der Anwendung zum richtigen Zeitpunkt zu aktivieren und zu injizieren.
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()); }
Bei der Ausgabe von beforeRendering() werden die für das benutzerdefinierte Rendering benötigten QRhi Ressourcen wie QRhiBuffer, QRhiGraphicsPipeline und verwandte Objekte erstellt, sofern dies noch nicht geschehen ist.
Die Daten in den Puffern werden mit QRhiResourceUpdateBatch und QRhiCommandBuffer::resourceUpdate() aktualisiert (genauer gesagt, die Datenaktualisierungsvorgänge werden in die Warteschlange gestellt). Der Scheitelpunktpuffer ändert seinen Inhalt nicht mehr, sobald die ersten Scheitelpunkte in ihn hochgeladen wurden. Der einheitliche Puffer hingegen ist ein dynamic Puffer, wie es für solche Puffer typisch ist. Sein Inhalt, zumindest einige Regionen, wird für jeden Frame aktualisiert. Daher der bedingungslose Aufruf von updateDynamicBuffer() für Offset 0 und eine Bytegröße von 4 (was sizeof(float)
ist, da der C++-Typ float
zufällig dem 32-Bit-Typ von GLSL float
entspricht). Was an dieser Position gespeichert wird, ist der Wert von t
, und der wird in jedem Frame aktualisiert, also bei jedem Aufruf von frameStart().
Es gibt einen zusätzlichen Float-Wert im Puffer, der bei Offset 4 beginnt. Dieser wird verwendet, um den Unterschieden im Koordinatensystem der 3D-APIs Rechnung zu tragen: Wenn isYUpInNDC() den Wert false
zurückgibt, was insbesondere bei Vulkan der Fall ist, wird der Wert auf -1,0 gesetzt, was dazu führt, dass der Y-Wert im 2-Komponenten-Vektor, der (mit Interpolation) an den Fragment-Shader weitergegeben wird und auf dessen Grundlage die Farbe berechnet wird, gespiegelt wird. Auf diese Weise ist die Ausgabe auf dem Bildschirm identisch (d.h. die linke obere Ecke ist grünlich, die linke untere rot), unabhängig davon, welche 3D-API verwendet wird. Dieser Wert wird nur einmal im einheitlichen Puffer aktualisiert, ähnlich wie der Vertex-Puffer. Dies verdeutlicht ein Problem, mit dem sich Low-Level-Rendering-Code, der portabel sein soll, oft auseinandersetzen muss: die Unterschiede im Koordinatensystem in normalisierten Gerätekoordinaten (NDC) und in Bildern und Framebuffern. Zum Beispiel verwendet das NDC überall außer in Vulkan ein System mit dem Ursprung unten links. Framebuffer hingegen verwenden überall außer bei OpenGL ein System mit dem Ursprung oben links. Typische Renderer, die mit einer perspektivischen Projektion arbeiten, können dieses Problem oft ignorieren, indem sie sich bequem auf QRhi::clipSpaceCorrMatrix() verlassen, das eine Matrix ist, die mit der Projektionsmatrix multipliziert werden kann und sowohl einen Y-Flip anwendet, wenn er benötigt wird, als auch der Tatsache Rechnung trägt, dass die Clip-Space-Tiefe -1..1
mit OpenGL, aber 0..1
überall sonst läuft. In einigen Fällen, wie in diesem Beispiel, ist dies jedoch nicht anwendbar. Stattdessen müssen die Anwendung und die Shader-Logik die notwendigen Anpassungen der Vertex- und UV-Positionen auf der Grundlage der Abfrage von QRhi::isYUpInNDC() und QRhi::isYUpInFramebuffer() vornehmen.
Um Zugriff auf die Objekte QRhi und QRhiSwapChain zu erhalten, die Qt Quick verwendet, können sie einfach von QQuickWindow aus abgefragt werden. Beachten Sie, dass dies voraussetzt, dass QQuickWindow ein reguläres Bildschirmfenster ist. Wenn es stattdessen QQuickRenderControl verwendet, z.B. um Off-Screen-Rendering in eine Textur durchzuführen, wäre die Abfrage der Swapchain falsch, da es dann keine Swapchain gibt.
Aufgrund des Signals, das ausgegeben wird, nachdem Qt Quick QRhi::beginFrame () aufruft, ist es bereits möglich, den Befehlspuffer und das Renderziel aus der Swapchain abzufragen. Dies ermöglicht es, bequem ein QRhiCommandBuffer::resourceUpdate() auf das von QRhiSwapChain::currentFrameCommandBuffer() zurückgegebene Objekt anzuwenden. Bei der Erstellung einer Grafikpipeline kann ein QRhiRenderPassDescriptor von dem QRhiRenderTarget abgerufen werden, das von QRhiSwapChain::currentFrameRenderTarget() zurückgegeben wird. (Beachten Sie, dass die hier erstellte Grafikpipeline nur für das Rendern in die Swapchain oder bestenfalls in ein anderes Rendering-Ziel geeignet ist, das mit der Swapchain compatible verbunden ist; wenn wir in eine Textur rendern wollten, wäre wahrscheinlich eine andere QRhiRenderPassDescriptor und damit eine andere Grafikpipeline erforderlich, da sich die Textur- und Swapchain-Formate unterscheiden können)
void SquircleRenderer::frameStart() { // Diese Funktion wird auf dem Render-Thread aufgerufen, falls es einen gibt. 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 Floatsm_uniformBuffer.reset(rhi->newBuffer(QRhiBuffer::Dynamic, 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); }
Schließlich wird auf QQuickWindow::beforeRenderPassRecording() ein Zeichnungsaufruf für einen Dreiecksstreifen mit 4 Scheitelpunkten aufgezeichnet. In diesem Beispiel wird in der Praxis einfach ein Viereck gezeichnet und die Pixelfarben mit Hilfe der Logik in den Fragment-Shadern berechnet, aber es steht den Anwendungen frei, kompliziertere Zeichnungen vorzunehmen: Das Erstellen mehrerer Grafikpipelines und das Aufzeichnen mehrerer Zeichenaufrufe ist ebenfalls völlig in Ordnung. Wichtig ist, dass alles, was auf der QRhiCommandBuffer aufgezeichnet wird, die von der swapchain des Fensters abgerufen wird, dem eigenen Rendering des Qt Quick Szenegraphen im Haupt-Rendering-Durchgang vorangestellt wird.
Hinweis: Das bedeutet, dass der Inhalt von Qt Quick von den in den Tiefenpuffer geschriebenen Werten beeinflusst werden kann, wenn Tiefenpuffer mit Tiefenprüfung und dem Schreiben von Tiefenwerten verwendet werden. Siehe Qt Quick Scene Graph Default Renderer für Details zum Renderer des Szenegraphen, insbesondere die Abschnitte über die Behandlung von opaken und Alpha-überblendeten Primitiven.
Um die Fenstergröße in Pixeln zu erhalten, wird QRhiRenderTarget::pixelSize() verwendet. Dies ist praktisch, weil das Beispiel auf diese Weise die Größe des Ansichtsfensters nicht auf andere Weise berechnen muss und sich nicht um die Anwendung der high DPI scale factor kümmern muss.
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); }
Die Vertex- und Fragment-Shader durchlaufen die standardmäßige QRhi Shader-Konditionierungspipeline. Ursprünglich als Vulkan-kompatibles GLSL geschrieben, werden sie nach SPIR-V kompiliert und dann von den Qt-Tools in andere Shading-Sprachen transponiert. Bei der Verwendung von CMake stützt sich das Beispiel auf den Befehl qt_add_shaders
, der es einfach und bequem macht, die Shader mit der Anwendung zu bündeln und die notwendige Verarbeitung zur Build-Zeit durchzuführen. Siehe Qt Shader Tools Build System Integration für weitere Details.
Die Angabe von BASE
hilft, das Präfix ../shared
zu entfernen, während PREFIX
das beabsichtigte Präfix /scenegraph/rhiunderqml
hinzufügt. Der endgültige Pfad lautet also :/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 )
Um qmake zu unterstützen, liefert das Beispiel immer noch die .qsb
Dateien aus, die normalerweise zur Build-Zeit erzeugt werden, und listet sie in der qrc-Datei auf. Dieser Ansatz wird jedoch nicht für neue Anwendungen empfohlen, die CMake als Build-System verwenden.
Siehe auch Scene Graph - RHI Texture Item und Scene Graph - Custom QSGRenderNode.
© 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.