Szenengraph – RHI unter QML
Zeigt, wie man mit „ QRhi “ direkt in einer „ Qt Quick “-Szene rendert.

Einführung
Das Beispiel „RHI Under QML“ zeigt, wie eine Anwendung die Signale „ QQuickWindow::beforeRendering()“ und „ QQuickWindow::beforeRenderPassRecording()“ nutzen kann, um benutzerdefinierte, auf „ QRhi “ basierende Inhalte in einer „ Qt Quick “-Szene darzustellen.
Anwendungen, die „ QRhi “-Inhalte über der „ Qt Quick “-Szene darstellen möchten, sollten „ QQuickWindow::beforeRendering()“ verwenden, um Daten in Puffer zu laden, und eine Verbindung zum Signal „ QQuickWindow::afterRenderPassRecording()“ herstellen.
In diesem Beispiel werden wir außerdem sehen, wie es möglich ist, Werte, die in QML verfügbar sind, für die auf „ QRhi “ basierende Darstellung zu nutzen. Wir animieren den Schwellenwert mithilfe eines „ NumberAnimation “ in der QML-Datei, und dieser Float-Wert wird dann in einem Uniform-Puffer an den Fragment-Shader weitergegeben.
Das Beispiel entspricht in den meisten Punkten den Beispielen „OpenGL unter QML“, „Direct3D 11 unter QML“, „Metal unter QML“ und „Vulkan unter QML “. Diese Beispiele rendern denselben Inhalt durch direkte Nutzung einer 3D-API. Dieses Beispiel hingegen ist vollständig plattformübergreifend und portabel, da es von Haus aus die Arbeit mit allen von „ QRhi “ unterstützten 3D-APIs (wie OpenGL, Vulkan, Metal, Direct3D 11 und 12) unterstützt.
Hinweis: Dieses Beispiel demonstriert fortgeschrittene, Low-Level-Funktionalität für portables, plattformübergreifendes 3D-Rendering, wobei es sich auf APIs aus dem Qt GUI-Modul stützt, für die nur eine begrenzte Kompatibilitätsgarantie besteht. Um die APIs von „ QRhi “ nutzen zu können, wird die Anwendung mit „ Qt::GuiPrivate “ verknüpft und „ <rhi/qrhi.h> “ eingebunden.
Das Hinzufügen von benutzerdefiniertem Rendering als Unter- oder Überlagerung ist eine der drei Möglichkeiten, benutzerdefiniertes 2D-/3D-Rendering in eine „ Qt Quick “-Szene zu integrieren. Die beiden anderen Optionen bestehen darin, das Rendering „inline“ mit dem eigenen Rendering der „ Qt Quick “-Szene unter Verwendung von QSGRenderNode durchzuführen oder einen komplett separaten Render-Durchlauf für ein dediziertes Render-Ziel (eine Textur) zu generieren und anschließend die Textur über ein Element in der Szene anzeigen zu lassen. Beachten Sie zu diesen Ansätzen die Beispiele „Scene Graph – RHI Texture Item “ und „Scene Graph – Custom QSGRenderNode “.
Kernkonzepte
Das Signal „beforeRendering()“ wird zu Beginn jedes Frames ausgesendet, bevor der Szenenbaum mit dem Rendern beginnt; daher werden alle „ QRhi “-Draw-Calls, die als Reaktion auf dieses Signal erfolgen, unter den „ Qt Quick “-Elementen gestapelt. Es gibt jedoch zwei Signale, die hier relevant sind: Die anwendungsspezifischen „ QRhi “-Befehle sollten in denselben Befehlspuffer geschrieben werden, der auch vom Szenenbaum verwendet wird, und darüber hinaus sollten die Befehle zum selben Render-Durchlauf gehören. „beforeRendering()“ allein reicht hierfür nicht aus, da es zu Beginn des Frames ausgelöst wird, also bevor die Aufzeichnung eines Render-Passes über „ QRhiCommandBuffer::beginPass()“ gestartet wird. Durch die zusätzliche Anbindung an „beforeRenderPassRecording()“ werden die anwendungsspezifischen Befehle und das Rendering des Szenengraphen schließlich in der richtigen Reihenfolge angeordnet:
- Die Render-Schleife des Szenengraphen ruft „ QRhi::beginFrame()“ auf
- QQuickWindow::beforeRendering() wird ausgelöst – die Anwendung bereitet Ressourcen für ihr benutzerdefiniertes Rendering vor
- Der Szenenbaum ruft ` QRhiCommandBuffer::beginPass()` auf
- QQuickWindow::beforeRenderPassRecording() wird ausgelöst – die Anwendung zeichnet Draw-Calls auf
- Der Szenengraph zeichnet die Draw-Calls auf
Schritt-für-Schritt-Anleitung
Das benutzerdefinierte Rendering ist in einem benutzerdefinierten ` QQuickItem` gekapselt. ` RhiSquircle ` leitet sich von ` QQuickItem` ab und wird in QML bereitgestellt (beachten Sie die ` 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 spielen Position und Größe des Elements keine Rolle, und es implementiert ` updatePaintNode()` nicht neu.
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;
};Stattdessen wird das Element, sobald es mit einem „ QQuickWindow “ verknüpft wird, mit dem Signal „ QQuickWindow::beforeSynchronizing()“ verbunden. Die Verwendung von „ Qt::DirectConnection “ ist wichtig, da dieses Signal im Render-Thread „ Qt Quick “ ausgelöst wird, sofern ein solcher vorhanden ist. Wir möchten, dass der verbundene Slot im selben 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 Synchronisationsphase des Szenengraphen wird die Rendering-Infrastruktur erstellt, sofern dies noch nicht geschehen ist, und die für das Rendering relevanten Daten werden synchronisiert, d. h. vom Element „ RhiSquircle “, das sich auf dem Hauptthread befindet, in das Objekt „ SquircleRenderer “ kopiert, das sich auf dem Render-Thread befindet. (Gibt es keinen Render-Thread, befinden sich beide Objekte im Haupt-Thread.) Der Zugriff auf die Daten ist sicher, da der Haupt-Thread gesperrt ist, während der Render-Thread seine Synchronisationsphase ausführt. Weitere Informationen zum Threading- und Rendering-Modell des Szenengraphen finden Sie unter „Qt Quick -Szenengrafik “.
Zusätzlich zum Wert von ` t` wird auch der zugehörige Zeiger ` QQuickWindow ` kopiert. Zwar könnte das ` SquircleRenderer ` die Methode ` window()` für das Element ` RhiSquircle ` auch dann abfragen, wenn es im Render-Thread ausgeführt wird, doch ist dies theoretisch nicht ganz sicher. Daher wird eine Kopie erstellt.
Beim Einrichten des „ SquircleRenderer “ werden Verbindungen zu „ beforeRendering()“ und „ beforeRenderPassRecording()“ hergestellt, die entscheidend dafür sind, dass die benutzerdefinierten 3D-Rendering-Befehle der Anwendung zum richtigen Zeitpunkt ausgeführt und eingefügt werden können.
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());
}Wenn beforeRendering() ausgelöst wird, werden die für unser benutzerdefiniertes Rendering benötigten Ressourcen von QRhi, wie z. B. QRhiBuffer, QRhiGraphicsPipeline und zugehörige Objekte, erstellt, sofern dies noch nicht geschehen ist.
Die Daten in den Puffern werden mithilfe von ` QRhiResourceUpdateBatch ` und ` QRhiCommandBuffer::resourceUpdate()` aktualisiert (genauer gesagt: Die Datenaktualisierungsoperationen werden in die Warteschlange gestellt). Der Vertex-Puffer ändert seinen Inhalt nicht mehr, sobald der anfängliche Satz an Scheitelpunkten in ihn geladen wurde. Der Uniform-Puffer ist hingegen ein „ dynamic “-Puffer, wie es für solche Puffer typisch ist. Sein Inhalt – zumindest in einigen Bereichen – wird bei jedem Frame aktualisiert. Daher erfolgt der bedingungslose Aufruf von ` updateDynamicBuffer()` für den Offset 0 und eine Byte-Größe von 4 (was ` sizeof(float) ` entspricht, da der C++-Typ ` float ` zufällig mit dem 32-Bit-Typ ` float` in GLSL übereinstimmt). An dieser Position ist der Wert von ` t` gespeichert, und dieser wird in jedem Frame aktualisiert, d. h. bei jedem Aufruf von `frameStart()`.
Im Puffer befindet sich ein weiterer Float-Wert, beginnend bei Offset 4. Dieser wird verwendet, um die Unterschiede im Koordinatensystem der 3D-APIs auszugleichen: 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 übergeben wird, auf den Kopf gestellt wird; auf dessen Grundlage die Farbe berechnet wird. Auf diese Weise ist die Darstellung auf dem Bildschirm identisch (d. h., die obere linke Ecke ist grünlich, die untere linke rotlich), unabhängig davon, welche 3D-API verwendet wird. Dieser Wert wird im Uniform-Puffer nur einmal aktualisiert, ähnlich wie im Vertex-Puffer. Dies verdeutlicht ein Problem, mit dem sich Low-Level-Rendering-Code, der portabel sein soll, häufig auseinandersetzen muss: die Unterschiede im Koordinatensystem bei normalisierten Gerätekordinaten (NDC) sowie bei Bildern und Framebuffern. Beispielsweise verwendet das NDC-System überall außer bei Vulkan ein Koordinatensystem mit Ursprung in der unteren linken Ecke. Framebuffer hingegen verwenden überall außer bei OpenGL ein Koordinatensystem mit Ursprung in der oberen linken Ecke. Typische Renderer, die mit einer perspektivischen Projektion arbeiten, können dieses Problem oft ignorieren, indem sie bequem auf ` QRhi::clipSpaceCorrMatrix()` zurückgreifen – eine Matrix, die mit der Projektionsmatrix multipliziert werden kann und bei Bedarf sowohl eine Y-Spiegelung anwendet als auch der Tatsache Rechnung trägt, dass die Tiefe im Clip-Raum bei OpenGL „ -1..1 “ verläuft, überall sonst jedoch „ 0..1 “. In einigen Fällen, wie beispielsweise in diesem Beispiel, ist dies jedoch nicht anwendbar. Stattdessen müssen die Anwendung und die Shader-Logik die erforderlichen Anpassungen der Vertex- und UV-Positionen entsprechend vornehmen, basierend auf den Abfragen von QRhi::isYUpInNDC() und QRhi::isYUpInFramebuffer().
Um Zugriff auf die von ` Qt Quick ` verwendeten Objekte ` QRhi ` und ` QRhiSwapChain ` zu erhalten, können diese einfach über ` QQuickWindow` abgefragt werden. Beachten Sie, dass dies davon ausgeht, dass ` QQuickWindow ` ein reguläres Bildschirmfenster ist. Würde stattdessen ` QQuickRenderControl ` verwendet, z. B. um ein Off-Screen-Rendering in eine Textur durchzuführen, wäre die Abfrage der Swapchain falsch, da in diesem Fall keine Swapchain vorhanden ist.
Da das Signal ausgegeben wird, nachdem „ Qt Quick “ „ QRhi::beginFrame()“ aufgerufen hat, ist es bereits möglich, den Befehlspuffer und das Renderziel aus der Swapchain abzufragen. Dies ermöglicht es, bequem „ QRhiCommandBuffer::resourceUpdate()“ auf das von „ QRhiSwapChain::currentFrameCommandBuffer()“ zurückgegebene Objekt anzuwenden. Beim Erstellen einer Grafikpipeline kann ein „ QRhiRenderPassDescriptor “ aus dem von „ QRhiSwapChain::currentFrameRenderTarget()“ zurückgegebenen „ QRhiRenderTarget “ abgerufen werden. (Beachten Sie, dass dies bedeutet, dass die hier erstellte Grafikpipeline nur für das Rendern in die Swapchain oder bestenfalls in ein anderes Renderziel geeignet ist, das mit ihr „ compatible “ ist; wenn wir in eine Textur rendern wollten, wäre wahrscheinlich ein anderes „ QRhiRenderPassDescriptor “ und somit eine andere Grafikpipeline erforderlich, da sich die Formate von Textur und Swapchain unterscheiden können.)
void SquircleRenderer::frameStart()
{
// Diese Funktion wird im Render-Thread aufgerufen, sofern vorhanden.
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 Floats
m_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::TargetBlendblend;
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 bei ` QQuickWindow::beforeRenderPassRecording()` ein Draw-Call für einen Dreiecksstreifen mit 4 Eckpunkten aufgezeichnet. In der Praxis zeichnet dieses Beispiel lediglich ein Viereck und berechnet die Pixelfarben mithilfe der Logik in den Fragment-Shadern, aber Anwendungen können auch komplexere Zeichnungen ausführen: Das Erstellen mehrerer Grafik-Pipelines und das Aufzeichnen mehrerer Draw-Calls ist ebenfalls völlig in Ordnung. Wichtig ist dabei zu beachten, dass alles, was auf dem „ QRhiCommandBuffer “ aufgezeichnet wird – der über die „ swapchain “ des Fensters abgerufen wird –, effektiv vor dem eigenen Rendering des „ Qt Quick “-Szenengraphen innerhalb des Haupt-Render-Durchgangs eingefügt wird.
Hinweis: Das bedeutet, dass bei Verwendung eines Tiefenpuffers mit Tiefenprüfung und dem Schreiben von Tiefenwerten der Inhalt von „ Qt Quick “ durch die in den Tiefenpuffer geschriebenen Werte beeinflusst werden kann. Weitere Informationen zum Renderer des Szenengraphen finden Sie unter „Qt Quick Scene Graph Default Renderer“, insbesondere in den Abschnitten über die Behandlung von opaken und alpha-gemischten Primitiven.
Um die Fenstergröße in Pixeln zu ermitteln, wird ` QRhiRenderTarget::pixelSize()` verwendet. Dies ist praktisch, da das Beispiel auf diese Weise die Viewport-Größe nicht auf andere Weise berechnen muss und sich nicht um die Anwendung der ` high DPI scale factor` kümmern muss, falls vorhanden.
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 Shader-Konditionierungspipeline von QRhi. Ursprünglich als Vulkan-kompatibles GLSL geschrieben, werden sie in SPIR-V kompiliert und anschließend mit den Qt-Tools in andere Shading-Sprachen transpiliert. 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 erforderlichen Verarbeitungsschritte bereits während der Kompilierung durchzuführen. Weitere Informationen finden Sie unter qt_add_shaders().
Die Angabe von „ BASE “ hilft dabei, das Präfix „ ../shared “ zu entfernen, während „ PREFIX “ das gewünschte Präfix „ /scenegraph/rhiunderqml “ hinzufügt. Somit lautet der endgültige Pfad „ :/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
)Zur Unterstützung von qmake enthält das Beispiel weiterhin die .qsb -Dateien, die normalerweise zum Zeitpunkt der Kompilierung generiert würden, und listet sie in der qrc-Datei auf. Dieser Ansatz wird jedoch für neue Anwendungen, die CMake als Build-System verwenden, nicht empfohlen.
Siehe auch Szenengraph – RHI-Textur-Element und Szenengraph – Benutzerdefinierter QSGRenderNode.
© 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.