Spec-Zone.ru › Qt

Qt 3D Render Framegraph

Аспект Qt 3D Render позволяет полностью реализовать алгоритм визуализации, управляемый данными. Управляющая структура данных известна как framegraph. Аналогично тому, как Qt 3D ECS (система сущностей и компонентов) позволяет определять так называемый Scenegraph, создавая сцену из дерева Сущностей и Компонентов, framegraph также представляет собой древовидную структуру, но предназначенную для другой цели. А именно, для управления способом визуализации сцены.

В ходе визуализации одного кадра 3D-рендерер, скорее всего, многократно изменит состояние. Количество и характер этих изменений зависят не только от того, какие материалы (шейдеры, геометрия мешей, текстуры и переменные униформы) находятся в сцене, но и от используемой вами схемы визуализации высокого уровня.

Например, использование традиционной простой схемы forward rendering сильно отличается от подхода deferred rendering. Другие функции, такие как отражения, тени, несколько областей вывода и проход по 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++ являются взаимооднозначными, поэтому вы можете выбрать тот, который вам больше нравится. В целях удобочитаемости и краткости для этой статьи был выбран 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, мы рассмотрим несколько примеров и разберём их.

Простой рендерер forward

Forward rendering — это когда вы используете OpenGL традиционным образом и визуализируете непосредственно в буфер заднего плана один объект за раз, применяя шейдинг к каждому из них по мере продвижения. Это противоположно deferred rendering, где мы визуализируем в промежуточный G-буфер. Вот простой FrameGraph, который можно использовать для forward rendering:

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, то получившаяся сцена будет визуализирована корректно.

Deferred Renderer

Когда дело доходит до визуализации, deferred rendering — это другой зверь с точки зрения конфигурации рендерера по сравнению с forward rendering. Вместо того чтобы рисовать каждую меш и применять эффект шейдера для её затенения, deferred rendering использует метод двух проходов визуализации.

Сначала все меши в сцене рисуются с использованием одного и того же шейдера, который, как правило, выводит, по крайней мере, четыре значения для каждого фрагмента:

  • Вектор нормали в мировом пространстве
  • Цвет (или некоторые другие свойства материала)
  • Глубина
  • Вектор позиции в мировом пространстве

Каждое из этих значений будет храниться в текстуре. Текстуры нормали, цвета, глубины и позиции образуют то, что называется 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.2/qt3drender-framegraph.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API