Вступление

В этой статье рассматривается концепция Vertex Factory и ее реализация в Unreal Engine 4. Это будет подробное объяснение с большим количеством технического жаргона и специфических терминов UE4. Я предполагаю, что тот, кто читает это, имеет опыт работы с компьютерной графикой и знаком с некоторыми концепциями (вершинный шейдер, вершинный буфер, SRV и т. Д.).

Обратите внимание, что эта статья является частью серии сообщений, вы можете найти вступление к этой серии здесь.

Заявление об ограничении ответственности. Объяснение, которое я даю, является результатом моего понимания исходного кода движка. Это может отражать истинный замысел разработчиков UE, но также может быть неправильным. Если второе, не стесняйтесь поправлять меня.

Эта статья будет немного длинной, поэтому давайте начнем с общего плана, чтобы вы не заблудились при чтении.

  1. Во-первых, мы рассмотрим некоторые специфические термины UE4, которые нам понадобятся на протяжении всей статьи, поскольку фабрики вершин в значительной степени основаны на них.
  2. После этого мы объясним цель фабрики вершин и то, как она создает различные ресурсы, необходимые для достижения цели проектирования. Это будет включать почти все объекты и типы данных, которые используются при реализации фабрики вершин.
  3. Остальная часть статьи будет посвящена шейдерной стороне дела. Это не ограничивается только HLSL и файлами шейдера, но также включает аспекты реализации фабрики вершин, которые связывают C ++ с кодом шейдера, и то, как это влияет на его компиляцию. .

Уровень абстракции графического API: аппаратный интерфейс отрисовки (RHI)

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

Вероятно, любой коммерческий игровой движок имеет собственную реализацию слоя графической абстракции. Для Unreal Engine это называется Интерфейс оборудования рендеринга (RHI).

FRenderResource и FRHIResource

Эти два являются ключевыми компонентами кода рендеринга UE4, поскольку они представляют собой интерфейсы, которые реализуют большинство ресурсов. Это означает, что любой из ресурсов, которые вы найдете в коде отрисовки, является либо ресурсом отрисовки, либо ресурсом RHI.

FRHIResource - это базовый тип любого ресурса RHI, которым могут быть буферы вершин, буферы индекса, состояния наложения и т. д. Практически любой ресурс, с которым вы знакомы по графическому API, имеет эквивалент RHI.

Обратите внимание, что вы обычно не взаимодействуете с этими ресурсами напрямую, они предназначены для использования средством визуализации и его компонентами.

FRenderResource - это интерфейс, который определяет общий шаблон поведения ресурса визуализации. Они определены в модуле рендеринга и могут создавать и инкапсулировать FRHIResources. Именно с этими ресурсами мы можем взаимодействовать и создавать напрямую, а внутри они создают для нас необходимые FRHIRресурсы. Вот почему этот интерфейс включает различные методы для инициализации и освобождения ресурсов RHI, принадлежащих ресурсу рендеринга (InitRHI (), ReleaseRHI (), и т. Д.).

Хорошо, с RHI, давайте перейдем к основному содержанию этой статьи; Вершинные фабрики. Начнем с главного вопроса.

Для чего используются фабрики вершин?

Итак, как следует из названия, Vertex Factory в основном отвечает за передачу вершин определенного типа меша от CPU к GPU, где они будут использоваться в Vertex Shader . Если вы работаете с фоном компьютерной графики, вы, вероятно, знаете, что на уровне графического API это подразумевает создание различных ресурсов и их привязку к состоянию рендеринга:

  1. Создайте буферы вершин и свяжите их.
  2. Создайте макет ввода и свяжите его.
  3. Создание вершинного шейдера и его привязка.

Итак, давайте использовать термины UE4, которые мы объясняли ранее; Фабрика вершин - это тип FRenderResource, который отвечает за получение данных сетки из актива или другого источника и их использование для создания необходимых FRHIResource. Он инкапсулирует эти ресурсы, и когда придет время рендерить меш, рендереру понадобится его фабрика вершин (косвенно, базовые ресурсы RHI, которые он инкапсулирует).

Итак, теперь нам нужно ответить, как и где фабрика вершин создает свои ресурсы RHI.

В качестве учебного примера мы будем использовать одну из встроенных фабрик вершин. Большинство компонентов сетки используют LocalVertexFactory или его подкласс, поэтому мы и будем его использовать.

Пусть вас не пугает эта диаграмма, мы объясним все, что в нее входит.

FStaticMeshDataType:

Это класс, который содержит ресурсы, необходимые фабрике вершин для инициализации своих ресурсов RHI. Это выглядит так.

LocalVertexFactory имеет локальный класс с именем FDataType, который наследуется от FStaticMeshDataType, и просто добавляет еще один SRV указатель, который будет использоваться со скелетными сетками. Он также имеет экземпляр члена этого подкласса с именем Data, чтобы инкапсулировать все, что потребуется для создания ресурсов.

FVertexStreamComponent:

