Qt 3D Render Framegraph
Аспект Qt 3D Render позволяет полностью задавать алгоритм отрисовки данными. Управляющая структура данных известна как framegraph. Подобно тому, как Qt 3D ECS (система сущностей и компонентов) позволяет определять так называемый Scenegraph, создавая сцену из дерева сущностей и компонентов, framegraph также представляет собой древовидную структуру, но предназначенную для другой цели. А именно, управления способом отрисовки сцены.
В процессе отрисовки одного кадра 3D-рендерер, вероятно, многократно будет менять состояние. Количество и характер этих изменений зависят не только от того, какие материалы (шейдеры, геометрия мешей, текстуры и переменные униформы) находятся в сцене, но и от того, какую схему рендеринга высокого уровня вы используете.
Например, использование традиционной простой схемы прямого рендеринга сильно отличается от подхода отложенного рендеринга. Другие функции, такие как отражения, тени, несколько портов вывода и проход по Z-буферу на ранних стадиях, также изменяют состояние, которое рендерер должен установить в течение кадра и когда эти изменения состояния должны произойти.
В качестве сравнения, рендерер scenegraph Qt Quick 2, отвечающий за отрисовку сцен Qt Quick 2, жёстко запрограммирован на C++ для выполнения таких задач, как объединение примитивов и отрисовка непрозрачных элементов, за которыми следует отрисовка прозрачных элементов. В случае Qt Quick 2 это вполне приемлемо, так как это покрывает все требования. Как вы можете видеть из некоторых приведённых выше примеров, такой жёстко запрограммированный рендерер, вероятно, недостаточно гибкий для общих 3D-сцен ввиду множества доступных методов рендеринга. Или если рендерер можно сделать достаточно гибким, чтобы охватить все такие случаи, его производительность, скорее всего, пострадает от слишком общей реализации. К тому же, всё время разрабатываются новые методы рендеринга. Поэтому нам нужен подход, который является одновременно гибким и расширяемым, одновременно простым в использовании и поддержке. И вот framegraph!
Каждый узел в framegraph определяет часть конфигурации, которую рендерер будет использовать для отрисовки сцены. Положение узла в дереве framegraph определяет, когда и где поддерево, укоренённое в этом узле, будет активной конфигурацией в конвейере рендеринга. Как мы увидим позже, рендерер обходит это дерево, чтобы собрать состояние, необходимое для вашего алгоритма рендеринга в каждой точке кадра.
Очевидно, если вы просто хотите отобразить на экране простой куб, вы можете подумать, что это избыточно. Однако, как только вы захотите начать создавать немного более сложные сцены, это оказывается полезным. Для распространённых случаев Qt 3D предоставляет некоторые примеры framegraph, готовые к использованию сразу после распаковки.
Мы продемонстрируем гибкость концепции framegraph, представив несколько примеров и получившиеся framegraph.
Обратите внимание, что в отличие от Scenegraph, который состоит из сущностей и компонентов, framegraph состоит только из вложенных узлов, которые все являются подклассами Qt3DRender::QFrameGraphNode. Это связано с тем, что узлы framegraph не являются моделируемыми объектами в нашем виртуальном мире, а представляют собой вспомогательную информацию.
Мы вскоре увидим, как построить наш первый простой framegraph, но прежде мы представим доступные вам узлы framegraph. Кроме того, как и в случае с деревом Scenegraph, API QML и C++ соответствуют 1 к 1, поэтому вы можете использовать тот, который вам больше нравится. В целях удобочитаемости и краткости в этой статье был выбран API QML.
Прелесть framegraph заключается в том, что объединяя эти простые типы узлов, можно настроить рендерер в соответствии с вашими конкретными потребностями, вообще не касаясь сложного, низкоуровневого кода рендеринга на C/C++.
Правила FrameGraph
Чтобы построить правильно функционирующее дерево framegraph, вы должны знать несколько правил о том, как оно обходится и как передать его рендереру Qt 3D.
Установка FrameGraph
Дерево FrameGraph должно быть назначено свойству activeFrameGraph компонента QRenderSettings, который сам является компонентом корневой сущности в сцене Qt 3D. Это делает его активным framegraph для рендерера. Конечно, поскольку это привязка свойства QML, активный framegraph (или части его) можно изменять в реальном времени. Например, если вы хотите использовать разные подходы к рендерингу для внутренних и внешних сцен или включить или выключить некоторый специальный эффект.
Entity {
id: sceneRoot
components: RenderSettings {
activeFrameGraph: ... // FrameGraph tree
}
} Примечание: activeFrameGraph — это свойство по умолчанию компонента FrameGraph в QML.
Entity {
id: sceneRoot
components: RenderSettings {
... // FrameGraph tree
}
} Как используется FrameGraph
- Qt 3D рендерер выполняет поиск в глубину по дереву framegraph. Обратите внимание, что, поскольку обход выполняется в глубину, важен порядок определения узлов.
- Когда рендерер достигает листового узла framegraph, он собирает всё состояние, указанное путём от листового узла до корневого узла. Это определяет состояние, используемое для отрисовки части кадра. Если вас интересуют внутренности Qt 3D, эта совокупность состояния называется RenderView.
- Исходя из конфигурации, содержащейся в RenderView, рендерер собирает все сущности в Scenegraph для отрисовки и из них создаёт набор RenderCommands и связывает их с RenderView.
- Комбинация RenderView и набора RenderCommands передаётся для передачи в OpenGL.
- Когда это повторяется для каждого листового узла в framegraph, кадр завершается, и рендерер вызывает QOpenGLContext::swapBuffers() для отображения кадра.
В основе своей framegraph — это метод конфигурирования Qt 3D рендерера на основе данных. Благодаря своей основе на данных, мы можем изменять конфигурацию в реальном времени, позволить разработчикам или дизайнерам, не работающим на C++, менять структуру кадра и пробовать новые подходы к рендерингу, не написав тысячи строк кода шаблонов.
Примеры Framegraph
Теперь, когда вы знаете правила, которым следует придерживаться при написании дерева framegraph, мы рассмотрим несколько примеров и разберём их.
Простой рендерер прямого отображения
Прямой рендеринг — это когда вы используете OpenGL традиционным способом и рисуете напрямую в задний буфер один объект за раз, затеняя каждый из них по мере обработки. Это противоположно отложенному рендерингу, где мы рисуем во вспомогательный G-буфер. Вот простой FrameGraph, который можно использовать для прямого рендеринга:
Viewport {
normalizedRect: Qt.rect(0.0, 0.0, 1.0, 1.0)
property alias camera: cameraSelector.camera
ClearBuffers {
buffers: ClearBuffers.ColorDepthBuffer
CameraSelector {
id: cameraSelector
}
}
} Как вы можете видеть, это дерево имеет один листовой узел и состоит из 3 узлов в целом, как показано на следующей диаграмме.
Используя правила, определённые выше, это дерево framegraph даёт одну RenderView с следующей конфигурацией:
- Листовой узел -> RenderView
- Порт вывода, заполняющий весь экран (использует нормализованные координаты, чтобы легко поддерживать вложенные порты вывода)
- Буферы цвета и глубины устанавливаются для очистки
- Указана камера в свойстве exposed camera
Несколько разных деревьев FrameGraph могут давать один и тот же результат рендеринга. Пока состояние, собираемое от листа к корню, одинаково, результат также будет одинаковым. Лучше всего помещать состояние, которое остаётся неизменным дольше, ближе к корню framegraph, так как это приведёт к меньшему количеству листовых узлов и, следовательно, меньшему количеству RenderView в целом.
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
}
}
} FrameGraph с несколькими портами вывода
Перейдём к несколько более сложному примеру, который отображает Scenegraph с точки зрения 4 виртуальных камер в 4 квадрантах окна. Это распространённая конфигурация для 3D-CAD или инструментов моделирования или может быть настроена для помощи в отрисовке заднего зеркала в гоночной игре или дисплея камер CCTV.

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 }
}
} Это дерево немного сложнее с 5 листовыми узлами. Следуя тем же правилам, что и раньше, мы создаём 5 объектов RenderView из FrameGraph. Следующие диаграммы показывают построение для первых двух RenderView. Остальные RenderView очень похожи на вторую диаграмму, просто с другими поддеревьями.
В полном объёме созданные RenderView:
- RenderView (1)
- Определён полноэкранный порт вывода
- Буферы цвета и глубины устанавливаются для очистки
- RenderView (2)
- Определён полноэкранный порт вывода
- Определён дочерний порт вывода (порт отрисовки будет масштабироваться относительно родительского)
- CameraSelector указан
- RenderView (3)
- Определён полноэкранный порт вывода
- Определён дочерний порт вывода (порт отрисовки будет масштабироваться относительно родительского)
- CameraSelector указан
- RenderView (4)
- Определён полноэкранный порт вывода
- Определён дочерний порт вывода (порт отрисовки будет масштабироваться относительно родительского)
- CameraSelector указан
- RenderView (5)
- Определён полноэкранный порт вывода
- Определён дочерний порт вывода (порт отрисовки будет масштабироваться относительно родительского)
- CameraSelector указан
Однако в этом случае важен порядок. Если узел ClearBuffers был бы последним вместо первого, это привело бы к чёрному экрану просто по той причине, что всё было бы очищено сразу после того, как было так тщательно отображено. По аналогичной причине его нельзя использовать в качестве корня FrameGraph, так как это привело бы к вызову очистки всего экрана для каждого нашего порта вывода.
Хотя порядок объявления FrameGraph важен, Qt 3D может обрабатывать каждый RenderView параллельно, поскольку каждый RenderView независим от других в целях генерации набора RenderCommands для передачи, в то время как состояние RenderView действительно.
Qt 3D использует подход, основанный на задачах, для параллелизма, который естественным образом масштабируется с числом доступных ядер. Это показано на следующей диаграмме для предыдущего примера.
RenderCommands для RenderView могут генерироваться параллельно на многих ядрах, и, пока мы позаботимся о правильной передаче RenderView в нужном порядке в выделенной потоке передачи OpenGL, результирующая сцена будет отображена правильно.
Отложенный рендерер
Когда дело доходит до рендеринга, отложенный рендеринг — это другой зверь с точки зрения конфигурации рендерера по сравнению с прямым рендерингом. Вместо того, чтобы отображать каждую сетку и применять эффект шейдера для затенения, отложенный рендеринг использует метод двух проходов рендеринга.
Сначала все сетки в сцене рисуются с использованием одного и того же шейдера, который будет выводить, обычно для каждого фрагмента, по крайней мере четыре значения:
- Вектор нормали к мировой поверхности
- Цвет (или другие свойства материала)
- Глубина
- Мировой вектор положения
Каждое из этих значений будет храниться в текстуре. Текстуры нормали, цвета, глубины и позиции образуют то, что называется G-буфером. Ничего не отображается на экране во время первого прохода, а вместо этого рисуется в G-буфер, готовый к последующему использованию.
После того, как все меши были отрисованы, G-буфер заполняется всеми мешами, которые в данный момент видны камере. Затем второй проход рендеринга используется для отрисовки сцены на буфер экрана с окончательной цветовой заливкой, считывая значения нормали, цвета и позиции из текстур G-буфера и выводом цвета на полноэкранный квад.
Преимущества такого подхода заключаются в том, что мощные вычисления, необходимые для сложных эффектов, используются только во время второго прохода, только для элементов, которые фактически видны камере. Первый проход не требует больших вычислительных ресурсов, так как каждый меш отрисовывается с простым шейдером. Отложенный рендеринг, таким образом, отделяет затенение и освещение от количества объектов в сцене и вместо этого связывает их с разрешением экрана (и G-буфера). Этот метод используется во многих играх благодаря возможности использования большого количества динамических источников света за счет дополнительного использования памяти GPU.
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)
}
}
}
}
}
}
}
}
} (Вышеприведенный код адаптирован из qt3d/tests/manual/deferred-renderer-qml.)
Графически, результирующая диаграмма фреймов выглядит так:
А результирующие RenderViews таковы:
- RenderView (1)
- Укажите, какая камера должна использоваться
- Определите область обзора, которая заполняет весь экран
- Выберите все сущности для компонента слоя sceneLayer
- Установите
gBufferв качестве активной цели рендеринга - Очистите цвет и глубину в текущей связанной цели рендеринга (в
gBuffer) - Выберите только сущности в сцене, у которых есть Material и Technique, соответствующие аннотациям в RenderPassFilter
- RenderView (2)
- Определите область обзора, которая заполняет весь экран
- Выберите все сущности для компонента слоя screenQuadLayer
- Очистите буферы цвета и глубины в текущем связанном фреймбуфере (экране)
- Выберите только сущности в сцене, у которых есть Material и Technique, соответствующие аннотациям в RenderPassFilter
Другие преимущества диаграммы фреймов
Поскольку дерево FrameGraph полностью основано на данных и может динамически изменяться во время выполнения, вы можете:
- Иметь разные деревья framegraph для разных платформ и оборудования и выбирать наиболее подходящее во время выполнения
- Легко добавлять и включать визуальную отладку в сцене
- Использовать разные деревья FrameGraph в зависимости от типа рендеринга, необходимого для конкретной области сцены
- Реализовать новую технику рендеринга, не изменяя внутренние компоненты Qt 3D
Заключение
Мы представили FrameGraph и типы узлов, которые его составляют. Затем мы обсудили несколько примеров, чтобы проиллюстрировать правила построения framegraph и то, как движок Qt 3D использует framegraph за кулисами. Теперь у вас должно быть довольно хорошее представление о FrameGraph и о том, как его использовать (возможно, для добавления раннего прохода заполнения z в рендерер с прямым проходом). Также всегда помните, что FrameGraph — это инструмент, который вы можете использовать, чтобы не быть привязанным к предоставляемому рендереру и материалам, которые Qt 3D предоставляет по умолчанию.
© The Qt Company Ltd
Licensed under the GNU Free Documentation License, Version 1.3.
https://doc.qt.io/qt-6.1/qt3drender-framegraph.html