Auf dieser Seite

Qt 3D Render-Framegraph

Der Render-Aspekt von „ Qt 3D “ ermöglicht es, den Rendering-Algorithmus vollständig datengesteuert zu gestalten. Die steuernde Datenstruktur wird als Framegraph bezeichnet. Ähnlich wie das ECS (Entity-Component-System) von „ Qt 3D “, mit dem man einen sogenannten „Scenegraph“ definieren kann, indem man eine Szene aus einem Baum aus Entitäten und Komponenten aufbaut, ist auch der „Framegraph“ eine Baumstruktur, die jedoch einem anderen Zweck dient. Nämlich der Steuerung, wie die Szene gerendert wird.

Im Verlauf des Renderings eines einzelnen Frames wechselt ein 3D-Renderer wahrscheinlich mehrfach 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 vorkommen, sondern auch davon, welches übergeordnete Rendering-Schema du verwendest.

Beispielsweise unterscheidet sich die Verwendung eines traditionellen, einfachen Forward-Rendering-Schemas erheblich von der Verwendung eines Deferred-Rendering-Ansatzes. Andere Funktionen wie Reflexionen, Schatten, mehrere Viewports und frühe Z-Fill-Durchläufe beeinflussen alle, welche Zustände ein Renderer im Verlauf eines Frames einstellen muss und wann diese Zustandsänderungen erfolgen müssen.

Zum Vergleich: Der „Qt Quick -2“-Scenegraph-Renderer, der für das Zeichnen der Szenen in „ Qt Quick -2“ zuständig ist, ist fest in C++ programmiert, um beispielsweise Primitive zu bündeln und undurchsichtige Objekte vor dem Rendern transparenter Objekte darzustellen. Im Fall von „ Qt Quick -2“ ist das völlig in Ordnung, da dies alle Anforderungen abdeckt. Wie aus einigen der oben aufgeführten Beispiele ersichtlich ist, dürfte ein solcher fest programmierter Renderer angesichts der Vielzahl verfügbarer Rendering-Methoden für generische 3D-Szenen wahrscheinlich nicht flexibel genug sein. Oder wenn ein Renderer flexibel genug gestaltet werden könnte, um alle derartigen Fälle abzudecken, würde seine Leistung wahrscheinlich darunter leiden, dass er zu allgemein gehalten ist. Erschwerend kommt hinzu, dass ständig neue Rendering-Methoden erforscht werden. Wir benötigten daher einen Ansatz, der sowohl flexibel als auch erweiterbar ist und sich gleichzeitig einfach nutzen und warten lässt. Hier kommt der Framegraph ins Spiel!

Jeder Knoten im Framegraph definiert einen Teil der Konfiguration, die der Renderer zum Rendern der Szene verwendet. Die Position eines Knotens im Framegraph-Baum bestimmt, wann und wo der an diesem Knoten verwurzelte Teilbaum die aktive Konfiguration in der Rendering-Pipeline darstellt. Wie wir später sehen werden, durchläuft der Renderer diesen Baum, um an jedem Punkt des Frames den für Ihren Rendering-Algorithmus erforderlichen Zustand aufzubauen.

Wenn Sie lediglich einen einfachen Würfel auf dem Bildschirm rendern möchten, erscheint Ihnen das natürlich vielleicht übertrieben. Sobald Sie jedoch etwas komplexere Szenen erstellen möchten, erweist sich dies als sehr nützlich. Für die gängigen Fälle stellt Qt 3D einige Beispiel-Framegraphs zur Verfügung, die sofort einsatzbereit sind.

Wir werden die Flexibilität des Framegraph-Konzepts anhand einiger Beispiele und der daraus resultierenden Framegraphs veranschaulichen.

Bitte beachte, dass sich der Framegraph im Gegensatz zum Scenegraph, der aus Entitäten und Komponenten besteht, ausschließlich aus verschachtelten Knoten zusammensetzt, die alle Unterklassen von Qt3DRender::QFrameGraphNode sind. Das liegt daran, dass die Framegraph-Knoten keine simulierten Objekte in unserer virtuellen Welt sind, sondern vielmehr unterstützende Informationen darstellen.

Wir werden gleich sehen, wie wir unseren ersten einfachen Framegraph erstellen, doch zuvor stellen wir Ihnen die verfügbaren Framegraph-Knoten vor. Ebenso wie beim Scenegraph-Baum stimmen die QML- und C++-APIs eins zu eins überein, sodass Sie diejenige verwenden 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 ganz nach Ihren spezifischen Anforderungen zu konfigurieren, ohne sich überhaupt mit kompliziertem, low-level C/C++-Rendering-Code befassen zu müssen.