Наиболее важными членами экземпляра FDataType являются Компоненты потока. Вы, наверное, заметили, что каждый компонент потока хранит однородные данные, которые могут быть буфером вершин для одного атрибута (Position, TextureCooridinates и т. Д.). Это потому, что UE4 не чередует все атрибуты в одном буфере вершин, а использует буфер вершин для каждого атрибута. У этого выбора есть список преимуществ, которые я не могу перечислить в этой статье, но вы можете прочитать о нем здесь.

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

FVertexStreamComponent - одна из этих сущностей, и это не что иное, как оболочка для ресурса Vertex Buffer и других метаданных о потоке. Он используется для создания элемента объявления вершин и потока вершин. Вот как это выглядит:

Примечание. Буфер вершин, используемый FVertexStreamComponent, относится к типу FVertexBuffer, который является FRenderResource, который создает и инкапсулирует один FRHIResource, который… как вы уже догадались, FRHIVertexBuffer.

FVertexElement:

Эта структура просто содержит некоторые данные о потоке (но не о буфере вершин потока), которые используются для создания записи в декларации вершин. Вот данные, которые он содержит.

Примечание. EVertexElementType - это перечисление для формата типа данных элемента, аналогичное DXGI_FORMAT в DirectX и VKFormat в Вулкан.

FVertexDeclarationElementList:

Это не что иное, как массив FVertexElement, который можно использовать для создания объявления вершины.

Декларация:

Объявление вершины является эквивалентом ресурса RHI для макета ввода. Он описывает различные атрибуты, которые будут включать данные вершины. Например, положение, нормаль, касательная и т. Д.

Базовый класс FVertexFactory имеет вспомогательный метод для создания FVertexDeclaration путем предоставления FVertexDeclarationElementList. Он называется InitDeclaration () и выглядит так:

Здесь следует отметить две вещи;

  1. Фабрика вершин имеет ссылку на 3 объявления вершин, и нам нужно указать тип потока при вызове InitDeclaration (), чтобы он мог инициализировать правильное объявление. Кроме объявления вершины по умолчанию, которое используется для основных проходов рендеринга. Есть два объявления вершин для потока только позиции и потока только позиции и обычного потока. они используются в определенных проходах, таких как, например, проход глубины.
  2. Объявления кешируются в PipelineStateCache. Это имеет смысл, поскольку объявление вершины с одинаковыми элементами и типами может повторно использоваться различными типами фабрик вершин, не создавая каждый раз новый ресурс RHI.

FVertexStream:

Структура, содержащая информацию, необходимую для установки потока вершин. Это очень похоже на FVertexStreamComponent, единственными дополнительными данными, которые содержит эта структура, является тип потока, в котором будет использоваться этот поток вершин. (PositionOnly, PositionAndNormalOnly или Default ).

Подобно ссылкам в 3 FVertexDeclaration, фабрика вершин также имеет 3 массива FVertexStream для каждого типа потока.

FVertexInputStream:

Вздох ... Другой тип данных потока вершин. Он также содержит те же данные (буфер вершин и данные о потоке). Разница в том, что здесь буфером вершин является FRHIVertexBuffer, а не FVertexBuffer. Кроме того, это тип, который, вероятно, будет использоваться модулем визуализации для привязки буфера вершин потока. Средство визуализации может получить массив FVertexInputStream, вызвав метод GetStreams () на фабрике вершин. Это довольно простой метод, и все, что он делает, - это создает FVertexInputStream для каждого FVertexStream запрашиваемого типа потока. Метод слишком длинный, чтобы включать его сюда, но вот его подпись:

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

FMeshDrawCommand :: SubmitDraw (…)

FRHICommandList :: SetStreamSource (…)

FRHICommandList :: DrawIndexedPrimitive (…)

Шейдер Vertex Factory

Хорошо, теперь мы знаем, как фабрика вершин создает объявления и потоки. Последний ресурс, который нас интересует, - это вершинный шейдер.

Первое, что нужно понять, это то, что UE4 использует метод на основе шаблона для кода шейдера вершинных фабрик. Это означает, что код шейдера фабрики вершин не определяет вершинный шейдер и вообще не имеет точки входа. Он определяет список функций и структур, следующих за определенным интерфейсом. Затем они используются вершинным шейдером выполняемого прохода. Вот почему файлы шейдеров вершинных фабрик имеют расширение .ush, а не .usf. Это просто файлы заголовков шейдеров, содержащие определения.

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

Перестановки шейдеров Vertex Factory:

Если мы заранее знаем, какие материалы будут использоваться с нашим сетевым компонентом, не можем ли мы ограничить движок компилированием только тех перестановок «материал x вершина», которые нас интересуют?

Да, на самом деле фабрика вершин решает, какие перестановки должны быть скомпилированы с ее кодом шейдера заголовка. Это делается путем реализации метода ShouldCompilePermutations (), который возвращает логическое значение и принимает FVertexFactoryShaderPermutationsParameters (приятное название ). Эта сущность содержит информацию о материале и платформе, и в зависимости от этой информации фабрика вершин решает, должна ли перестановка компилироваться или нет.

Макросы Vertex Factory:

