Как стать автором
Обновить

Рисуем воду на Direct3D. Часть 1. Архитектура графического конвейера и API

Время на прочтение 7 мин
Количество просмотров 12K
В этой статье, разделенной на несколько частей, я в общих чертах объясню архитектуру современных версий Direct3D(10-11), а также покажу, как с помощью этого API нарисовать вот такую вот сцену кораллового рифа, основным достоинством которой является простая в реализации, но красивая и относительно убедительно выглядящая вода:
image


Начать следует с описания архитектуры Direct3D.

API Direct3D является прямым отражением архитектуры современных видеокарт, и инкапсулирует в себе графический конвейер следующего вида:

image

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

Теперь объясню каждую из стадий по отдельности.

Input Assembler (сокращенно — IA) — получает данные о вершинах из буферов, находящихся системной памяти и собирает из них примитивы, которые используются последующими стадиями конвейера. Также, IA прикрепляет к вершинам некоторые метаданные, генерируемые рантаймом D3D, такие как номер вершины.

Vertex Shader (вершинный шейдер, VS) — обязательная, и полностью программируемая стадия конвейера. Как несложно догадаться, запускается для каждой вершины(англ. vertex), и получает на вход данные о ней от Input Assembler'а. Используется для различного преобразования вершин, для трансформации их координат из одной координатной системы в другую, для генерации нормалей, текстурных координат, обсчета освещения и другого. Данные от вершинного шейдера поступают либо непосредственно растеризатору, либо в Stream Output(если данная стадия конвейера установлена, но геометрический шейдер — нет), либо геометрическому шейдеру, либо поверхностному шейдеру.

Hull Shader(поверхностный шейдер, HS), Domain Shader(доменный шейдер, DS) и Tesselator(тесселятор) — стадии, добавленные в Shader Model 5.0 (и D3D11, соответственно), и используемые в процессе тесселяции(разбиения примитивов на более мелкие, для повышения детализации изображения). Эти стадии конвейера опциональны, а так как в моей сцене они не используются, я не буду на них подробно останавливаться. Желающие могут почитать о них, например, на MSDN.

Geometry Shader(геометрический шейдер, GS) — обрабатывает примитивы(точки, линии или треугольники), собранные из вершин, обработанных предыдущими стадиями конвейера. Геометрические шейдеры могут генерировать новые примитивы на лету(то есть их выхлоп не обязательно должен быть 1-к-1, как в случае, например, вершиных шейдеров). Используются для генерации геометрии теней(shadow volume), спрайтов(системы частиц и пр.), отражений(напр. однопроходная отрисовка в cube map) и тому подобного. Хотя могут использоваться и для тесселяции, это не рекомендуется. Данные от геометрического шейдера поступают либо в Stream Output, либо в растеризатор.

Stream Output(SO) — необязательная стадия конвейера, используется для выгрузки обработанных конвейером вершин обратно в системную память(чтобы выхлоп SO мог быть считан CPU или использоваться конвейером при следующем запуске). Данные получает либо от геометрического шейдера, либо, в случае его отсутствия — от вершинного или доменного.

Rasterizer (растеризатор, RS) — «сердце» графического конвейера. Назначение этой стадии, как понятно из названия — растеризация примитивов, то есть разбиение их на пиксели(хотя название «пиксель» не совсем корректно — под пикселем обычно понимается то, что находится непосредственно во фреймбуфере, т.е. то, что отображается на экране, так что правильнее будет «фрагменты»). Растеризатор получет на вход векторную информацию о вершинах, от предыдущих стадий конвейера, и преобразовывает ее в растровую, отсекая примитивы вне области видимости, интерполируя значения, связанные с вершинами(такие, как текстурные координаты) и проецируя их позиции на двумерную область просмотра(англ. viewport). Данные от растеризатора поступают к пиксельному шейдеру, если тот установлен.

Pixel Shader(пиксельный шейдер, PS) — работает с фрагментами изображения, полученными от растеризатора. Используется для реализации огромного многообразия графических эффектов, и на выход, в стадию Output Merger'а, отдает цвет фрагмента, и, опционально, значение глубины(значение, используемое для определения, какие фрагменты лежат ближе к камере).

Output Merger(OM) — последняя стадия графического конвейера. Для каждого фрагмента, полученного от пиксельного шейдера, проводит тест глубины и стенсил-тест, определяя, должен ли фрагмент попасть во фреймбуфер и производит смешивание цветов, если оно включено.

Теперь собственно об API.

API Direct3D основано на облегченном COM(Microsoft Component Object Model). Облегченном настолько, что от «полновесного» COM в нем остался только концепт интерфейсов.

Для тех, кто незнаком с понятием COM-интерфейса — небольшое лирическое отступление.

Концепт COM-интерфейса близок по своей сути к концепту интерфейсов из .NET(потому как .NET это, фактически, развитие COM). По своей сути, это абстрактный класс, у которого есть только методы, и которому поставлен в соответствие некоторый 16-байтовый идентификатор(GUID). Физически, интерфейс это коллекция функций, то есть, указатель на массив с указателями на функции(с Си-совместимым ABI, и обычно с stdcall-конвенцией вызова), у которых первым аргументом идет указатель на сам интерфейс. За каждым интерфейсом стоит некоторый объект, который его реализует, и каждый объект может реализовать несколько различных типов интерфейсов. Microsoft постулирует, что единственный способ связаться с объектом, который реализует некоторый интерфейс — это через указатель на этот интерфейс, а именно — вызывая его методы.

