RHI Fenster Beispiel
Dieses Beispiel zeigt, wie man eine minimale QWindow-basierte Anwendung mit QRhi erstellt.
Qt 6.6 bietet seine beschleunigte 3D-API und Shader-Abstraktionsschicht nun auch für Anwendungen an. Anwendungen können nun dieselben 3D-Grafikklassen verwenden, die Qt selbst zur Implementierung des Qt Quick Scenegraphs oder der Qt Quick 3D Engine verwendet. In früheren Qt-Versionen waren QRhi und die zugehörigen Klassen allesamt private APIs. Ab 6.6 gehören diese Klassen in eine ähnliche Kategorie wie die QPA-Klassenfamilie: weder vollständig öffentlich noch privat, sondern etwas dazwischen, mit einem begrenzteren Kompatibilitätsversprechen im Vergleich zu öffentlichen APIs. Andererseits sind QRhi und die zugehörigen Klassen jetzt mit einer vollständigen Dokumentation ausgestattet, ähnlich wie bei öffentlichen APIs.
Es gibt mehrere Möglichkeiten, QRhi zu verwenden, das Beispiel hier zeigt den einfachsten Ansatz: die Verwendung von QWindow, ohne Qt Quick, Qt Quick 3D oder Widgets in irgendeiner Form zu verwenden, und die Einrichtung der gesamten Rendering- und Windowing-Infrastruktur in der Anwendung.
Im Gegensatz dazu, wenn man eine QML-Anwendung mit Qt Quick oder Qt Quick 3D schreibt und QRhi-basiertes Rendering hinzufügen möchte, wird sich eine solche Anwendung auf die Fenster- und Rendering-Infrastruktur Qt Quick verlassen, die bereits initialisiert wurde, und sie wird wahrscheinlich eine existierende QRhi -Instanz von QQuickWindow abfragen. Der Umgang mit QRhi::create(), Plattform/API-Spezifika wie Vulkan instances oder die korrekte Handhabung von expose und Größenänderungsereignissen für das Fenster werden alle von Qt Quick verwaltet. In diesem Beispiel hingegen wird all dies von der Anwendung selbst verwaltet und erledigt.
Hinweis: Insbesondere für QWidget-basierte Anwendungen ist zu beachten, dass QWidget::createWindowContainer() die Einbettung eines QWindow (unterstützt durch ein natives Fenster) in die Widget-basierte Benutzeroberfläche ermöglicht. Daher ist die Klasse HelloWindow
aus diesem Beispiel in QWidget-basierten Anwendungen wiederverwendbar, vorausgesetzt, die notwendige Initialisierung von main()
ist ebenfalls vorhanden.
3D-API-Unterstützung
Die Anwendung unterstützt alle aktuellen QRhi backends. Wenn keine Befehlszeilenargumente angegeben werden, werden plattformspezifische Standardwerte verwendet: Direct 3D 11 unter Windows, OpenGL unter Linux, Metal unter macOS/iOS.
Die Ausführung mit --help
zeigt die verfügbaren Befehlszeilenoptionen an:
- -d oder -d3d11 für Direct 3D 11
- -D oder -d3d12 für Direct 3D 12
- -m oder -metal für Metal
- -v oder -vulkan für Vulkan
- -g oder -opengl für OpenGL oder OpenGL ES
- -n oder -null für die Null backend
Hinweise zum Build-System
Diese Anwendung stützt sich ausschließlich auf das Modul Qt GUI. Sie verwendet weder Qt Widgets noch Qt Quick.
Um auf die RHI-APIs zugreifen zu können, die allen Qt-Anwendungen zur Verfügung stehen, aber ein begrenztes Kompatibilitätsversprechen beinhalten, listet der CMake-Befehl target_link_libraries
Qt6::GuiPrivate
auf. Dies ermöglicht die erfolgreiche Kompilierung der #include <rhi/qrhi.h>
include-Anweisung.
Merkmale
Die Anwendung bietet folgende Funktionen:
- Eine größenveränderbare QWindow,
- einen Swapchain- und Tiefenschablonenpuffer, der sich an die Größe des Fensters anpasst,
- Logik zum Initialisieren, Rendern und Abbauen zum richtigen Zeitpunkt, basierend auf Ereignissen wie QExposeEvent und QPlatformSurfaceEvent,
- Rendering eines bildschirmfüllenden texturierten Vierecks unter Verwendung einer Textur, deren Inhalt auf QImage über QPainter generiert wird (unter Verwendung der Rasterpainting-Engine, d. h. die Generierung der Pixeldaten des Bildes erfolgt ausschließlich auf CPU-Basis; diese Daten werden dann in eine GPU-Textur hochgeladen),
- Rendering eines Dreiecks mit aktivierter Überblendung und Tiefenprüfung unter Verwendung einer perspektivischen Projektion, wobei eine Modelltransformation angewendet wird, die sich bei jedem Bild ändert,
- eine effiziente, plattformübergreifende Rendering-Schleife unter Verwendung von requestUpdate().
Shader
Die Anwendung verwendet zwei Sätze von Vertex- und Fragment-Shader-Paaren:
- eines für das Vollbild-Quad, das keine Scheitelpunkt-Eingaben verwendet und bei dem der Fragment-Shader eine Textur abtastet (
quad.vert
,quad.frag
), - und ein weiteres Paar für das Dreieck, bei dem Scheitelpunktpositionen und Farben in einem Scheitelpunktpuffer und eine Modellansichts-Projektionsmatrix in einem einheitlichen Puffer bereitgestellt werden (
color.vert
,color.frag
).
Die Shader sind als Vulkan-kompatibler GLSL-Quellcode geschrieben.
Da es sich um ein Qt GUI Modulbeispiel handelt, kann dieses Beispiel keine Abhängigkeit von dem Qt Shader Tools Modul haben. Dies bedeutet, dass CMake-Hilfsfunktionen wie qt_add_shaders
nicht verwendet werden können. Daher hat das Beispiel die vorverarbeiteten .qsb
Dateien im Ordner shaders/prebuilt
und sie werden einfach über qt_add_resources
in die ausführbare Datei eingebunden. Dieser Ansatz wird im Allgemeinen nicht für Anwendungen empfohlen, man sollte eher qt_add_shaders verwenden, was die manuelle Erzeugung und Verwaltung der .qsb
Dateien vermeidet.
Um die .qsb
Dateien für dieses Beispiel zu erzeugen, wurde der Befehl qsb --qt6 color.vert -o prebuilt/color.vert.qsb
usw. verwendet. Dies führt zur Kompilierung nach SPIR-V und anschließend zur Transpilierung in GLSL (100 es
und 120
), HLSL (5.0) und MSL (1.2). Alle Shader-Versionen werden dann in eine QShader gepackt und auf der Festplatte serialisiert.
API-spezifische Initialisierung
Für einige der 3D-APIs muss die main()-Funktion die entsprechende API-spezifische Initialisierung durchführen, z. B. um eine QVulkanInstance bei der Verwendung von Vulkan zu erstellen. Für OpenGL müssen wir sicherstellen, dass ein Tiefenpuffer verfügbar ist, dies geschieht über QSurfaceFormat. Diese Schritte fallen nicht in den Bereich von QRhi, da QRhi Backends für OpenGL oder Vulkan auf den bestehenden Qt-Funktionen wie QOpenGLContext oder QVulkanInstance aufbauen.
// Für OpenGL, um sicherzustellen, dass es einen Tiefen-/Schablonenpuffer für das Fenster gibt. // Bei anderen APIs ist dies unter der Kontrolle der Anwendung (QRhiRenderBuffer etc.) // und daher ist für diese keine spezielle Einrichtung erforderlich. QSurfaceFormat fmt; fmt.setDepthBufferSize(24); fmt.setStencilBufferSize(8); // Spezialfall macOS, um dort die Verwendung von OpenGL zu ermöglichen // (der Standard-Metal ist allerdings der empfohlene Ansatz) // gl_VertexID ist ein GLSL-130-Feature, und daher ist der Standard-OpenGL-2.1-Kontext // den wir unter macOS erhalten, nicht ausreichend.#ifdef Q_OS_MACOSfmt.setVersion(4, 1); fmt.setProfile(QSurfaceFormat::CoreProfile);#endif QSurfaceFormat::setDefaultFormat(fmt); // Für Vulkan.#if QT_CONFIG(vulkan) QVulkanInstance inst; if (graphicsApi == QRhi::Vulkan) { // Validierung anfordern, falls verfügbar. Dies ist völlig optional // und hat Auswirkungen auf die Leistung und sollte in der Produktion vermieden werden.inst.setLayers({ "VK_LAYER_KHRONOS_validation" }); // Spielt schön mit QRhi.inst.setExtensions(QRhiVulkanInitParams::preferredInstanceExtensions()); if (!inst.create()) { qWarning("Failed to create Vulkan instance, switching to OpenGL"); graphicsApi = QRhi::OpenGLES2; } }#endif
Hinweis: Beachten Sie bei Vulkan, wie QRhiVulkanInitParams::preferredInstanceExtensions() berücksichtigt wird, um sicherzustellen, dass die entsprechenden Erweiterungen aktiviert sind.
HelloWindow
ist eine Unterklasse von RhiWindow
, die wiederum eine QWindow ist. RhiWindow
enthält alles, was für die Verwaltung eines größenveränderlichen Fensters mit einer Swapchain (und einem Tiefenschablonenpuffer) benötigt wird, und ist potenziell auch in anderen Anwendungen wiederverwendbar. HelloWindow
enthält die Rendering-Logik, die für diese spezielle Beispielanwendung spezifisch ist.
Im Konstruktor der Unterklasse QWindow wird der Oberflächentyp auf der Grundlage der ausgewählten 3D-API festgelegt.
RhiWindow::RhiWindow(QRhi::Implementation graphicsApi) : m_graphicsApi(graphicsApi) { switch (graphicsApi) { case QRhi::OpenGLES2: setSurfaceType(OpenGLSurface); break; case QRhi::Vulkan: setSurfaceType(VulkanSurface); break; case QRhi::D3D11: case QRhi::D3D12: setSurfaceType(Direct3DSurface); break; case QRhi::Metal: setSurfaceType(MetalSurface); break; case QRhi::Null: break; // RasterSurface } }
Das Erstellen und Initialisieren eines QRhi Objekts wird in RhiWindow::init() implementiert. Beachten Sie, dass diese Funktion nur aufgerufen wird, wenn das Fenster renderable
ist, was durch ein expose event angezeigt wird.
Je nachdem, welche 3D-API verwendet wird, muss die entsprechende InitParams-Struktur an QRhi::create() übergeben werden. Bei OpenGL zum Beispiel muss ein QOffscreenSurface (oder ein anderes QSurface) von der Anwendung erstellt und dem QRhi zur Verfügung gestellt werden. Bei Vulkan ist ein erfolgreich initialisiertes QVulkanInstance erforderlich. Andere, wie Direct 3D oder Metal, benötigen keine zusätzlichen Informationen, um initialisiert werden zu können.
void RhiWindow::init() { if (m_graphicsApi == QRhi::Null) { QRhiNullInitParams params; m_rhi.reset(QRhi::create(QRhi::Null, ¶ms)); }#if QT_CONFIG(opengl) if (m_graphicsApi == QRhi::OpenGLES2) { m_fallbackSurface.reset(QRhiGles2InitParams::newFallbackSurface()); QRhiGles2InitParams params; params.fallbackSurface = m_fallbackSurface.get(); params.window = this; m_rhi.reset(QRhi::create(QRhi::OpenGLES2, ¶ms)); }#endif#if QT_CONFIG(vulkan) if (m_graphicsApi == QRhi::Vulkan) { QRhiVulkanInitParams params; params.inst = vulkanInstance(); params.window = this; m_rhi.reset(QRhi::create(QRhi::Vulkan, ¶ms)); }#endif#ifdef Q_OS_WIN if (m_graphicsApi == QRhi::D3D11) { QRhiD3D11InitParams params; // Aktivieren Sie die Debug-Schicht, falls verfügbar. Dies ist optional // und sollte in Produktionsbuilds vermieden werden.params.enableDebugLayer = true; m_rhi.reset(QRhi::create(QRhi::D3D11, ¶ms)); } else if (m_graphicsApi == QRhi::D3D12) { QRhiD3D12InitParams params; // Aktivieren Sie die Debug-Schicht, falls vorhanden. Dies ist optional // und sollte in Produktionsbuilds vermieden werden.params.enableDebugLayer = true; m_rhi.reset(QRhi::create(QRhi::D3D12, ¶ms)); }#endif#if QT_CONFIG(metal) if (m_graphicsApi == QRhi::Metal) { QRhiMetalInitParams params; m_rhi.reset(QRhi::create(QRhi::Metal, ¶ms)); }#endif if (!m_rhi) qFatal("Failed to create RHI backend");
Abgesehen davon ist alles andere, der gesamte Rendering-Code, vollständig plattformübergreifend und hat keine Verzweigungen oder Bedingungen, die für eine der 3D-APIs spezifisch sind.
Ereignisse freilegen
Was renderable
genau bedeutet, ist plattformspezifisch. Unter macOS zum Beispiel ist ein Fenster, das vollständig verdeckt ist (vollständig hinter einem anderen Fenster), nicht renderbar, während unter Windows die Verdeckung keine Bedeutung hat. Glücklicherweise braucht die Anwendung kein spezielles Wissen darüber: Die Plattform-Plugins von Qt abstrahieren die Unterschiede hinter dem expose-Ereignis. Die Neuimplementierung von exposeEvent() muss sich jedoch auch bewusst sein, dass eine leere Ausgabegröße (z.B. Breite und Höhe von 0) ebenfalls als nicht darstellbare Situation behandelt werden sollte. Unter Windows zum Beispiel ist dies der Fall, wenn das Fenster minimiert wird. Daher die Prüfung auf Basis von QRhiSwapChain::surfacePixelSize().
Diese Implementierung der Expose-Ereignisbehandlung versucht, robust, sicher und portabel zu sein. Qt Quick selbst implementiert eine sehr ähnliche Logik in seinen Render-Schleifen.
void RhiWindow::exposeEvent(QExposeEvent *) { // initialize and start rendering when the window becomes usable for graphics purposes if (isExposed() && !m_initialized) { init(); resizeSwapChain(); m_initialized = true; } const QSize surfaceSize = m_hasSwapChain ? m_sc->surfacePixelSize() : QSize(); // stop pushing frames when not exposed (or size is 0) if ((!isExposed() || (m_hasSwapChain && surfaceSize.isEmpty())) && m_initialized && !m_notExposed) m_notExposed = true; // Continue when exposed again and the surface has a valid size. Note that // surfaceSize can be (0, 0) even though size() reports a valid one, hence // trusting surfacePixelSize() and not QWindow. if (isExposed() && m_initialized && m_notExposed && !surfaceSize.isEmpty()) { m_notExposed = false; m_newlyExposed = true; } // always render a frame on exposeEvent() (when exposed) in order to update // immediately on window resize. if (isExposed() && !surfaceSize.isEmpty()) render(); }
In RhiWindow::render(), das als Reaktion auf das von requestUpdate() erzeugte UpdateRequest -Ereignis aufgerufen wird, gibt es die folgende Prüfung, um zu verhindern, dass ein Renderversuch unternommen wird, wenn die Initialisierung der Swapchain fehlgeschlagen ist oder wenn das Fenster nicht mehr gerendert werden kann.
void RhiWindow::render() { if (!m_hasSwapChain || m_notExposed) return;
Swapchain, Depth-Stencil-Puffer und Größenänderung
Zum Rendern auf QWindow wird ein QRhiSwapChain benötigt. Außerdem wird eine QRhiRenderBuffer erstellt, die als Tiefenschablonenpuffer dient, da die Anwendung demonstriert, wie die Tiefenprüfung in einer Grafikpipeline aktiviert werden kann. Bei einigen älteren 3D-APIs ist die Verwaltung des Tiefen-/Schablonenpuffers für ein Fenster Teil der entsprechenden Windowing-Systemschnittstellen-API (EGL, WGL, GLX usw., d. h. der Tiefen-/Schablonenpuffer wird implizit zusammen mit dem window surface
verwaltet), während sich die Verwaltung des Tiefen-/Schablonenpuffers für ein fensterbasiertes Rendering-Ziel bei modernen APIs nicht von Rendering-Zielen außerhalb des Bildschirms unterscheidet. QRhi abstrahiert dies, aber für eine optimale Leistung muss immer noch angegeben werden, dass der QRhiRenderBuffer used with together with a QRhiSwapChain ist.
Die QRhiSwapChain ist mit der QWindow und dem Tiefen-/Schablonenpuffer verbunden.
std::unique_ptr<QRhiSwapChain> m_sc; std::unique_ptr<QRhiRenderBuffer> m_ds; std::unique_ptr<QRhiRenderPassDescriptor> m_rp; m_sc.reset(m_rhi->newSwapChain()); m_ds.reset(m_rhi->newRenderBuffer(QRhiRenderBuffer::DepthStencil, QSize(), // no need to set the size here, due to UsedWithSwapChainOnly 1, QRhiRenderBuffer::UsedWithSwapChainOnly)); m_sc->setWindow(this); m_sc->setDepthStencil(m_ds.get()); m_rp.reset(m_sc->newCompatibleRenderPassDescriptor()); m_sc->setRenderPassDescriptor(m_rp.get());
Wenn sich die Fenstergröße ändert, muss auch die Größe der Swapchain angepasst werden. Dies ist in resizeSwapChain() implementiert.
void RhiWindow::resizeSwapChain() { m_hasSwapChain = m_sc->createOrResize(); // also handles m_ds const QSize outputSize = m_sc->currentPixelSize(); 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); }
Im Gegensatz zu den anderen Unterklassen von QRhiResource weist QRhiSwapChain eine etwas andere Semantik auf, wenn es um seine create-Funktion geht. Wie der Name createOrResize() andeutet, muss diese Funktion immer dann aufgerufen werden, wenn bekannt ist, dass die Größe des Ausgabefensters nicht mit der letzten Initialisierung der Swapchain übereinstimmt. Die zugehörige QRhiRenderBuffer für depth-stencil wird automatisch auf size gesetzt, und create() wird implizit von createOrResize() der Swapchain aufgerufen.
Dies ist auch ein geeigneter Ort, um die Projektions- und Ansichtsmatrizen (neu) zu berechnen, da die perspektivische Projektion, die wir einrichten, vom Seitenverhältnis der Ausgabe abhängt.
Hinweis: Um Unterschiede im Koordinatensystem zu beseitigen, wird a backend/API-specific "correction" matrix von QRhi abgefragt und in die Projektionsmatrix eingefügt. Dies ermöglicht es der Anwendung, mit Vertex-Daten im OpenGL-Stil zu arbeiten, wobei ein Koordinatensystem mit dem Ursprung unten links angenommen wird.
Die Funktion resizeSwapChain() wird von RhiWindow::render() aufgerufen, wenn festgestellt wird, dass die aktuell gemeldete Größe nicht mehr mit derjenigen übereinstimmt, mit der die Swapchain zuletzt initialisiert wurde.
Siehe QRhiSwapChain::currentPixelSize() und QRhiSwapChain::surfacePixelSize() für weitere Details.
Die Unterstützung für hohe DPI-Werte ist eingebaut: die Größen sind, wie der Name schon sagt, immer in Pixeln, wobei die fensterspezifischen scale factor berücksichtigt werden. Auf der Ebene von QRhi (und der 3D-API) gibt es kein Konzept für die Skalierung mit hohen DPI-Werten, alles wird immer in Pixeln angegeben. Das bedeutet, dass ein QWindow mit einer Größe() von 1280x720 und einer devicePixelRatio() von 2 ein Rendering-Ziel (Swapchain) mit einer (Pixel-)Größe von 2560x1440 ist.
// If the window got resized or newly exposed, resize the swapchain. (the // newly-exposed case is not actually required by some platforms, but is // here for robustness and portability) // // This (exposeEvent + the logic here) is the only safe way to perform // resize handling. Note the usage of the RHI's surfacePixelSize(), and // never QWindow::size(). (the two may or may not be the same under the hood, // depending on the backend and platform) // if (m_sc->currentPixelSize() != m_sc->surfacePixelSize() || m_newlyExposed) { resizeSwapChain(); if (!m_hasSwapChain) return; m_newlyExposed = false; }
Rendering-Schleife
Die Anwendung rendert kontinuierlich, gedrosselt durch die Darstellungsrate (vsync). Dies wird durch den Aufruf von requestUpdate() aus RhiWindow::render() sichergestellt, wenn das aktuell aufgezeichnete Bild übermittelt wurde.
m_rhi->endFrame(m_sc.get()); // Always request the next frame via requestUpdate(). On some platforms this is backed // by a platform-specific solution, e.g. CVDisplayLink on macOS, which is potentially // more efficient than a timer, queued metacalls, etc. requestUpdate(); }
Dies führt schließlich zum Erhalt eines UpdateRequest Ereignisses. Dies wird in der Neuimplementierung von event() behandelt.
bool RhiWindow::event(QEvent *e) { switch (e->type()) { case QEvent::UpdateRequest: render(); break; case QEvent::PlatformSurface: // this is the proper time to tear down the swapchain (while the native window and surface are still around) if (static_cast<QPlatformSurfaceEvent *>(e)->surfaceEventType() == QPlatformSurfaceEvent::SurfaceAboutToBeDestroyed) releaseSwapChain(); break; default: break; } return QWindow::event(e); }
Einrichtung von Ressourcen und Pipelines
Die Anwendung zeichnet einen einzigen Rendering-Durchgang auf, der zwei Zeichenaufrufe mit zwei verschiedenen Grafikpipelines auslöst. Die eine ist der "Hintergrund" mit der Textur, die das von QPainter erzeugte Bild enthält, dann wird ein einzelnes Dreieck mit aktivierter Überblendung darüber gerendert.
Der mit dem Dreieck verwendete Vertex- und Uniform-Buffer wird wie folgt erstellt. Die Größe des Uniform-Buffers beträgt 68 Byte, da der Shader ein mat4
und ein float
Mitglied im Uniform-Block hat. Achten Sie auf die std140-Layoutregeln. Dies stellt in diesem Beispiel keine Überraschungen dar, da das Mitglied float
, das auf mat4
folgt, die korrekte Ausrichtung ohne zusätzliches Padding hat, aber es kann in anderen Anwendungen relevant werden, insbesondere wenn Sie mit Typen wie vec2
oder vec3
arbeiten. Im Zweifelsfall sollten Sie QShaderDescription auf QShader überprüfen oder, was oft bequemer ist, das Werkzeug qsb
auf der Datei .qsb
mit dem Argument -d
ausführen, um die Metadaten in menschenlesbarer Form zu prüfen. Die gedruckten Informationen enthalten unter anderem die Offsets der Uniform Block Member, die Größen und die Gesamtgröße jedes Uniform Blocks in Bytes.
void HelloWindow::customInit() { m_initialUpdates = m_rhi->nextResourceUpdateBatch(); m_vbuf.reset(m_rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(vertexData))); m_vbuf->create(); m_initialUpdates->uploadStaticBuffer(m_vbuf.get(), vertexData); static const quint32 UBUF_SIZE = 68; m_ubuf.reset(m_rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, UBUF_SIZE)); m_ubuf->create();
Die Vertex- und Fragment-Shader benötigen beide einen einheitlichen Puffer am Bindungspunkt 0. Dies wird durch das QRhiShaderResourceBindings Objekt sichergestellt. Die Grafikpipeline wird dann mit den Shadern und einer Reihe von zusätzlichen Informationen eingerichtet. Das Beispiel stützt sich auch auf einige bequeme Standardeinstellungen, z. B. ist die primitive Topologie Triangles, aber das ist die Standardeinstellung und wird daher nicht explizit festgelegt. Siehe QRhiGraphicsPipeline für weitere Details.
Neben der Angabe der Topologie und der verschiedenen Zustände muss die Pipeline auch mit einem anderen Element verknüpft werden:
- Das Scheitelpunkt-Eingabe-Layout in Form einer QRhiVertexInputLayout. Diese spezifiziert den Typ und die Anzahl der Komponenten für jede Scheitelpunkt-Eingabestelle, den Gesamtstride in Bytes pro Scheitelpunkt und andere zugehörige Daten. QRhiVertexInputLayout enthält nur Daten, keine tatsächlichen nativen Ressourcen, und ist kopierbar.
- Ein gültiges und erfolgreich initialisiertes QRhiShaderResourceBindings Objekt. Dies beschreibt das Layout der Ressourcenbindungen (einheitliche Puffer, Texturen, Sampler), die die Shader erwarten. Dies muss entweder das QRhiShaderResourceBindings sein, das bei der Aufzeichnung der Zeichenaufrufe verwendet wird, oder ein anderes, das layout-compatible with it ist. Diese einfache Anwendung wählt den ersteren Ansatz.
- Ein gültiges QRhiRenderPassDescriptor Objekt. Dieses muss vom Rendering-Ziel abgerufen werden, oder be compatible with. Das Beispiel verwendet Ersteres, indem ein QRhiRenderPassDescriptor Objekt über QRhiSwapChain::newCompatibleRenderPassDescriptor() erstellt wird.
m_colorTriSrb.reset(m_rhi->newShaderResourceBindings()); static const QRhiShaderResourceBinding::StageFlags visibility = QRhiShaderResourceBinding::VertexStage | QRhiShaderResourceBinding::FragmentStage; m_colorTriSrb->setBindings({ QRhiShaderResourceBinding::uniformBuffer(0, visibility, m_ubuf.get()) }); m_colorTriSrb->create(); m_colorPipeline.reset(m_rhi->newGraphicsPipeline()); // Enable depth testing; not quite needed for a simple triangle, but we // have a depth-stencil buffer so why not. m_colorPipeline->setDepthTest(true); m_colorPipeline->setDepthWrite(true); // Blend factors default to One, OneOneMinusSrcAlpha, which is convenient. QRhiGraphicsPipeline::TargetBlend premulAlphaBlend; premulAlphaBlend.enable = true; m_colorPipeline->setTargetBlends({ premulAlphaBlend }); m_colorPipeline->setShaderStages({ { QRhiShaderStage::Vertex, getShader(QLatin1String(":/color.vert.qsb")) }, { QRhiShaderStage::Fragment, getShader(QLatin1String(":/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_colorPipeline->setVertexInputLayout(inputLayout); m_colorPipeline->setShaderResourceBindings(m_colorTriSrb.get()); m_colorPipeline->setRenderPassDescriptor(m_rp.get()); m_colorPipeline->create();
getShader() ist eine Hilfsfunktion, die eine .qsb
Datei lädt und ein QShader aus ihr deserialisiert.
static QShader getShader(const QString &name) { QFile f(name); if (f.open(QIODevice::ReadOnly)) return QShader::fromSerialized(f.readAll()); return QShader(); }
Der color.vert
Shader spezifiziert das Folgende als die Vertex-Eingänge:
layout(location = 0) in vec4 position; layout(location = 1) in vec3 color;
Der C++-Code liefert jedoch Scheitelpunktdaten als 2 Floats für die Position, mit 3 Floats für die Farbe verschachtelt. (x
, y
, r
, g
, b
für jeden Scheitelpunkt) Deshalb ist der Stride 5 * sizeof(float)
und die Eingänge für die Positionen 0 und 1 sind als Float2
bzw. Float3
angegeben. Dies ist gültig, und die z
und w
der vec4
Position werden automatisch gesetzt.
Rendering
Die Aufnahme eines Frames wird durch den Aufruf von QRhi::beginFrame() gestartet und durch den Aufruf von QRhi::endFrame() beendet.
QRhi::FrameOpResult result = m_rhi->beginFrame(m_sc.get()); if (result == QRhi::FrameOpSwapChainOutOfDate) { resizeSwapChain(); if (!m_hasSwapChain) return; result = m_rhi->beginFrame(m_sc.get()); } if (result ! = QRhi::FrameOpSuccess) { qWarning("beginFrame failed with %d, will retry", result); requestUpdate(); return; } customRender();
Einige der Ressourcen (Puffer, Texturen) haben statische Daten in der Anwendung, was bedeutet, dass sich der Inhalt nie ändert. Der Inhalt des Vertex-Puffers wird zum Beispiel im Initialisierungsschritt bereitgestellt und danach nicht mehr verändert. Diese Datenaktualisierungsvorgänge werden in m_initialUpdates
aufgezeichnet. Wenn sie noch nicht abgeschlossen sind, werden die Befehle für diesen Stapel von Ressourcenaktualisierungen mit dem Stapel pro Frame zusammengeführt.
void HelloWindow::customRender() { QRhiResourceUpdateBatch *resourceUpdates = m_rhi->nextResourceUpdateBatch(); if (m_initialUpdates) { resourceUpdates->merge(m_initialUpdates); m_initialUpdates->release(); m_initialUpdates = nullptr; }
Die Aktualisierung der Ressourcen pro Frame ist notwendig, da sich der Inhalt des einheitlichen Puffers mit der Modellansichtsprojektionsmatrix und der Deckkraft bei jedem Frame ändert.
m_rotation += 1.0f; QMatrix4x4 modelViewProjection = m_viewProjection; modelViewProjection.rotate(m_rotation, 0, 1, 0); resourceUpdates->updateDynamicBuffer(m_ubuf.get(), 0, 64, modelViewProjection.constData()); m_opacity += m_opacityDir * 0.005f; if (m_opacity < 0.0f || m_opacity > 1.0f) { m_opacityDir *= -1; m_opacity = qBound(0.0f, m_opacity, 1.0f); } resourceUpdates->updateDynamicBuffer(m_ubuf.get(), 64, 4, &m_opacity);
Zu Beginn der Aufzeichnung des Render-Passes wird eine QRhiCommandBuffer abgefragt und die Ausgabegröße bestimmt, die für die Einrichtung des Ansichtsfensters und die Größenänderung unserer Vollbildtextur bei Bedarf nützlich ist.
QRhiCommandBuffer *cb = m_sc->currentFrameCommandBuffer(); const QSize outputSizeInPixels = m_sc->currentPixelSize();
Das Starten eines Renderpasses impliziert das Löschen der Farb- und Tiefenschablonenpuffer des Renderziels (es sei denn, die Renderzielflags geben etwas anderes an, aber das ist nur eine Option für texturbasierte Renderziele). Hier geben wir Schwarz für Farbe, 1.0f für Tiefe und 0 für Stencil (unbenutzt) an. Das letzte Argument, resourceUpdates
, sorgt dafür, dass die im Stapel aufgezeichneten Datenaktualisierungsbefehle übertragen werden. Alternativ hätten wir stattdessen auch QRhiCommandBuffer::resourceUpdate() verwenden können. Der Rendering-Pass zielt auf eine Swapchain ab, daher wird currentFrameRenderTarget() aufgerufen, um eine gültige QRhiRenderTarget zu erhalten.
cb->beginPass(m_sc->currentFrameRenderTarget(), Qt::black, { 1.0f, 0 }, resourceUpdates);
Die Aufzeichnung des Zeichenaufrufs für das Dreieck ist einfach: Setzen Sie die Pipeline, setzen Sie die Shader-Ressourcen, setzen Sie den/die Vertex/Index-Puffer und zeichnen Sie den Zeichenaufruf auf. Hier verwenden wir eine nicht-indizierte Zeichnung mit nur 3 Scheitelpunkten.
cb->setGraphicsPipeline(m_colorPipeline.get()); cb->setShaderResources(); const QRhiCommandBuffer::VertexInput vbufBinding(m_vbuf.get(), 0); cb->setVertexInput(0, 1, &vbufBinding); cb->draw(3); cb->endPass();
Für den Aufruf setShaderResources() werden keine Argumente angegeben, was bedeutet, dass m_colorTriSrb
verwendet wird, da es mit dem aktiven QRhiGraphicsPipeline (m_colorPipeline
) verbunden war.
Wir werden nicht auf die Details des Renderings des Vollbild-Hintergrundbildes eingehen. Siehe dazu den Quellcode des Beispiels. Es lohnt sich jedoch, auf ein allgemeines Muster für die "Größenänderung" einer Textur oder Pufferressource hinzuweisen. Es gibt keine Möglichkeit, die Größe einer vorhandenen nativen Ressource zu ändern, so dass auf die Änderung einer Textur- oder Puffergröße ein Aufruf von create() folgen muss, um die zugrunde liegenden nativen Ressourcen freizugeben und neu zu erstellen. Um sicherzustellen, dass die QRhiTexture immer die erforderliche Größe hat, implementiert die Anwendung die folgende Logik. Beachten Sie, dass m_texture
während der gesamten Lebensdauer des Fensters gültig bleibt, was bedeutet, dass Objektreferenzen darauf, z. B. in einem QRhiShaderResourceBindings, weiterhin gültig sind. Nur die zugrundeliegenden nativen Ressourcen kommen und gehen mit der Zeit.
Beachten Sie auch, dass wir ein Gerätepixelverhältnis für das Bild festlegen, das dem Fenster entspricht, in das wir zeichnen. Dadurch wird sichergestellt, dass der Zeichencode DPR-agnostisch sein kann und unabhängig vom DPR das gleiche Layout erzeugt, während er gleichzeitig die zusätzlichen Pixel für eine verbesserte Wiedergabetreue nutzt.
void HelloWindow::ensureFullscreenTexture(const QSize &pixelSize, QRhiResourceUpdateBatch *u) { if (m_texture && m_texture->pixelSize() == pixelSize) return; if (!m_texture) m_texture.reset(m_rhi->newTexture(QRhiTexture::RGBA8, pixelSize)); else m_texture->setPixelSize(pixelSize); m_texture->create(); QImage image(pixelSize, QImage::Format_RGBA8888_Premultiplied); image.setDevicePixelRatio(devicePixelRatio());
Sobald eine QImage generiert wurde und das QPainter-basierte Zeichnen darin abgeschlossen ist, verwenden wir uploadTexture(), um einen Textur-Upload auf den Ressourcen-Aktualisierungsstapel aufzuzeichnen:
u->uploadTexture(m_texture.get(), image);
Siehe auch QRhi, QRhiSwapChain, QWindow, QRhiCommandBuffer, QRhiResourceUpdateBatch, QRhiBuffer, und QRhiTexture.
© 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.