Qt 3D Rendering Framegraph
Der Aspekt Qt 3D Rendering ermöglicht es, den Rendering-Algorithmus vollständig datengesteuert zu gestalten. Die kontrollierende Datenstruktur wird als Framegraph bezeichnet. Ähnlich wie das Qt 3D ECS (Entity Component System) es Ihnen ermöglicht, einen sogenannten Scenegraph zu definieren, indem Sie eine Szene aus einem Baum von Entities und Components aufbauen, ist der Framegraph ebenfalls eine Baumstruktur, die jedoch für einen anderen Zweck verwendet wird. Nämlich die Steuerung , wie die Szene gerendert wird.
Während des Renderns eines einzelnen Frames ändert ein 3D-Renderer wahrscheinlich viele Male seinen Zustand. Die Anzahl und Art dieser Zustandsänderungen hängt nicht nur davon ab, welche Materialien (Shader, Mesh-Geometrie, Texturen und einheitliche Variablen) in der Szene vorhanden sind, sondern auch davon, welches High-Level-Rendering-Schema Sie verwenden.
Die Verwendung eines herkömmlichen einfachen Vorwärts-Rendering-Schemas unterscheidet sich beispielsweise stark von der Verwendung eines Deferred-Rendering-Ansatzes. Andere Funktionen wie Reflexionen, Schatten, mehrere Ansichtsfenster und frühe Z-Fill-Passes verändern die Zustände, die ein Renderer im Laufe eines Frames setzen muss, und wann diese Zustandsänderungen stattfinden müssen.
Zum Vergleich: Der Renderer vonQt Quick 2, der für das Zeichnen von Qt Quick 2-Szenen verantwortlich ist, ist in C++ fest verdrahtet, um Dinge wie das Stapeln von Primitiven und das Rendern von undurchsichtigen Elementen, gefolgt vom Rendern von transparenten Elementen, durchzuführen. Im Fall von Qt Quick 2 ist das vollkommen in Ordnung, da es alle Anforderungen abdeckt. Wie Sie an einigen der oben aufgeführten Beispiele sehen können, ist ein solcher fest verdrahteter Renderer angesichts der Vielzahl der verfügbaren Rendering-Methoden wahrscheinlich nicht flexibel genug für allgemeine 3D-Szenen. Und selbst wenn ein Renderer so flexibel gemacht werden könnte, dass er alle diese Fälle abdeckt, würde seine Leistung wahrscheinlich darunter leiden, dass er zu allgemein ist. Erschwerend kommt hinzu, dass ständig neue Rendering-Methoden erforscht werden. Wir brauchten also einen Ansatz, der sowohl flexibel und erweiterbar als auch einfach zu benutzen und zu warten ist. Hier kommt der Framegraph ins Spiel!
Jeder Knoten im Framegraph definiert einen Teil der Konfiguration, die der Renderer zum Rendern der Szene verwenden wird. Die Position eines Knotens im Framegraph-Baum bestimmt, wann und wo der Teilbaum, der in diesem Knoten wurzelt, die aktive Konfiguration in der Rendering-Pipeline sein wird. Wie wir später sehen werden, durchläuft der Renderer diesen Baum, um den für Ihren Rendering-Algorithmus erforderlichen Zustand an jedem Punkt des Frames aufzubauen.
Wenn Sie nur einen einfachen Würfel auf dem Bildschirm rendern wollen, ist das natürlich ein Overkill. Sobald Sie jedoch anfangen, etwas komplexere Szenen zu erstellen, ist dies sehr nützlich. Für die häufigsten Fälle bietet Qt 3D einige Beispiel-Framegraphen, die sofort einsatzbereit sind.
Wir werden die Flexibilität des Framegraphen-Konzepts anhand einiger Beispiele und der daraus resultierenden Framegraphen demonstrieren.
Bitte beachten Sie, dass im Gegensatz zum Scenegraph, der aus Entitäten und Komponenten besteht, der Framegraph nur aus verschachtelten Knoten besteht, die alle Unterklassen von Qt3DRender::QFrameGraphNode sind. Das liegt daran, dass die Framegraph-Knoten keine simulierten Objekte in unserer virtuellen Welt sind, sondern eher unterstützende Informationen.
Wir werden bald sehen, wie wir unseren ersten einfachen Framegraph konstruieren, aber vorher werden wir Ihnen die verfügbaren Framegraph-Knoten vorstellen. Wie auch beim Scenegraph-Baum stimmen die QML- und C++-APIs 1 zu 1 überein, so dass Sie diejenige bevorzugen können, die Ihnen am besten gefällt. Aus Gründen der Lesbarkeit und Prägnanz wurde für diesen Artikel die QML-API gewählt.
Das Schöne am Framegraph ist, dass es durch die Kombination dieser einfachen Knotentypen möglich ist, den Renderer so zu konfigurieren, dass er Ihren speziellen Anforderungen entspricht, ohne dass Sie irgendeinen haarigen Low-Level-C/C++-Rendering-Code anfassen müssen.
FrameGraph-Regeln
Um einen korrekt funktionierenden Framegraph-Baum zu konstruieren, sollten Sie einige Regeln kennen, wie er durchlaufen wird und wie er an den Renderer Qt 3D weitergegeben werden kann.
Einstellen des Framegraphs
Der FrameGraph-Baum sollte der activeFrameGraph-Eigenschaft einer QRenderSettings-Komponente zugewiesen werden, die selbst eine Komponente der Root-Entität in der Qt 3D -Szene ist. Dies macht ihn zum aktiven Framegraph für den Renderer. Da es sich um eine QML-Eigenschaftsbindung handelt, kann der aktive Framegraph (oder Teile davon) zur Laufzeit geändert werden. Zum Beispiel, wenn Sie unterschiedliche Rendering-Ansätze für Innen- und Außenszenen verwenden oder einen speziellen Effekt aktivieren oder deaktivieren möchten.
Entity { id: sceneRoot components: RenderSettings { activeFrameGraph: ... // FrameGraph tree } }
Hinweis: activeFrameGraph ist die Standardeigenschaft der FrameGraph-Komponente in QML.
Entity { id: sceneRoot components: RenderSettings { ... // FrameGraph tree } }
Wie der Framegraph verwendet wird
- Der Renderer Qt 3D führt eine Durchquerung des Framegraph-Baums in der ersten Tiefe durch. Da die Durchquerung in der Tiefe erfolgt, ist die Reihenfolge, in der Sie die Knoten definieren, wichtig.
- Wenn der Renderer einen Blattknoten des Framegraphen erreicht, sammelt er alle Zustände, die durch den Pfad vom Blattknoten zum Wurzelknoten angegeben sind. Dadurch wird der Zustand definiert, der zum Rendern eines Abschnitts des Frames verwendet wird. Wenn Sie an den Interna von Qt 3D interessiert sind, wird diese Sammlung von Zuständen als RenderView bezeichnet.
- Angesichts der in einer RenderView enthaltenen Konfiguration sammelt der Renderer alle zu rendernden Entitäten im Scenegraph, erstellt daraus einen Satz von RenderCommands und verknüpft sie mit der RenderView.
- Die Kombination aus RenderView und einem Satz von RenderCommands wird zur Übergabe an OpenGL übergeben.
- Wenn dies für jeden Blattknoten im Framegraph wiederholt wird, ist der Frame vollständig und der Renderer ruft QOpenGLContext::swapBuffers() auf, um den Frame anzuzeigen.
Im Kern ist der Framegraph eine datengesteuerte Methode zur Konfiguration des Qt 3D Renderers. Aufgrund seiner datengesteuerten Natur können wir die Konfiguration zur Laufzeit ändern, so dass auch Nicht-C++-Entwickler oder -Designer die Struktur eines Frames ändern und neue Rendering-Ansätze ausprobieren können, ohne Tausende von Zeilen Boiler Plate Code schreiben zu müssen.
Framegraph-Beispiele
Nachdem Sie nun die Regeln kennen, die beim Schreiben eines Framegraph-Baums zu beachten sind, werden wir einige Beispiele durchgehen und sie aufschlüsseln.
Ein einfacher Forward Renderer
Beim Forward Rendering verwendet man OpenGL in seiner traditionellen Art und Weise und rendert direkt in den Backbuffer, ein Objekt nach dem anderen, wobei jedes einzelne schattiert wird, während wir gehen. Dies steht im Gegensatz zum Deferred Rendering, bei dem wir in einen dazwischenliegenden G-Buffer rendern. Hier ist ein einfacher FrameGraph, der für Forward Rendering verwendet werden kann:
Viewport { normalizedRect: Qt.rect(0.0, 0.0, 1.0, 1.0) property alias camera: cameraSelector.camera ClearBuffers { buffers: ClearBuffers.ColorDepthBuffer CameraSelector { id: cameraSelector } } }
Wie Sie sehen können, hat dieser Baum ein einzelnes Blatt und besteht aus insgesamt 3 Knoten, wie im folgenden Diagramm dargestellt.
Unter Verwendung der oben definierten Regeln ergibt dieser Framegraph-Baum eine einzelne RenderView mit der folgenden Konfiguration:
- Blattknoten -> RenderView
- Viewport, das den gesamten Bildschirm ausfüllt (verwendet normalisierte Koordinaten, um die Unterstützung verschachtelter Viewports zu erleichtern)
- Farb- und Tiefenpuffer sind so eingestellt, dass sie geleert werden
- Die Kamera wird in der exponierten Kameraeigenschaft angegeben
Mehrere verschiedene FrameGraph-Bäume können das gleiche Rendering-Ergebnis liefern. Solange der Zustand, der vom Blatt bis zur Wurzel gesammelt wird, derselbe ist, wird auch das Ergebnis dasselbe sein. Es ist am besten, den Zustand, der am längsten konstant bleibt, näher an die Wurzel des FrameGraphs zu legen, da dies zu weniger Blattknoten und damit zu weniger RenderViews insgesamt führt.
Viewport { normalizedRect: Qt.rect(0.0, 0.0, 1.0, 1.0) property alias camera: cameraSelector.camera CameraSelector { id: cameraSelector ClearBuffers { buffers: ClearBuffers.ColorDepthBuffer } } }
CameraSelector { Viewport { normalizedRect: Qt.rect(0.0, 0.0, 1.0, 1.0) ClearBuffers { buffers: ClearBuffers.ColorDepthBuffer } } }
Ein FrameGraph mit mehreren Ansichtsfenstern
Gehen wir nun zu einem etwas komplexeren Beispiel über, das einen Scenegraph aus der Sicht von 4 virtuellen Kameras in die 4 Quadranten des Fensters rendert. Dies ist eine übliche Konfiguration für 3D-CAD- oder Modellierungstools oder könnte angepasst werden, um bei der Darstellung eines Rückspiegels in einem Autorennspiel oder einer CCTV-Kameraanzeige zu helfen.
Viewport { id: mainViewport normalizedRect: Qt.rect(0, 0, 1, 1) property alias Camera: cameraSelectorTopLeftViewport.camera property alias Camera: cameraSelectorTopRightViewport.camera property alias Camera: cameraSelectorBottomLeftViewport.camera property alias Camera: cameraSelectorBottomRightViewport.camera ClearBuffers { buffers: ClearBuffers.ColorDepthBuffer } Viewport { id: topLeftViewport normalizedRect: Qt.rect(0, 0, 0.5, 0.5) CameraSelector { id: cameraSelectorTopLeftViewport } } Viewport { id: topRightViewport normalizedRect: Qt.rect(0.5, 0, 0.5, 0.5) CameraSelector { id: cameraSelectorTopRightViewport } } Viewport { id: bottomLeftViewport normalizedRect: Qt.rect(0, 0.5, 0.5, 0.5) CameraSelector { id: cameraSelectorBottomLeftViewport } } Viewport { id: bottomRightViewport normalizedRect: Qt.rect(0.5, 0.5, 0.5, 0.5) CameraSelector { id: cameraSelectorBottomRightViewport } } }
Dieser Baum ist mit 5 Blättern etwas komplexer. Nach den gleichen Regeln wie zuvor konstruieren wir 5 RenderView-Objekte aus dem FrameGraph. Die folgenden Diagramme zeigen die Konstruktion für die ersten beiden RenderViews. Die restlichen RenderViews sind dem zweiten Diagramm sehr ähnlich, nur mit den anderen Teilbäumen.
Insgesamt werden die folgenden RenderViews erstellt:
- RenderView (1)
- Fullscreen viewport definiert
- Farb- und Tiefenpuffer sind so eingestellt, dass sie geleert werden
- RenderView (2)
- Vollbild-Ansichtsfenster definiert
- Sub-Viewport definiert (Rendering-Viewport wird relativ zu seinem Parent skaliert)
- CameraSelector angegeben
- RenderView (3)
- Vollbild-Ansichtsfenster definiert
- Sub-Viewport definiert (Rendering-Viewport wird relativ zu seinem Parent skaliert)
- CameraSelector angegeben
- RenderView (4)
- Vollbild-Ansichtsfenster definiert
- Sub-Viewport definiert (Rendering-Viewport wird relativ zu seinem Parent skaliert)
- CameraSelector angegeben
- RenderView (5)
- Vollbild-Ansichtsfenster definiert
- Sub-Viewport definiert (Rendering-Viewport wird relativ zu seinem Parent skaliert)
- CameraSelector angegeben
In diesem Fall ist jedoch die Reihenfolge wichtig. Wenn der Knoten ClearBuffers der letzte statt der erste wäre, würde dies zu einem schwarzen Bildschirm führen, weil alles gelöscht würde, nachdem es so sorgfältig gerendert wurde. Aus einem ähnlichen Grund könnte er nicht als Wurzel des FrameGraphs verwendet werden, da dies zu einem Aufruf zum Löschen des gesamten Bildschirms für jedes unserer Ansichtsfenster führen würde.
Obwohl die Reihenfolge der Deklaration des FrameGraphs wichtig ist, ist Qt 3D in der Lage, jede RenderView parallel zu verarbeiten, da jede RenderView unabhängig von den anderen ist, um eine Reihe von RenderCommands zu erzeugen, die übermittelt werden, während der Zustand der RenderView in Kraft ist.
Qt 3D verwendet einen aufgabenbasierten Ansatz zur Parallelisierung, der natürlich mit der Anzahl der verfügbaren Kerne skaliert. Dies ist im folgenden Diagramm für das vorherige Beispiel dargestellt.
Die RenderCommands für die RenderViews können parallel auf vielen Kernen generiert werden, und solange wir darauf achten, die RenderViews in der richtigen Reihenfolge an den dedizierten OpenGL-Submission-Thread zu senden, wird die resultierende Szene korrekt gerendert.
Aufgeschobener Renderer
Was das Rendering betrifft, so ist das Deferred Rendering in Bezug auf die Renderer-Konfiguration ein ganz anderes Kaliber als das Forward Rendering. Anstatt jedes Mesh zu zeichnen und einen Shader-Effekt anzuwenden, um es zu schattieren, wird beim Deferred Rendering eine Methode mit zwei Render-Passes angewandt.
Zunächst werden alle Meshes in der Szene mit demselben Shader gezeichnet, der normalerweise für jedes Fragment mindestens vier Werte ausgibt:
- Weltnormalvektor
- Farbe (oder einige andere Materialeigenschaften)
- Tiefe
- Weltlagevektor
Jeder dieser Werte wird in einer Textur gespeichert. Die Normalen-, Farb-, Tiefen- und Positionstexturen bilden den so genannten G-Puffer. Während des ersten Durchlaufs wird nichts auf den Bildschirm gezeichnet, sondern in den G-Buffer gezeichnet, der für die spätere Verwendung bereit ist.
Sobald alle Meshes gezeichnet wurden, wird der G-Buffer mit allen Meshes gefüllt, die derzeit von der Kamera gesehen werden können. Im zweiten Rendering-Durchgang wird die Szene dann mit der endgültigen Farbschattierung in den Back-Buffer gerendert, indem die Normal-, Farb- und Positionswerte aus den G-Buffer-Texturen ausgelesen und eine Farbe auf ein Vollbild-Quad ausgegeben wird.
Der Vorteil dieser Technik besteht darin, dass die hohe Rechenleistung, die für komplexe Effekte erforderlich ist, im zweiten Durchgang nur für die Elemente verwendet wird, die tatsächlich von der Kamera gesehen werden. Der erste Durchgang kostet nicht viel Rechenleistung, da jedes Mesh mit einem einfachen Shader gezeichnet wird. Deferred Rendering entkoppelt also Shading und Beleuchtung von der Anzahl der Objekte in einer Szene und koppelt sie stattdessen an die Auflösung des Bildschirms (und des G-Buffers). Dies ist eine Technik, die in vielen Spielen verwendet wurde, da sie die Möglichkeit bietet, eine große Anzahl dynamischer Lichter auf Kosten des zusätzlichen GPU-Speicherverbrauchs zu verwenden.
Viewport { id: root normalizedRect: Qt.rect(0.0, 0.0, 1.0, 1.0) property GBuffer gBuffer property alias camera: sceneCameraSelector.camera property alias sceneLayer: sceneLayerFilter.layers property alias screenQuadLayer: screenQuadLayerFilter.layers RenderSurfaceSelector { CameraSelector { id: sceneCameraSelector // Fill G-Buffer LayerFilter { id: sceneLayerFilter RenderTargetSelector { id: gBufferTargetSelector target: gBuffer ClearBuffers { buffers: ClearBuffers.ColorDepthBuffer RenderPassFilter { id: geometryPass matchAny: FilterKey { name: "pass" value: "geometry" } } } } } TechniqueFilter { parameters: [ Parameter { name: "color"; value: gBuffer.color }, Parameter { name: "position"; value: gBuffer.position }, Parameter { name: "normal"; value: gBuffer.normal }, Parameter { name: "depth"; value: gBuffer.depth } ] RenderStateSet { // Render FullScreen Quad renderStates: [ BlendEquation { blendFunction: BlendEquation.Add }, BlendEquationArguments { sourceRgb: BlendEquationArguments.SourceAlpha destinationRgb: BlendEquationArguments.DestinationColor } ] LayerFilter { id: screenQuadLayerFilter ClearBuffers { buffers: ClearBuffers.ColorDepthBuffer RenderPassFilter { matchAny: FilterKey { name: "pass" value: "final" } parameters: Parameter { name: "winSize" value: Qt.size(1024, 768) } } } } } } } } }
(Der obige Code wurde aus qt3d/tests/manual/deferred-renderer-qml übernommen).
Grafisch sieht der resultierende Framegraph wie folgt aus:
Und die resultierenden RenderViews sind:
- RenderView (1)
- Festlegen, welche Kamera verwendet werden soll
- Definieren Sie einen Viewport, der den gesamten Bildschirm ausfüllt
- Alle Entities für die Layer-Komponente sceneLayer auswählen
gBuffer
als aktives Renderziel festlegen- Farbe und Tiefe des aktuell gebundenen Renderziels (
gBuffer
) löschen - Wählen Sie nur Entities in der Szene aus, die ein Material und eine Technik haben, die mit den Anmerkungen in der RenderPassFilter
- RenderView (2)
- Definieren Sie einen Viewport, der den gesamten Bildschirm ausfüllt
- Wählen Sie alle Entities für die Ebenenkomponente screenQuadLayer
- Lösche die Farb- und Tiefenpuffer auf dem aktuell gebundenen Framebuffer (dem Bildschirm)
- Wählen Sie nur Entities in der Szene aus, die ein Material und eine Technik haben, die mit den Anmerkungen in der RenderPassFilter
Weitere Vorteile des Framegraphs
Da der FrameGraph-Baum vollständig datengesteuert ist und zur Laufzeit dynamisch modifiziert werden kann, können Sie:
- Verschiedene Framegraph-Bäume für verschiedene Plattformen und Hardware und Auswahl des am besten geeigneten zur Laufzeit
- Einfaches Hinzufügen und Aktivieren von visuellem Debugging in einer Szene
- unterschiedliche FrameGraph-Bäume verwenden, je nachdem, was Sie für einen bestimmten Bereich der Szene rendern müssen
- Implementieren Sie eine neue Rendering-Technik, ohne die Interna von Qt 3D ändern zu müssen.
Schlussfolgerung
Wir haben den FrameGraph und die Knotentypen, aus denen er sich zusammensetzt, vorgestellt. Anschließend haben wir einige Beispiele besprochen, um die Regeln für den Aufbau des FrameGraphs zu veranschaulichen und zu zeigen, wie die Engine Qt 3D den FrameGraph hinter den Kulissen verwendet. Inzwischen sollten Sie einen ziemlich guten Überblick über den FrameGraph haben und wissen, wie er verwendet werden kann (z. B. um einen frühen Z-Fill-Pass zu einem Forward Renderer hinzuzufügen). Außerdem sollten Sie immer im Hinterkopf behalten, dass der FrameGraph ein Werkzeug ist, das Sie verwenden können, so dass Sie nicht an den mitgelieferten Renderer und die Materialien gebunden sind, die Qt 3D von Haus aus bietet.
© 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.