Spec-Zone.ru › Qt 5.9

Qt 3D Render Framegraph

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

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

Например, использование традиционной простой схемы прямого рендеринга сильно отличается от использования подхода отложенного рендеринга. Другие функции, такие как отражения, тени, несколько окон просмотра и ранние проходы z-fill, все изменяют, какие состояния рендерер должен установить в течение кадра и когда должны произойти эти изменения состояния.

В качестве сравнения, рендерер Qt Quick 2 scenegraph, ответственный за отрисовку сцен 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 {
     rect: 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 {
     rect: Qt.rect(0.0, 0.0, 1.0, 1.0)
     property alias camera: cameraSelector.camera

     CameraSelector {
          id: cameraSelector

          ClearBuffers {
               buffers: ClearBuffers.ColorDepthBuffer
          }
     }
}
CameraSelector {
      Viewport {
           rect: Qt.rect(0.0, 0.0, 1.0, 1.0)

           ClearBuffers {
                buffers: ClearBuffers.ColorDepthBuffer
           }
      }
}

FrameGraph с несколькими окнами просмотра

Перейдём к несколько более сложному примеру, который рендерит Scenegraph с точки зрения 4 виртуальных камер в 4 квадрантах окна. Это распространённая конфигурация для 3D-CAD- или моделирующих инструментов или может быть адаптирована для рендеринга зеркала заднего вида в игре гонок на автомобилях или дисплея камеры видеонаблюдения.

Viewport {
     id: mainViewport
     rect: 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
          rect: Qt.rect(0, 0, 0.5, 0.5)
          CameraSelector { id: cameraSelectorTopLeftViewport }
     }

     Viewport {
          id: topRightViewport
          rect: Qt.rect(0.5, 0, 0.5, 0.5)
          CameraSelector { id: cameraSelectorTopRightViewport }
     }

     Viewport {
          id: bottomLeftViewport
          rect: Qt.rect(0, 0.5, 0.5, 0.5)
          CameraSelector { id: cameraSelectorBottomLeftViewport }
     }

     Viewport {
          id: bottomRightViewport
          rect: 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 для RenderViews могут быть сгенерированы параллельно на нескольких ядрах, и если мы позаботимся о передаче RenderViews в правильном порядке в выделенной потоке для передачи в OpenGL, полученная сцена будет отрисована правильно.

Отложенный рендерер

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

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

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

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

После отрисовки всех мешей, буфер G-Buffer заполняется всеми мешами, которые в данный момент видны камере. Затем второй проход рендеринга используется для отрисовки сцены на буфер заднего плана с окончательной цветовой заливкой, считывая значения нормали, цвета и позиции из текстур G-буфера и выводя цвет на квад с полным экраном.

Преимущества этой техники заключаются в том, что мощные вычисления, необходимые для сложных эффектов, используются только во время второго прохода, только для элементов, которые действительно видны камере. Первый проход не требует больших вычислительных ресурсов, так как каждый меш отрисовывается с помощью простого шейдера. Таким образом, отложенный рендеринг отделяет шейдинг и освещение от количества объектов в сцене и вместо этого связывает их с разрешением экрана (и G-буфера). Эта техника используется во многих играх благодаря возможности использовать большое количество динамических источников света за счет дополнительного использования памяти графического процессора.

Viewport {
     rect: Qt.rect(0.0, 0.0, 1.0, 1.0)

     property alias gBuffer: gBufferTargetSelector.target
     property alias camera: sceneCameraSelector.camera

     LayerFilter {
          layers: "scene"

          RenderTargetSelector {
                id: gBufferTargetSelector

                ClearBuffers {
                     buffers: ClearBuffers.ColorDepthBuffer

                     RenderPassFilter {
                           id: geometryPass
                           includes: Annotation { name: "pass"; value: "geometry" }

                           CameraSelector {
                                 id: sceneCameraSelector
                           }
                     }
                }
          }
     }

     LayerFilter {
          layers: "screenQuad"

          ClearBuffers {
               buffers: ClearBuffers.ColorDepthBuffer

               RenderPassFilter {
                     id: finalPass
                     includes: Annotation { name: "pass"; value: "final" }
               }
         }
     }
}

Графически, результирующий граф кадров выглядит следующим образом:

И результирующие RenderViews:

  • RenderView (1)
    • Определите область просмотра, заполняющую весь экран
    • Выберите все сущности, у которых компонент Layer соответствует "scene"
    • Установите gBuffer в качестве активной цели рендеринга
    • Очистите цвет и глубину в текущей привязанной цели рендеринга (в gBuffer)
    • Выберите только сущности в сцене, у которых есть Material и Technique, соответствующие аннотациям в RenderPassFilter
    • Укажите, какая камера должна использоваться
  • RenderView (2)
    • Определите область просмотра, заполняющую весь экран
    • Выберите все сущности, у которых компонент Layer соответствует "screenQuad"
    • Очистите буферы цвета и глубины в текущем привязанном фреймбуфере (экран)
    • Выберите только сущности в сцене, у которых есть Material и Technique, соответствующие аннотациям в RenderPassFilter

Другие преимущества фреймграфа

Поскольку дерево FrameGraph полностью основано на данных и может быть динамически изменено во время выполнения, вы можете:

  • Иметь разные деревья фреймграфов для разных платформ и аппаратных средств и выбирать наиболее подходящее во время выполнения
  • Легко добавлять и включать визуальную отладку в сцене
  • Использовать разные деревья FrameGraph, в зависимости от того, что вам нужно отобразить для определенной области сцены
  • Реализовать новую технику рендеринга, не изменяя внутренние компоненты Qt 3D

Заключение

Мы представили FrameGraph и типы узлов, которые его составляют. Затем мы рассмотрели несколько примеров, чтобы проиллюстрировать правила построения фреймграфа и то, как движок Qt 3D использует фреймграф за кулисами. Теперь у вас должно быть довольно хорошее представление о FrameGraph и о том, как его можно использовать (например, для добавления раннего прохода z-fill к рендереру вперёд). Также всегда помните, что FrameGraph — это инструмент для вас, чтобы вы не были привязаны к предоставляемому рендереру и материалам, которые Qt 3D предоставляет по умолчанию.

© The Qt Company Ltd
Licensed under the GNU Free Documentation License, Version 1.3.
https://doc.qt.io/qt-5.9/qt3drender-framegraph.html

Spec-Zone.ru

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