Интерфейсы могут друг от друга наследоваться, и большинство COM-интерфейсов, в том числе в Direct3D, наследуются от особенного интерфейса — IUnknown, который реализует управление временем жизни объекта, реализующего интерфейс, через подсчет ссылок, и позволяет получать от объекта указатели на интерфейсы различных типов по их GUID.

Надо сказать, что хотя программа для этой статьи написана на C++, но так как COM-интерфейсы имеют Си-совместимое ABI, то работать с ними можно из любого языка, который способен работать с нативным кодом, прямо ли, или через FFI. В случае с MSVC++ и .NET, это делать особенно удобно, так как MS органично интегрировала COM в объектные системы C++ и .NET соответственно.

Интерфейсы Direct3D 10 и 11 можно условно разделить на несколько типов:

Интерфейсы DXGI. DXGI это низкоуровневое API, на котором базируются все новые компоненты графической подсистемы Windows. В случае с D3D нас особенно интересует интерфейс IDXGISwapChain — он инкапсулирует в себе цепочку буферов, в которые графический конвейер рисует, и отвечает за их привязку к определенному winapi-окошку(HWND). Хотя использовать этот интерфейс совершенно не обязательно(даже для отрисовки «в окно» — мы можем рисовать в текстуру и потом передавать ее HDC в GDI), он используется часто, так как очень удобен.

Интерфейсы виртуального адаптера. Используются для создания различных ресурсов, для конфигурации графического конвейера и для его запуска. В D3D10 за все это был ответственнен один интерфейс, ID3D10Device(или ID3D10Device1, для D3D10.1. Вообще, в именовании интерфейсов D3D и DXGI — префикс «I» в названии означает что тип представляет собой тип интерфейса, префикс вроде «DXGI» или «D3D11» означает конкретное API, а суффикс, если такой имеется, обозначает минорную версию API), в D3D11 его разделили на два — ID3D11Device(создание ресурсов), и ID3D11DeviceContext(оставшиеся две задачи).
Объекты, реализующие эти интерфейсы также реализуют и некоторые интерфейсы DXGI — например мы можем запросить у ID3D11Device интерфейс IDXGIDevice(вызвав метод QueryInterface(который включен в IUnknown, а ID3D11Device наследуется от IUnknown) у первого)

Тут следует упомянуть о том, что используя самое новое API мы совершенно не обязательно должны требовать от железа, на котором наша программа будет работать, полного соответствия этому API. В D3D10.1 Microsoft ввели концепт «feature level», который позволяет программам, использующим новые версии API запускаться даже на D3D9-железе(если они не будут требовать от API фич, в определенный feature level, не входящих, естественно). В случае моей сцены, я буду использовать D3D11, но виртуальный адаптер буду создавать с флагом D3D_FEATURE_LEVEL_10_0, и использовать соответственно шейдеры 4й модели.

Вспомогательные интерфейсы. Такие как ID3D11Debug, ID3D11InfoQueue, ID3D11Counter или ID3D11ShaderReflection — используются для получения дополнительной информации о состоянии конвейера, о шейдерах, для измерения производительности и другого подобного.

Интерфейсы ресурсов. В D3D под ресурсом понимается какая-либо текстура(например ID3D11Texture2D — двумерная текстура), либо просто буфер(содержащий, например, данные о вершинах). Объекты ресурсов реализуют также и различные интерфейсы DXGI, вроде IDXGIResource, и это — ключ к интероперабельности между различными графическими подсистемами(такими Direct2D, GDI) и разными версиями одной подсистемы(D3D9, 10 и 11), в новых версиях Windows.

Интерфейсы представления(англ. view). Каждый ресурс мы можем использовать для нескольких различных целей, и возможно даже одновременно. В новых версиях D3D мы не поставляем интерфейсы текстур и буферов конвейеру напрямую(кроме буферов вершин, индексов и констант), вместо этого мы создаем объект, реализующий один из интерфейсов представления, и поставляем конвейеру его.
В D3D11 присутствуют следующие типы view-интерфейсов:
  • ID3D11RenderTargetView — представление цели рендеринга(англ. render target). Как понятно из названия(вообще, надо сказать, имена типов в D3D и DXGI очень говорящие, хотя и длинные), в ресурс, связанный с данным представлением, графический конвейер рисует
  • ID3D11ShaderResourceView — представление для ресурсов, используемых шейдерами(пример — текстура, из которой пиксельный шейдер выбирает тексель для формирования цвета фрагмента).
  • ID3D11DepthStencilView — представление для ресурсов, используемых как буфер глубины и стенсила.
  • ID3D11UnorderedAccessView — представление для ресурсов, используемых вычислительными шейдерами(Compute Shader, CS — так как они к процессу рендеринга практически не относятся, я не буду их описывать).

Интерфейсы шейдеров. Например, ID3D11VertexShader. Инкапсулируют код шейдера для определенной программируемой стадии графического конвейера, и используются для установки шейдера для этой стадии.

Интерфейсы состояния(англ. state). Используются для конфигурирования различных непрограммируемых стадий конвейера. Примеры — ID3D11RasterizerState(конфигурирует растеризатор), ID3D11InputLayout(хранит информацию о вершинах, поставляемых из буферов вершин в Input Assembler), ID3D11BlendState(конфигурирует процесс смешивания цветов в Output Merger).

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

Полный код доступен на github, по следующей ссылке: github.com/Lovesan/Reef

Теги:
Хабы:
+34
Комментарии 21
Комментарии Комментарии 21

Публикации

Истории

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн
Геймтон «DatsEdenSpace» от DatsTeam
Дата 5 – 6 апреля
Время 17:00 – 20:00
Место
Онлайн