FrameGraph-Regeln

Um einen korrekt funktionierenden FrameGraph-Baum aufzubauen, sollten Sie einige Regeln kennen, wie dieser durchlaufen wird und wie Sie ihn dem „ Qt 3D “-Renderer übergeben.

Einrichten des FrameGraphs

Der FrameGraph-Baum sollte der Eigenschaft `activeFrameGraph` einer `QRenderSettings`-Komponente zugewiesen werden, die wiederum eine Komponente der Stammentität in der „ Qt 3D “-Szene ist. Dadurch wird er zum aktiven FrameGraph für den Renderer. Da es sich hierbei um eine QML-Eigenschaftsbindung handelt, kann der aktive Framegraph (oder Teile davon) natürlich zur Laufzeit dynamisch geändert werden. Beispielsweise, wenn Sie unterschiedliche Rendering-Ansätze für Innen- und Außenszenen verwenden oder einen bestimmten Spezialeffekt aktivieren bzw. deaktivieren möchten.

Entity {
    id: sceneRoot
    components: RenderSettings {
         activeFrameGraph: ... // FrameGraph tree
    }
}

Hinweis: „activeFrameGraph“ ist die Standard-Eigenschaft der FrameGraph-Komponente in QML.

Entity {
    id: sceneRoot
    components: RenderSettings {
         ... // FrameGraph tree
    }
}

Verwendung des Framegraphs

  • Der „ Qt 3D “-Renderer führt eine tiefenspezifische Durchquerung des FrameGraph-Baums durch. Beachten Sie, dass aufgrund der tiefenspezifischen Durchquerung die Reihenfolge, in der Sie Knoten definieren, von Bedeutung ist.
  • Wenn der Renderer einen Blattknoten des FrameGraphs erreicht, sammelt er alle Zustände, die durch den Pfad vom Blattknoten zum Wurzelknoten festgelegt sind. Dies definiert den Zustand, der zum Rendern eines Abschnitts des Frames verwendet wird. Falls Sie sich für die internen Abläufe von „ Qt 3D “ interessieren: Diese Sammlung von Zuständen wird als „RenderView“ bezeichnet.
  • Anhand der in einer „RenderView“ enthaltenen Konfiguration sammelt der Renderer alle zu rendernden Entitäten im Szenengraphen, erstellt daraus eine Reihe von „RenderCommands“ und ordnet diese der „RenderView“ zu.
  • Die Kombination aus RenderView und der Menge an RenderCommands wird zur Übergabe an OpenGL weitergeleitet.
  • Wird dies für jeden Blattknoten im Framegraph wiederholt, 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. Dank seines datengesteuerten Charakters können wir die Konfiguration zur Laufzeit ändern, Nicht-C++-Entwicklern oder Designern ermöglichen, die Struktur eines Frames zu ändern, und neue Rendering-Ansätze ausprobieren, ohne Tausende von Zeilen Boilerplate-Code schreiben zu müssen.

Beispiele für Framegraphs

Nachdem Sie nun die Regeln kennen, die beim Erstellen eines Framegraph-Baums zu beachten sind, werden wir einige Beispiele durchgehen und diese näher erläutern.

Ein einfacher Forward-Renderer

Beim Forward-Rendering wird OpenGL auf herkömmliche Weise verwendet und direkt in den Backbuffer gerendert, wobei jeweils ein Objekt nach dem anderen schattiert wird. Dies steht im Gegensatz zum Deferred Rendering, bei dem in einen zwischengeschalteten G-Buffer gerendert wird. Hier ist ein einfacher FrameGraph, der für das 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 du sehen kannst, hat dieser Baum ein einziges Blatt und besteht aus insgesamt 3 Knoten, wie in der folgenden Abbildung dargestellt.

Diagramm, das veranschaulicht, wie die Rendering-Schritte zu einer RenderView mit Viewport, TechniqueFilter und CameraSelector zusammengeführt werden

Unter Anwendung der oben definierten Regeln ergibt dieser FrameGraph-Baum eine einzige RenderView mit der folgenden Konfiguration:

  • Blattknoten -> RenderView
    • Viewport, der den gesamten Bildschirm ausfüllt (verwendet normalisierte Koordinaten, um die Unterstützung verschachtelter Viewports zu vereinfachen)
    • Farb- und Tiefenpuffer sind so eingestellt, dass sie gelöscht werden
    • Kamera, die in der exponierten Kameraeigenschaft angegeben ist

