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

В этой статье, разделенной на несколько частей, я в общих чертах объясню архитектуру современных версий 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

Комментарии 21

    +4
    Очень не показательная картинка, на мой взгляд, лучше было бы показать анимацию.
      –1
      окей, возможно на досуге сделаю видео
      но лучше скомпилировать код и самому посмотреть :)
      0
      Как для меня, статья не о чем. Уже слюнки потекли в ожидании чего-то практического и тут такой облом…
        –1
        ну эта статья — туториал по d3d, вобщем-то, а не гайд по написанию мегареалистичной воды.

        для последнего можно смотреть сэмплы FFT Ocean и Island из NVIDIA D3D 11 SDK:
        developer.nvidia.com/nvidia-graphics-sdk-11-direct3d
          –2
          Хорошо, тогда измени заголовок, ибо его начало говорит совсем о другом.
        +1
        На картинки вода очень нереалистичная… используя 2 треугольника и искажение текстур в шейдере можно добиться намного большего с минимум усилий.
          +12
          Как фанат OpenGL — оставлю это здесь — madebyevan.com/webgl-water/
              0
              Кстати просветите — webgl — всегда полагается исключительно на процессор и не использует видеокарту в текущих реализациях или у меня просто что то не то с конфигурацией? Ибо пример тормозит — полностью загружая одно ядро процессора…
                0
                а у меня как всегда просто ругнулся что нет поддержки webGL (ubuntu+intel столетней давности)
                  0
                  видимо, у вас используется програмная реализация webGl типа mesa
                    0
                    А как можно задействовать аппаратную? ОС, браузер, железо?
                      0
                      Возможно, конфиги браузера, драйвера на железо.
                      It will also only work if your graphics card supports OpenGL 2.0, so it might be a pain to get working
                  –1
                  Your browser does not support WebGL.
                  Отлично.
                    0
                    как фанат OpenGL вы должны понимать что от GL там только рисование пикселов.
                    а весь рэйтрэйсинг, каустика, тени, и прочее сделано на жаваскрипте.
                    0
                    прекрасно отображается, ff6/w7x64; crome/w7x64
                      +2
                      Абстрактное описание архитектуры D3D — не есть туториал, а «водой» такую картинку перестали называть ещё лет 10 назад до появления программируемого конвейера.
                        0
                        >Абстрактное описание архитектуры D3D — не есть туториал,
                        это только первая часть :)

                        > а «водой» такую картинку перестали называть ещё лет 10 назад до появления программируемого конвейера.
                        Окей, окей, раз я вижу это уже третий комментарий на эту тему, тогда я специально для крутых программистов графики, которые уже раз 20 писали воду «как в крузисе», и которым с моей поделки становится смешно, напишу:

                        1) Симуляция воды это очень сложный таск, и для небольшого туториала вроде моего — слишком объемный. Нет, ну правда, это не тема для туториала.
                        2) Вода, там где она красиво смотрится, обязана этим не самой даже себе, а объектам в сцене, которые красиво отражаются, красиво смотрятся под водой и так далее. У меня здесь из объектов(кроме воды) только скайбокс, а это негусто, мягко говоря; хотя, для туториала такого рода — самое то.
                        3) Тут слишком простая «wave model»(а большая часть реализма компьютерной воды идет из нее). Банальная сумма волн(причем только двух. Кстати, если поиграться с параметрами, добавить больше разных волн, будет смотреться лучше) в вершинном шейдере. Реалистичная модель волн требует либо сложных симуляций жидкостей(fluid dynamics) вокселями, либо сложных алгоритмов основанных на FFT(см. NVIDIA SDK 11, FFT Ocean, про который я уже упоминал), либо хотя бы кучи препросчитанных карт высот. И первое и второе это слишком круто для статьи такого уровня, а третьего у меня нет, и делать лень.
                        4) Пена, каустика — опять же, для туториала это слишком много.
                          0
                          Зачем тогда циклу уроков по основам D3D такое название, чтобы с доброй сотней других не путалось?
                            0
                            Я думаю, для цикла статей про реалистичную воду подходящее название будет соответственно «Реалистичная вода на Direct3D». И в этом цикле как бы не будет рассказываться про самые основы D3D, как вот в этой части. Я думаю, это очевидно.
                        +1
                        ИМХО не следовало в статью добавлять описание конвейера и архитектуры d3d, кто заинтересовался статьей — уже имеет познания в этой области, а кто нет — лучше почитать MSDN. Лучше начать с математических выкладок рендеринга самой воды.

                        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                        Самое читаемое