Инициализация Direct3D 11.1
С каждой версией DirectX создание первой программы, использующей трёхмерную графику, становится всё сложней и сложней. В этом уроке мы пройдём весь этап инициализации Direct3D. Сначала нам нужно разобраться, что из себя представляет Direct3D 11.
Direct3D
Direct3D используется для рисования трёхмерной графики. Вместо рисования чаще используется слово rendering. По-русски - визуализация или просто рендеринг. Под рендерингом мы будем понимать построение и вывод на экран трёхмерной сцены.
Direct3D 11.1 (последняя версия) состоит из следующих частей: Direct3D Graphics, DXGI, HLSL, DDS. DDS (это такой графический формат) уже не используется, HLSL (шейдеры) нам пока не нужен, а вот с первыми двумя частями мы разберёмся.
Direct3D Graphics
Собственно, это и есть основная часть Direct3D. Именно сюда относятся все основные интерфейсы Direct3D, с помощью которых выводится трёхмерная графика. Сюда относятся устройства и ресурсы. Здесь можно настраивать работу графического конвейера. Данные интерфейсы начинаются с префикса ID3D11, например: ID3D11Device, ID3D11DeviceContext.
Если уж вгрызаться в самую суть, то данная часть является лишь надстройкой над следующей частью - DXGI.
DXGI
DXGI - DirectX Graphics Infrastructure - инфраструктура графики DirectX. DXGI - это низкоуровневые команды. На самом деле, всё что относится к Direct3D общается с видеокартой через DXGI. Просто некоторые части DXGI открыты программисту напрямую, а некоторые скрыты за более высокоуровневыми интферфейсами. Данные интерфейсы начинаются с префикса IDXGI. К DXGI относится очень важный интерфейс IDXGISwapChain. Именно этот интерфейс позволяет показать визуализированное изображение в окне.
Основные интерфейсы Direct3D 11.1
Прежде чем приступать к инициализации Direct3D, нам необходимо рассмотреть основные понятия и связанные с ними интерфейсы.
Устройство (Device) - виртуальное представление видеокарты в нашей программе. Устройства представлены интерфейсом ID3D11Device. С помощью устройства создаются различные ресурсы.
Контекст устройства (Device Context) - интерфейс ID3D11DeviceContext. Контекст устройства отвечает за рендеринг. Именно данный интерфейс содержит основные команды для создания трёхмерной сцены.
Цепочка обмена (Swap Chain) - интерфейс IDXGISwapChain. Содержит буферы, в которые происходит рендеринг трёхмерной сцены. Цепочку обмена нужно рассмотреть подробнее.
IDXGISwapChain
При создании цепочки обмена она привязывается к конкретному окну (HWND) и устройству рендеринга (ID3D11Device). Каждая цепочка обмена состоит из одного и более буферов. Буфер в данном случае - это просто прямоугольная картинка. Размеры буферов должны соответствовать размерам клиентской области окна. У каждой цепочки обмена есть один передний буфер (front buffer) и задние буферы (back buffer) - от нуля и больше.
Несколько раз в секунду происходит рендеринг трёхмерной сцены. Полученное двухмерное изображение копируется в один из задних буферов цепочки обмена. Как только задний буфер заполнен, он меняется местами с передним буфером - происходит "представление" (present), т.е. содержимое заднего буфера становится видно в окне программы. Т.е. основная цель цепочки обмена - вывести рендеринг трёхмерной сцены на экран.
У цепочки обмена есть ещё интересные применения, которые мы рассмотрим позже. Сейчас же пора переходить к инициализации нашей первой программы на Direct3D.
Инициализация Direct3D 11
1. Сначала нужно создать устройство рендеринга и, связанные с ним контекст устройства и цепочку обмена.
2. Создание render-target view. View - вид, изображение, target - цель. Т.е. дословно render-target view - изображение цели рендеринга, а по сути - картинка, куда визуализируется трёхмерная сцена.
После этого можно начинать вывод графики.
Создание ID3D11Device
В DirectX приложениях все действия производятся интерфейсами. За одним исключением. Первый интерфейс создаётся через функцию. Главный интерфейс в Direct3D 11 - ID3D11Device. Для получения данного интерфейса можно воспользоваться одной из двух функций: D3D11CreateDevice, D3D11CreateDeviceAndSwapChain. Первая функция создаёт только устройство, вторая - устройство и цепочку обмена. Мы воспользуемся второй функцией - так проще. Давайте взглянем на прототип:
Функция D3D11CreateDeviceAndSwapChain
!1? HRESULT D3D11CreateDeviceAndSwapChain( _In_ IDXGIAdapter *pAdapter, _In_ D3D_DRIVER_TYPE DriverType, _In_ HMODULE Software, _In_ UINT Flags, _In_ const D3D_FEATURE_LEVEL *pFeatureLevels, _In_ UINT FeatureLevels, _In_ UINT SDKVersion, _In_ const DXGI_SWAP_CHAIN_DESC *pSwapChainDesc, _Out_ IDXGISwapChain **ppSwapChain, _Out_ ID3D11Device **ppDevice, _Out_ D3D_FEATURE_LEVEL *pFeatureLevel, _Out_ ID3D11DeviceContext **ppImmediateContext );?1!Функция принимает 12 аргументов. После выполнения функции будет создано три интерфейса и одна переменная. Давайте рассмотрим аргументы подробнее:
1. IDXGIAdapter *pAdapter - указатель на видеокарту. При передаче значения NULL будет использоваться видеокарта по-умолчанию. Понятно, что данный аргумент имеет смысл, если у вас есть не одна видеокарта подключённая к мониторам. Во всех программах мы будем указывать NULL.
2. D3D_DRIVER_TYPE DriverType - тип драйвера. Одно из значений перечисления D3D_DRIVER_TYPE. Мы будем использовать значение D3D_DRIVER_TYPE_HARDWARE, т.е. все операции выполняются видеокартой. Данное свойство имеет смысл менять, если ваша видеокарта не поддерживает DirectX 11.
3. HMODULE Software - описатель (handle) библиотеки dll, используемой как программный растеризатор. Данное свойство нужно указывать, когда предыдущий аргумент равен D3D_DRIVER_TYPE_SOFTWARE. Мы же будем передавать NULL.
4. UINT Flags - сочетание флагов из перечисления D3D11_CREATE_DEVICE_FLAG. Самый важный флаг, который можно указать - D3D11_CREATE_DEVICE_DEBUG. Данное значение включает отладочный слой - дополнительные проверки связывания ресурсов и шейдеров, а также описания ошибок. Direct3D 11 во время выполнения делится на два уровня (или слоя - layer): уровень ядра (core layer) и уровень отладки (debug layer). Уровень ядра - это и есть Direct3D API. Включая уровень отладки флагом D3D11_CREATE_DEVICE_DEBUG, можно получить дополнительную информацию. Мы разберём слой отладки в отдельном уроке, а пока будем использовать только слой ядра, данный аргумент - 0.
5. const D3D_FEATURE_LEVEL *pFeatureLevels - уровень возможностей. Здесь имеется ввиду какие возможности поддерживает видеокарта. На данный момент высший уровень возможностей: D3D_FEATURE_LEVEL_11_1. Именно его и будем использовать. Данный аргумент принимает указатель на массив. В массиве перечисляются разные уровни возможностей - от высшего к низшему. Если установленная видеокарта не поддерживает высший уровень возможностей, DirectX пытается создать устройство со следующим уровнем возможностей в массиве. Если в данный аргумент передать NULL, то будет использован следующий массив:
!1?{ D3D_FEATURE_LEVEL_11_0, D3D_FEATURE_LEVEL_10_1, D3D_FEATURE_LEVEL_10_0, D3D_FEATURE_LEVEL_9_3, D3D_FEATURE_LEVEL_9_2, D3D_FEATURE_LEVEL_9_1, };?1!Заметьте, что в данном массиве нет уровня D3D_FEATURE_LEVEL_11_1.
6. UINT FeatureLevels - количество элементов в массиве предыдущего аргумента. Если передавать один элемент (именно так я делаю в прикреплённом примере), то в предыдущий аргумент можно передавать указатель на переменную D3D_FEATURE_LEVEL, а не на массив.
7. UINT SDKVersion - версия набора для разработки. Для Direct3D 11 нужно передавать D3D11_SDK_VERSION.
8. const DXGI_SWAP_CHAIN_DESC *pSwapChainDesc - указатель на структуру описания цепочки обмена. Эту структуру мы рассмотрим ниже.
9. IDXGISwapChain **ppSwapChain - указатель на интерфейс цепочки обмена. После вызова функции можно использовать экземпляр этого интерфейса.
10. ID3D11Device **ppDevice - указатель на интерфейс D3D11 устройства. Данная функция как раз и предназначена для создания экземпляра ID3D11Device.
11. D3D_FEATURE_LEVEL *pFeatureLevel - в данной переменной будет сохранён уровень возможностей видеокарты. Во всех примерах я буду подразумевать, что данный уровень - D3D_FEATURE_LEVEL_11_1. Заметьте, что значение этой переменной зависит от пятого аргумента.
12. ID3D11DeviceContext **ppImmediateContext - указатель на контекст устройства d3d11.
В результате выполнения функции D3D11CreateDeviceAndSwapChain мы получим переменную, хранящую уровень возможностей, и экземпляры трёх интерфейсов: IDXGISwapChain, ID3D11Device, ID3D11DeviceContext. Заметьте, что 8 аргумент - указатель на структуру DXGI_SWAP_CHAIN_DESC. Эту структуру необходимо заполнить перед вызовом функции создания устройства D3D11 и цепочки обмена. Давайте взглянем на описание этой структуры:
Структура DXGI_SWAP_CHAIN_DESC
!1? typedef struct DXGI_SWAP_CHAIN_DESC { DXGI_MODE_DESC BufferDesc; DXGI_SAMPLE_DESC SampleDesc; DXGI_USAGE BufferUsage; UINT BufferCount; HWND OutputWindow; BOOL Windowed; DXGI_SWAP_EFFECT SwapEffect; UINT Flags; } DXGI_SWAP_CHAIN_DESC;?1!Данная структура определяет, какими свойствами должна обладать цепочка обмена. Рассмотрим поля:
1. DXGI_MODE_DESC BufferDesc - структура, описывающая буферы: BufferDesc.Width - ширина, BufferDesc.Height - высота, BufferDesc.Format - формат пикселей буферов, BufferDesc.ScanlineOrdering - метод растеризации - пока не важен, BufferDesc.Scaling - метод масштабирования - пока не важен, BufferDesc.RefreshRate - частота обновления в герцах, задаётся двумя полями: BufferDesc.RefreshRate.Numerator (числитель) и BufferDesc.RefreshRate.Denominator (знаменатель).
2. DXGI_SAMPLE_DESC SampleDesc - параметры мультисэмплинга. Данная структура состоит из двух полей: Count (количество) и Quality (качество). По умолчанию для качества задаётся 0, а для количества 1.
3. DXGI_USAGE BufferUsage - данное поле задаёт как используется буфер, а также доступ центрального процессора к этому буферу. Мы пока будем использовать буфер для вывода трёхмерной сцены в окно, поэтому будем задавать значение DXGI_USAGE_RENDER_TARGET_OUTPUT.
4. UINT BufferCount - количество буферов.
5. HWND OutputWindow - окно, в которое будет происходить вывод.
6. BOOL Windowed - поле задаёт полноэкранный/оконный режим.
7. DXGI_SWAP_EFFECT SwapEffect - поле задаёт, что будет происходить с содержимым буфера после выполнения команды Present. Значение по умолчанию - DXGI_SWAP_EFFECT_DISCARD. Мы подробно разберём это поле в следующих уроках.
8. UINT Flags - набор флагов, задающий поведение цепочки обмена. Мы пока будем использовать 0.
Создание устройства D3D11 и цепочки обмена
Теперь давайте посмотрим на код:
!1?ID3D11Device* dev; // устройство d3d11 ID3D11DeviceContext* devContext; // контекст устройства IDXGISwapChain* sc; // цепочка обмена DXGI_SWAP_CHAIN_DESC sсd; ZeroMemory(&sсd, sizeof(sсd)); sсd.BufferCount = 2; sсd.BufferDesc.Width = 500; sсd.BufferDesc.Height = 500; sсd.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; sсd.BufferDesc.RefreshRate.Numerator = 60; sсd.BufferDesc.RefreshRate.Denominator = 1; sсd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT; sсd.OutputWindow = hWnd; sсd.SampleDesc.Count = 1; sсd.SampleDesc.Quality = 0; sсd.Windowed = TRUE; D3D_FEATURE_LEVEL FeatureLevels = D3D_FEATURE_LEVEL_11_1; UINT numLevels = 1; D3D_FEATURE_LEVEL FeatureLevel; D3D11CreateDeviceAndSwapChain(NULL, D3D_DRIVER_TYPE_HARDWARE, NULL, 0, &FeatureLevels, numLevels, D3D11_SDK_VERSION, &sсd, &sc, &dev, &FeatureLevel, &devContext); ?1!Здесь происходит заполнение структуры DXGI_SWAP_CHAIN_DESC и вызов функции D3D11CreateDeviceAndSwapChain.
Создание render-target view
Давайте разберёмся, какой интерфейс что делает и для чего нужен render-target-view. Устройство D3D11 создаёт различные ресурсы. Цепочка обмена представляет (present) содержимое фонового буфера на экран. Контекст устройства D3D11 выполняет весь рендеринг. Смотрите что получается: контекст устройства "рисует" графику в буфер, а цепочка обмена показывает эту графику в окне. Но есть одна проблема. Контекст устройства не умеет работать с буферами цепочки обмена напрямую. Для этого как раз и служит render-target view. Контекст устройства для вывода графики использует интерфейс ID3D11RenderTargetView. Так вот, нам нужно привязать экземпляр данного интерфейса к заднему буферу цепочки обмена. Сначала мы получаем адрес заднего буфера (метод интерфейса цепочки обмена):
!1?HRESULT IDXGISwapChain::GetBuffer( [in] UINT Buffer, [in] REFIID riid, [in, out] void **ppSurface );?1!UINT Buffer - индекс буфера. Если мы задаём для цепочки обмена DXGI_SWAP_EFFECT_DISCARD, то данный аргумент всегда принимает 0.
REFIID riid - здесь мы указываем тип интерфейса, какой мы хотим получить. Нам нужен интерфейс ID3D11Texture2D (двухмерная картинка). Тип этого аргумента относится к COM. У нас будет отдельный урок по COM. Пока же мы просто передадим аргумент: __uuidof(ID3D11Texture2D).
void **ppSurface - указатель на интерфейс (в нашем случае - ID3D11Texture2D).
После выполнения метода GetBuffer у нас есть экземпляр интерфейса ID3D11Texture2D, который содержит задний буфер цепочки обмена. Теперь, используя этот интерфейс мы можем создать render-target view:
!1?HRESULT ID3D11Device::CreateRenderTargetView( [in] ID3D11Resource *pResource, [in] const D3D11_RENDER_TARGET_VIEW_DESC *pDesc, [out] ID3D11RenderTargetView **ppRTView );?1!ID3D11Resource *pResource - сюда мы передаём задний буфер - экземпляр интерфейса, который мы получили в предыдущем методе.
const D3D11_RENDER_TARGET_VIEW_DESC *pDesc - описание render-target view. На данный момент просто передаём NULL.
ID3D11RenderTargetView **ppRTView - адрес экземпляра интерфейса ID3D11RenderTargetView. Именно этот экземпляр будет использоваться для вывода графики контекстом устройства D3D11.
Давайте посмотрим на код создания render-target view:
!1?ID3D11RenderTargetView* view; ID3D11Texture2D* backBuffer; sc->GetBuffer(0, __uuidof(ID3D11Texture2D), (LPVOID*)&backBuffer); dev->CreateRenderTargetView(backBuffer, NULL, &view);?1!Вот и всё. Теперь мы можем рисовать графику.
Представление заднего буфера
Для представления заднего буфера (т.е. чтобы содержимое заднего буфера появилось в окне) достаточно вызвать метод Present:
!1?sc->Present(0, 0);?1!Первый аргумент - интервал синхронизации. Пока передаём ноль.
Второй аргумент - флаги. Тоже 0.
Если вы сейчас запустите программу. То увидите просто белое окно. А ведь действительно, мы не выполняли никаких операций рисования. Давайте хотя бы поменяем цвет фона. Для этого воспользуемся следующим методом:
!1?const float greyColor[4] = { 0.8f, 0.8f, 0.8f, 1.0f }; devContext->ClearRenderTargetView( view, greyColor);?1!Для контекста устройства мы вызываем метод ClearRenderTargetView (Очистить render-target view). Первый аргумент - render-target view, который нужно очистить, второй - цвет, которым нужно заполнить сцену. Цвет задаётся массивом из 4-ёх элементов типа float (красный, зелёный, синий, альфа). Добавьте этот код перед вызовом Present и увидите следующее окно:

Заключение
Мы научились инициализировать Direct3D приложение. В этом коде ещё много неясных моментов, которые мы выясним в дальнейших уроках. Сейчас гораздо важнее понять логику инициализации. Надеюсь, у меня получилось достаточно понятно описать этот процесс.
Комментарии