Mehrere verschiedene FrameGraph-Bäume können zum gleichen Renderergebnis führen. Solange der von den Blattknoten bis zur Wurzel erfasste Zustand identisch ist, ist auch das Ergebnis identisch. Es empfiehlt sich, Zustände, die am längsten konstant bleiben, näher an der Wurzel des FrameGraphs zu platzieren, da dies zu weniger Blattknoten und somit insgesamt zu weniger RenderViews 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

Wenden wir uns nun einem etwas komplexeren Beispiel zu, bei dem ein Scenegraph aus der Perspektive von vier virtuellen Kameras in die vier Quadranten des Fensters gerendert wird. Dies ist eine gängige Konfiguration für 3D-CAD- oder Modellierungstools und könnte angepasst werden, um beispielsweise das Rendern eines Rückspiegels in einem Autorennspiel oder die Anzeige einer CCTV-Kamera zu unterstützen.

Vier Kameraansichten einer 3D-Szene mit Pflanzkübeln, Fässern, einer Skulptur eines Knutt-Strandläufers und einer Schatzkiste

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 fünf Blättern etwas komplexer. Nach denselben Regeln wie zuvor erstellen wir fünf RenderView-Objekte aus dem FrameGraph. Die folgenden Diagramme zeigen den Aufbau der ersten beiden RenderViews. Die übrigen RenderViews sind dem zweiten Diagramm sehr ähnlich, nur mit den anderen Unterbäumen.

Diagramm, das vier Kamera-Ansichtsfenster zeigt, die zu einer „RenderView“ mit einem Ansichtsfenster und einem Clear-Puffer zusammengefügt wurden

Diagramm, das den Ansichtsbereich oben links und die Kameraauswahl in einer einzigen Renderansicht vereint

Insgesamt wurden folgende RenderViews erstellt:

  • RenderView (1)
    • Vollbild-Viewport definiert
    • Farb- und Tiefenpuffer sind so eingestellt, dass sie geleert werden
  • RenderView (2)
    • Vollbild-Ansichtsfenster definiert
    • Unteransichtsbereich definiert (der Rendering-Ansichtsbereich wird relativ zu seinem übergeordneten Bereich skaliert)
    • Kameraauswahl festgelegt
  • RenderView (3)
    • Vollbild-Ansichtsbereich definiert
    • Unteransicht definiert (die Renderansicht wird relativ zu ihrer übergeordneten Ansicht skaliert)
    • Kameraauswahl festgelegt
  • RenderView (4)
    • Vollbild-Ansichtsbereich definiert
    • Unteransicht definiert (die Renderansicht wird relativ zu ihrer übergeordneten Ansicht skaliert)
    • Kameraauswahl festgelegt
  • RenderView (5)
    • Vollbild-Ansichtsfenster definiert
    • Unteransicht definiert (die Renderansicht wird relativ zu ihrer übergeordneten Ansicht skaliert)
    • Kameraauswahl festgelegt

In diesem Fall ist jedoch die Reihenfolge entscheidend. Würde der „ClearBuffers“-Knoten an letzter statt an erster Stelle stehen, würde dies zu einem schwarzen Bildschirm führen – aus dem einfachen Grund, dass alles unmittelbar nach dem sorgfältigen Rendern wieder gelöscht würde. Aus einem ähnlichen Grund könnte er nicht als Wurzel des FrameGraphs verwendet werden, da dies dazu führen würde, dass für jeden unserer Viewports ein Aufruf zum Löschen des gesamten Bildschirms erfolgen müsste.

Obwohl die Deklarationsreihenfolge 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 generieren, die übermittelt werden, solange der Zustand der RenderView gültig ist.

Qt 3D verwendet einen aufgabenbasierten Ansatz zur Parallelisierung, der sich auf natürliche Weise mit der Anzahl der verfügbaren Kerne skaliert. Dies wird im folgenden Diagramm für das vorherige Beispiel veranschaulicht.

Diagramm, das veranschaulicht, wie der Jon-Manager in fünf Renderansichten unterteilt ist

Die RenderCommands für die RenderViews können parallel über viele Kerne hinweg generiert werden, und solange darauf geachtet wird, die RenderViews in der richtigen Reihenfolge im dedizierten OpenGL-Übermittlungs-Thread zu übermitteln, wird die resultierende Szene korrekt gerendert.

Deferred Renderer

Was das Rendern betrifft, ist das Deferred Rendering hinsichtlich der Renderer-Konfiguration etwas ganz anderes als das Forward Rendering. Anstatt jedes Mesh zu zeichnen und einen Shader-Effekt zur Schattierung anzuwenden, nutzt das Deferred Rendering eine Methode mit zwei Render-Durchläufen.