Как движок узнает, какой файл шейдера заголовка будет использоваться с каждой фабрикой вершин?

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

Этот макрос добавит к классу фабрики вершин статическую переменную-член типа FVertexFactoryType с именем StaticType и метод-член, который просто возвращает указатель на StaticType.

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

Второй макрос, который мы будем использовать, - это макрос IMPLEMENT_VERTEX_FACTORY_TYPE. Он инициализирует статический экземпляр FVertexFacrtoryType, который предыдущий макрос добавил в наш класс фабрики вершин. В этом макросе мы указываем путь к файлу заголовка шейдера, связанному с нашей фабрикой вершин.

Параметры шейдера Vertex Factory:

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

Чтобы указать, какая фабрика вершин использует этот класс в качестве типа параметра шейдера, мы используем макрос IMPLEMENT_VERTEX_FACTORY_PARAMETER_TYPE.

Универсальный буфер Local Vertex Factory

Это не используется во всех фабриках вершин, но используется в FLocalVertexFactory и его подклассах. Вот почему я думаю, что об этом стоит упомянуть.

LocalVertexFactory является универсальным и предназначен для поддержки нескольких вариантов использования. Вот почему он передает дополнительные данные в вершинный шейдер, используя глобальный однородный буфер. Хорошим примером является выборка вершин вручную (вы вручную выбираете данные вершин из буфера вершин вместо использования аппаратной выборки вершин), которая требует SRV для различных буферов вершин. Фабрика вершин передает необходимые SRV в свой унифицированный буфер вместе с другими дополнительными данными.

Структура, которая будет использоваться в унифицированном буфере, может быть объявлена ​​с помощью макросов. Вот как это выглядит для FLocalVertexFactory:

FLocalVertexFactory владеет ссылкой на унифицированный буфер RHI, созданный для этой структуры.

Примечание. Если вас интересуют глобальные параметры однородного шейдера, вы можете прочитать о них здесь.

Код шейдера Vertex Factory (VertexFactory.ush)

Хорошо, это последний кусок головоломки; Код шейдера в заголовке шейдера фабрики вершин. Как мы уже упоминали выше, он использует метод на основе шаблона, и все, что ему нужно определить, - это функции и структуры интерфейса.

Но каковы эти функции и структуры?

Основными структурами являются макеты ввода, которые описывают макет данных вершин для каждого типа потока.

struct FVertexFactoryInput {…}

struct FPositionOnlyVertexFactoryInput {…}

struct FPositionAndNormalOnlyVertexFactoryInput {…}

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

Вот некоторые из основных функций, которые необходимо определить:

VertexFactoryGetTangentToLocal (…)

VertexFactoryGetWorldPosition (…)

VertexFactoryGetRasterizedWorldPosition (…)

VertexFactoryGetPositionForVertexLighting (…)

VertexFactoryGetInterpolantsVSToPS (…)

Вы можете перегрузить эти функции для каждого типа ввода. Например, VertexFactoryGetWorldPosition (…) может определять различную логику для потока только позиции и для потока по умолчанию, поскольку он будет вызываться с разными типами параметров.

Документация UE4 включает некоторую информацию об этих структурах и функциях, вы можете найти больше о них здесь.

Стоит задать один вопрос: какой файл заголовка шейдера используют фабрики вершин, производные от FLocalVertexFactory? Используют ли они LocalVertexFactory.ush или свой собственный файл заголовка шейдера?

Что ж, оба метода действительны. Пока файл заголовка шейдера правильно реализует интерфейс и соответствует данным и параметрам, которые он предоставляет в коде C ++, он будет работать должным образом.

Однако все встроенные фабрики вершин, унаследованные от FLocalVertexFactory, используют LocalVertexFactory.ush и не имеют собственных. Они добавляют свой код в тот же файл и обертывают его с помощью директив препроцессора. Таким образом, при компиляции файла шейдера будет включен только активный код. вот хороший пример:

Как видите, директивы препроцессора используются для управления тем, что должно быть включено в макет ввода потока по умолчанию. Эти директивы препроцессора могут быть установлены с помощью метода фабрики вершин ModifyCompilationEnvironment (…)

Это все!

Поздравляю! Теперь вы знаете, как фабрики вершин реализованы в Unreal Engine 4. Эти большие знания влекут за собой большую ответственность, поэтому используйте их правильно!

В следующей статье мы реализуем фабрику вершин, которую будем использовать для нашего компонента пользовательской сетки. Это будет более короткий пост с прямыми шагами вперед. Репозиторий Github будет опубликован с этой статьей, поскольку мы будем использовать его в качестве справочника для кода.

Кредиты

Автор Аюб Хамасси. Большое спасибо Шахриар Шахраби за рецензирование этой длинной статьи.

Ресурсы

  1. Документы Unreal Engine 4: Программирование графики
  2. Сохранение данных вершин: чередовать или не чередовать
  3. Директивы препроцессора (HLSL)
  4. Shader Permutations in UE4
  5. Изучение Unreal Engine 4: Добавление глобального унифицированного параметра шейдера (1)