Zunächst werden alle Meshes in der Szene mit demselben Shader gezeichnet, der – in der Regel für jedes Fragment – mindestens vier Werte ausgibt:

  • Welt-Normalenvektor
  • Farbe (oder andere Materialeigenschaften)
  • Tiefe
  • Weltpositionsvektor

Jeder dieser Werte wird in einer Textur gespeichert. Die Normal-, Farb-, Tiefen- und Positionstxturen bilden zusammen den sogenannten G-Buffer. Während des ersten Durchgangs wird nichts auf dem Bildschirm gezeichnet, sondern in den G-Buffer geschrieben, um später verwendet zu werden.

Sobald alle Meshes gezeichnet wurden, ist der G-Buffer mit allen Meshes gefüllt, die derzeit von der Kamera erfasst werden können. Der zweite Renderdurchgang wird dann verwendet, um die Szene mit der endgültigen Farbschattierung in den Backbuffer zu rendern, indem die Normal-, Farb- und Positionswerte aus den G-Buffer-Texturen gelesen und eine Farbe auf ein Vollbild-Quad ausgegeben wird.

Der Vorteil dieser Technik besteht darin, dass die für komplexe Effekte erforderliche hohe Rechenleistung erst im zweiten Durchlauf und nur für die Elemente genutzt wird, die tatsächlich von der Kamera erfasst werden. Der erste Durchlauf beansprucht kaum Rechenleistung, da jedes Mesh mit einem einfachen Shader gezeichnet wird. Das Deferred Rendering entkoppelt somit 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). Diese Technik wird in vielen Spielen eingesetzt, da sie die Verwendung einer großen Anzahl dynamischer Lichtquellen ermöglicht – allerdings auf Kosten eines zusätzlichen GPU-Speicherbedarfs.

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 ist eine Adaption aus qt3d/tests/manual/deferred-renderer-qml.)

Grafisch sieht der resultierende Framegraph wie folgt aus:

Flussdiagramm eines Framegraphs für verzögertes Rendering mit Verzweigungen für Szene und Bildschirmquadrant

Und die resultierenden RenderViews sind:

  • RenderView (1)
    • Legen Sie fest, welche Kamera verwendet werden soll
    • Definieren Sie einen Viewport, der den gesamten Bildschirm ausfüllt
    • Wählen Sie alle Entitäten der Layer-Komponente „sceneLayer“ aus
    • Legen Sie das „ gBuffer “ als aktives Renderziel fest
    • Lösche die Farbe und die Tiefe auf dem aktuell gebundenen Renderziel (dem „ gBuffer “)
    • Wähle nur Entitäten in der Szene aus, deren Material und Technik mit den Angaben im „RenderPassFilter“ übereinstimmen
  • RenderView (2)
    • Definieren Sie einen Ansichtsbereich, der den gesamten Bildschirm ausfüllt
    • Wähle alle Entitäten für die Layer-Komponente `screenQuadLayer` aus
    • Lösche den Farb- und Tiefenpuffer auf dem aktuell gebundenen Framebuffer (dem Bildschirm)
    • Wähle nur Entitäten in der Szene aus, deren Material und Technik mit den Angaben im RenderPassFilter übereinstimmen

Weitere Vorteile des FrameGraphs

Da der FrameGraph-Baum vollständig datengesteuert ist und zur Laufzeit dynamisch geändert werden kann, haben Sie folgende Möglichkeiten:

  • unterschiedliche FrameGraph-Bäume für verschiedene Plattformen und Hardware verwenden und zur Laufzeit den am besten geeigneten auswählen
  • visuelles Debugging in einer Szene einfach hinzufügen und aktivieren
  • Je nach Art der Darstellung, die Sie für einen bestimmten Bereich der Szene benötigen, unterschiedliche FrameGraph-Bäume verwenden
  • eine neue Rendering-Technik implementieren, ohne die Interna von „ Qt 3D “ ändern zu müssen

Fazit

Wir haben den FrameGraph und die Knotentypen vorgestellt, aus denen er besteht. Anschließend haben wir anhand einiger Beispiele die Regeln zur Erstellung des FrameGraphs erläutert und gezeigt, wie die „ Qt 3D “-Engine den FrameGraph hinter den Kulissen nutzt. Inzwischen sollten Sie einen guten Überblick über den FrameGraph und seine Einsatzmöglichkeiten haben (beispielsweise zum Hinzufügen eines frühen Z-Fill-Durchgangs zu einem Forward-Renderer). Außerdem sollten Sie stets bedenken, dass der FrameGraph ein Werkzeug ist, das Sie nutzen können, um nicht an den vordefinierten Renderer und die Materialien gebunden zu sein, die „ Qt 3D “ standardmäßig bereitstellt.

© 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.