Direct3D 11 на C++ . Урок 1

Урок 1. Создание устройства DirectX

Давайте разберемся, что же нам пондобится. Кроме огромного желания сделать что-то крутое и обязательно трехмерное необходимо знание C++. Мы будем создавать окно при помощи WinAPI. Я использовал Visual C++ из Microsoft Visual Studio 2008, но в принципе версия C++ не играет роли. Вы можете взять любую другую среду. Кроме того, придется скачать с сайта Микрософт DirectX SDK. Ссылка найдется где-то здесь. После распаковки архива не забудьте через меню СервисaПараметры добавить каталоги с lib‘ами и include‘ами DirectX.

Итак, C++ выучен, SDK установлен, а идея будущей гениальной игры родилась. Что дальше? А дальше давайте разберемся, что такое вообще Direct3D. Как сообщает официальная справка, Direct3D – COM-библиотека, которая служит для облегчения и ускорения рисования в Windows. Direct3D напрямую работает с видеокартой, а это гораздо эффективнее, чем рисовать в стандартном окне при помощи стандартных API-функций. Кроме того, Direct3D сам проводит страшные трехмерные вычисления (ну или, во всяком случае, я так наивно думал, вспоминая DirectX8).

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

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


// Главный цикл сообщений

MSG msg = {0};

while( WM_QUIT != msg.message ) {

if( PeekMessage( &msg, NULL, 0, 0, PM_REMOVE ) )

{

TranslateMessage( &msg );

DispatchMessage( &msg );

}

else        // Если сообщений нет

Render();      // Рисуем

}

CleanupDevice();       // Удалить объекты Direct3D

Тут все понятно. Теперь давайте разберемся с создаваемыми объектами.


HINSTANCE               g_hInst = NULL;

HWND                    g_hWnd = NULL;

D3D_DRIVER_TYPE         g_driverType = D3D_DRIVER_TYPE_NULL;

D3D_FEATURE_LEVEL       g_featureLevel = D3D_FEATURE_LEVEL_11_0;

ID3D11Device*           g_pd3dDevice = NULL;

ID3D11DeviceContext*    g_pImmediateContext = NULL;

IDXGISwapChain*         g_pSwapChain = NULL;

ID3D11RenderTargetView* g_pRenderTargetView = NULL;

Пока можно расслабиться. Я подскажу, когда надо будет начинать думать. Переменные g_hInst и g_hWnd – идентификаторы нашего приложения и окна, их не будем вообще трогать. g_driverType понадобится для создания устройств Direct3D. Этот параметр указывает, производить вычисления в видеокарте или в центральном процессоре. Можно смело ставить на видеокарту, но Микрософт, как всегда, немного поиздевается над нами. g_featureLevel – параметр, указывающий, какую версию DirectX поддерживает наша видеокарта. Он, как и предыдущий, понадобится только для создания устройств Direct3D. Внимание, начинаем думать! Следующие три объекта работают в связке, они даже создаются одной функцией. Честно говоря, все три раньше были одним объектом D3DDevice, но программисты из Микрософт решили, что с новым поколением DirectX объекты должны размножаться. Боюсь представить, сколько их будет в 12 версии.

Давайте всмотримся внимательнее в объект-указатель на интерфейс ID3D11Device. Раньше девайс (устройство) использовался как для создания всяческих ресурсов (текстур, буферов трехмерных объектов, шейдеров и т. д.), так и для собственно рисования. Теперь его просто разрезали: созданием ресурсов занялся интерфейс ID3D11Device, а выводом графической информации – интерфейс ID3D11DeviceContext (контекст устройства, который Микрософт обозвали СобственноКонтекстом, а мы будем называть устройством рисования). Создавать IDXGISwapChain понадобилось для работы с буферами рисования и выводом нарисованного на экран. В любой программе будет присутствовать объект IDXGISwapChain, содержащий как минимум два буфера – задний (back buffer) и передний (front buffer). Передний буфер – это экран, точнее его часть внутри нашего окна. На заднем буфере мы в DirectX отрисовываем сцену, и, когда она готова, мы вызываем функцию g_pSwapChain->Present(…), которая копирует задний буфер в передний, т. е. показывает на экране все тщательно и любовно нарисованное.

Конечно, тут выползает вопрос: а зачем эта муть с двумя буферами? Почему бы сразу не рисовать на экран? Если такой вопрос не возник, или вы уже догадались – можете пропустить абзац. Давайте пофантазируем. Пусть мы пишем крутую игру, в которой злобный монстр, сверкая кровожадными глазами, рыщет вдоль стены замка на фоне сумеречного неба. Сначала мы расправимся с небом: нарисуем какую-нибудь сферу, обтянутую текстурой облаков. Потом нарисуем травку, потом стену замка и, наконец, красноглазого красавца. Вполне очевидно, что трава закроет собой часть неба, стена – часть неба и травы и монстр – часть стены. Представили, что будет на экране, если рисовать прямо на нем? А ведь рисовать придется десятки раз в секунду! И каждый раз на экране будут мелькать облака, стена и травка, которых не должно быть видно. У любого игрока разболятся глаза, он в панике выключит игру, и никто так и не увидит, как красиво монстр разрывает врагов на куски.

Последний объект – g_pRenderTargetView. Не пугайтесь, тут нет ничего сложного. Это и есть объект нашего заднего буфера, в котором мы будем рисовать свой трехмерный мир.

Перейдем к важным местам из функции InitDevice(), которая займется созданием и инициализацией только что изученных объектов. Вот так мы можем создать связку трех основных объектов:


// Структура, описывающая цепь связи (Swap Chain)

DXGI_SWAP_CHAIN_DESC sd;

ZeroMemory( &sd, sizeof( sd ) );  // очищаем ее

sd.BufferCount = 1;               // у нас один буфер

sd.BufferDesc.Width = 320;        // ширина буфера

sd.BufferDesc.Height = 210;             // высота буфера

sd.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; // формат пикселя буфере

sd.BufferDesc.RefreshRate.Numerator = 75;             // частота обновления экрана

sd.BufferDesc.RefreshRate.Denominator = 1;

sd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT; // назначение буфера

sd.OutputWindow = hWnd                  // привязываем к нашему окну

sd.SampleDesc.Count = 1;

sd.SampleDesc.Quality = 0;

sd.Windowed = TRUE;               // не полноэкранный режим

D3D11CreateDeviceAndSwapChain(NULL, D3D_DRIVER_TYPE_HARDWARE, NULL, 0, featureLevels, numFeatureLevels, D3D11_SDK_VERSION, &sd, &g_pSwapChain, &g_pd3dDevice, NULL, &g_pImmediateContext);

Сначала мы описываем передний буфер. sd.BufferDesc.Format установлен как DXGI_FORMAT_R8G8B8A8_UNORM. Это означает, что для каждого пикселя в буфере будут храниться 4 значения: красный, зеленый, синий компоненты цвета и альфа-канал, все по 8 бит. В DirectX вообще много всяких буферов, поэтому буферы, используемые для рисования, в Микрософте решили назвать Видами. Понятия не имею, почему именно видами. А вот передний буфер назвали буфером вывода. Поэтому флаг DXGI_USAGE_RENDER_TARGET_OUTPUT указывает, что буфер является целевым буфером для вывода графической информации, т. е. передним буфером. Название флага так и переводится: «использовать как целевой буфер для вывода». Наконец, мы вызываем функцию D3D11CreateDeviceAndSwapChain(…), которая по нашему описанию создает все устройства.

Ну вот, пока ничего сложного! Но теории получилось слишком много, поэтому остальное давайте рассмотрим по ходу написания программы. Нас еще ждет создание заднего буфера, функция рисования, а перед всем этим – обещанная пакость от Микрософт.

Для начала создайте пустой проект C++ Win32 под названием Urok1, добавьте в него файл Urok1.cpp и какую-нибудь иконку в ресурсы. У меня это значок DirectX’а, но можно что угодно впихнуть, хоть значок OpenGL – никто не обидится.

Теперь код:


//--------------------------------------------------------------------------------------

// Урок 1. Создание устройств Direct3D11. Основан на примере из SDK (c) Microsoft Corp.

//--------------------------------------------------------------------------------------

#include <windows.h>

#include <d3d11.h>

#include <d3dx11.h>

#include "resource.h"

//--------------------------------------------------------------------------------------

// Глобальные переменные

//--------------------------------------------------------------------------------------

HINSTANCE               g_hInst = NULL;

HWND                    g_hWnd = NULL;

D3D_DRIVER_TYPE         g_driverType = D3D_DRIVER_TYPE_NULL;

D3D_FEATURE_LEVEL       g_featureLevel = D3D_FEATURE_LEVEL_11_0;

ID3D11Device*           g_pd3dDevice = NULL;          // Устройство (для создания объектов)

ID3D11DeviceContext*    g_pImmediateContext = NULL;   // Контекст устройства (рисование)

IDXGISwapChain*         g_pSwapChain = NULL;          // Цепь связи (буфера с экраном)

ID3D11RenderTargetView* g_pRenderTargetView = NULL;   // Объект заднего буфера

//--------------------------------------------------------------------------------------

// Предварительные объявления функций

//--------------------------------------------------------------------------------------

HRESULT InitWindow( HINSTANCE hInstance, int nCmdShow );  // Создание окна

HRESULT InitDevice();      // Инициализация устройств DirectX

void CleanupDevice();      // Удаление созданнных устройств DirectX

LRESULT CALLBACK WndProc( HWND, UINT, WPARAM, LPARAM );       // Функция окна

void Render();      // Функция рисования

//--------------------------------------------------------------------------------------

// Точка входа в программу. Здесь мы все инициализируем и входим в цикл сообщений.

// Время простоя используем для вызова функции рисования.

//--------------------------------------------------------------------------------------

int WINAPI wWinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, LPWSTR lpCmdLine, int nCmdShow )

{

UNREFERENCED_PARAMETER( hPrevInstance );

UNREFERENCED_PARAMETER( lpCmdLine );

if (FAILED(InitWindow(hInstance, nCmdShow)))

return 0;

if (FAILED(InitDevice()))

{

CleanupDevice();

return 0;

}

// Главный цикл сообщений

MSG msg = {0};

while( WM_QUIT != msg.message )

{

if( PeekMessage( &msg, NULL, 0, 0, PM_REMOVE ) )

{

TranslateMessage( &msg );

DispatchMessage( &msg );

}

else // Если сообщений нет

{

Render();      // Рисуем

}

}

CleanupDevice();

return ( int )msg.wParam;

}

Здесь мы все уже рассматривали. При запуске программы создаем окно и устройства Direct3D, а затем входим в цикл обработки сообщений. Теперь еще довольно большой скучный кусок, который создает окно и обрабатывает системные сообщения. Весь код стандартный, без изменений. Так же выглядит создание окна на ассемблере.


//--------------------------------------------------------------------------------------

// Регистрация класса и создание окна

//--------------------------------------------------------------------------------------

HRESULT InitWindow( HINSTANCE hInstance, int nCmdShow )

{

// Регистрация класса

WNDCLASSEX wcex;

wcex.cbSize = sizeof(WNDCLASSEX);

wcex.style = CS_HREDRAW | CS_VREDRAW;

wcex.lpfnWndProc = WndProc;

wcex.cbClsExtra = 0;

wcex.cbWndExtra = 0;

wcex.hInstance = hInstance;

wcex.hIcon = NULL;

wcex.hCursor = LoadCursor (NULL, IDC_ARROW);

wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);

wcex.lpszMenuName = NULL;

wcex.lpszClassName = L"Urok01WindowClass";

wcex.hIconSm = LoadIcon (wcex.hInstance, (LPCTSTR)IDI_ICON1);

if (!RegisterClassEx(&wcex))

return E_FAIL;

// Создание окна

g_hInst = hInstance;

RECT rc = { 0, 0, 640, 480 };

AdjustWindowRect (&rc, WS_OVERLAPPEDWINDOW, FALSE);

g_hWnd = CreateWindow (L"Urok01WindowClass", L"Урок 1: Создание устройств Direct3D", WS_OVERLAPPEDWINDOW,

CW_USEDEFAULT, CW_USEDEFAULT, rc.right - rc.left, rc.bottom - rc.top, NULL, NULL, hInstance, NULL );

if (!g_hWnd)

return E_FAIL;

ShowWindow (g_hWnd, nCmdShow);

return S_OK;

}

//--------------------------------------------------------------------------------------

// Вызывается каждый раз, когда приложение получает сообщение

//--------------------------------------------------------------------------------------

LRESULT CALLBACK WndProc( HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam )

{

PAINTSTRUCT ps;

HDC hdc;

switch( message )

{

case WM_PAINT:

hdc = BeginPaint( hWnd, &ps );

EndPaint( hWnd, &ps );

break;

case WM_DESTROY:

PostQuitMessage( 0 );

break;

default:

return DefWindowProc( hWnd, message, wParam, lParam );

}

return 0;

}

Создавть API-шное окно на самом деле вовсе не обязательно. Можно и в проект MFC засунуть код DirectX, но зачем нам тащить с собой горы лишних скучных файлов? Рассмотрим долгожданную функцию создания Direct3D. На ней мы немного замедлимся, чтобы мозги не вскипели.


HRESULT InitDevice()

{

HRESULT hr = S_OK;

RECT rc;

GetClientRect( g_hWnd, &rc );

UINT width = rc.right - rc.left;           // получаем ширину

UINT height = rc.bottom - rc.top;   // и высоту окна

UINT createDeviceFlags = 0;

D3D_DRIVER_TYPE driverTypes[] =

{

D3D_DRIVER_TYPE_HARDWARE,

D3D_DRIVER_TYPE_WARP,

D3D_DRIVER_TYPE_REFERENCE,

};

UINT numDriverTypes = ARRAYSIZE( driverTypes );

С началом все понятно: мы при помощи функции GetClientRect(…) получаем координаты нашего окна и вычисляем его ширину и высоту. Они понадобятся позднее. Потом создается массив D3D_DRIVER_TYPE. Такое значение требуется указать при создании устройств Direct3D. Но мы не знаем заранее, поддерживается ли на компьютере хардварная обработка 3D (хотя на самом деле знаем: поддерживается). Поэтому Микрософт запихала возможные значения в массив в порядке убывания их желательности для нас и возрастания вероятности того, что система их поддерживает (ага, вероятность возрастает с 99,9% до 100). Вообще, Микрософт создает учебные примеры таким образом, что код, который можно написать одной строчкой, превращается в десять. Ниже мы пройдемся по этому массиву и будем пытаться создать устройство. Если пройдет ошибка, пробуем следующий тип. Всё примитивно, но поскольку сейчас все компьютеры поддерживают хардварную обработку, спрашивается, зачем такая перестраховка в учебном примере?


// Тут мы создаем список поддерживаемых версий DirectX

D3D_FEATURE_LEVEL featureLevels[] =

{

D3D_FEATURE_LEVEL_11_0,

D3D_FEATURE_LEVEL_10_1,

D3D_FEATURE_LEVEL_10_0,

};

UINT numFeatureLevels = ARRAYSIZE( featureLevels );

// Сейчас мы создадим устройства DirectX. Для начала заполним структуру,

// которая описывает свойства переднего буфера и привязывает его к нашему окну.

DXGI_SWAP_CHAIN_DESC sd;            // Структура, описывающая цепь связи (Swap Chain)

ZeroMemory( &sd, sizeof( sd ) );    // очищаем ее

sd.BufferCount = 1;                               // у нас один задний буфер

sd.BufferDesc.Width = width;                     // ширина буфера

sd.BufferDesc.Height = height;                          // высота буфера

sd.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;      // формат пикселя в буфере

sd.BufferDesc.RefreshRate.Numerator = 75;         // частота обновления экрана

sd.BufferDesc.RefreshRate.Denominator = 1;

sd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT; // назначение буфера - задний буфер

sd.OutputWindow = g_hWnd;                               // привязываем к нашему окну

sd.SampleDesc.Count = 1;

sd.SampleDesc.Quality = 0;

sd.Windowed = TRUE;                               // не полноэкранный режим

А вот список поддерживаемых версий действительно необходим. Кому нужна игра, которая перестанет запускаться при выходе обновленной версии DirectX? Если она запускается в 10 версии, то в 11-ой точно должна работать. Структуру с описанием переднего буфера мы уже рассматривали. Только теперь мы указываем в ней размеры нашего окна.


for( UINT driverTypeIndex = 0; driverTypeIndex < numDriverTypes; driverTypeIndex++ )

{

g_driverType = driverTypes[driverTypeIndex];

hr = D3D11CreateDeviceAndSwapChain ( NULL, g_driverType, NULL, createDeviceFlags, featureLevels, numFeatureLevels, D3D11_SDK_VERSION, &sd, &g_pSwapChain, &g_pd3dDevice, &g_featureLevel, &g_pImmediateContext );

if (SUCCEEDED(hr)) // Если устройства созданы успешно, то выходим из цикла

break;

}

if (FAILED(hr)) return hr;

Как и обещано, в цикле проходимся по массиву driverTypes. Вуаля! Все три устройства создаются одной строчкой. Но это не все. Нам ведь надо куда-то рисовать. А куда? Правильно, в задний буфер.


// Теперь создаем задний буфер. Обратите внимание, в SDK

// RenderTargetOutput - это передний буфер, а RenderTargetView - задний.

ID3D11Texture2D* pBackBuffer = NULL;

hr = g_pSwapChain->GetBuffer( 0, __uuidof(ID3D11Texture2D), (LPVOID*)&pBackBuffer );

if (FAILED(hr)) return hr;

// Я уже упоминал, что интерфейс g_pd3dDevice будет

// использоваться для создания остальных объектов

hr = g_pd3dDevice->CreateRenderTargetView( pBackBuffer, NULL, &g_pRenderTargetView );

pBackBuffer->Release();

if (FAILED(hr)) return hr;

// Подключаем объект заднего буфера к контексту устройства

g_pImmediateContext->OMSetRenderTargets( 1, &g_pRenderTargetView, NULL );

Не пугайтесь! Все кажется непонятным, но это только благодаря злодейскому замыслу Микрософта. В первой строчке создается объект текстуры. «Серьезно, текстуры?» — скажете вы. – «Что мы собрались покрывать текстурой, если еще даже рисовать негде?» На самом деле все очень умно. В Direct3D текстура – это не какое-то изображение, а просто область памяти. Память эту можно использовать в разных целях. Точнее, в трех. Как буфер для рисования, как буфер глубин и как собственно текстуру. Второй и третий случаи мы рассмотрим немного позже. Поэтому первой строчкой мы просто создали указатель на объект буфера. Вторая строчка загружает из объекта g_pSwapChain характеристики буфера. Он ведь должен точь-в-точь соответствовать переднему, да? Теперь давайте вспомним об объекте g_pRenderTargetView. Не зря же мы объявили его в самом начале. Он создается при помощи девайса по характеристикам, загруженным, как уже сказано, из g_pSwapChain. Напоминаю, через контекст мы будем рисовать. Поэтому в конце необходимо подключить созданный задний буфер к нему. Фффу, с этим разделались.


// Настройка вьюпорта

D3D11_VIEWPORT vp;

vp.Width = (FLOAT)width;

vp.Height = (FLOAT)height;

vp.MinDepth = 0.0f;

vp.MaxDepth = 1.0f;

vp.TopLeftX = 0;

vp.TopLeftY = 0;

// Подключаем вьюпорт к контексту устройства

g_pImmediateContext->RSSetViewports (1, &vp);

Еще один подарочек от Микрософта. Раньше вьюпорт всегда устанавливался по умолчанию при создании девайса, и не возникало необходимости инициализировать его самостоятельно. Здесь мы просто указываем, что верхний левый угол окна у нас имеет координаты (0, 0), а ширина и высота соответствуют ширине и высоте окна. Еще мы настраиваем масштаб буфера глубины. Просто пока не обращайте на это внимания. Вот теперь точно все.


return S_OK;

}

Осталось совсем немножко. Так выглядит функция очистки памяти:

//--------------------------------------------------------------------------------------

// Удалить все созданные объекты

//--------------------------------------------------------------------------------------

void CleanupDevice()

{

// Сначала отключим контекст устройства, потом отпустим объекты.

if( g_pImmediateContext ) g_pImmediateContext->ClearState();

// Порядок удаления имеет значение. Обратите внимание, мы удалеям

// эти объекты порядке, обратном тому, в котором создавали.

if( g_pRenderTargetView ) g_pRenderTargetView->Release();

if( g_pSwapChain ) g_pSwapChain->Release();

if( g_pImmediateContext ) g_pImmediateContext->Release();

if( g_pd3dDevice ) g_pd3dDevice->Release();

}

Пояснять просто нечего. И то, ради чего мы столько возились: функция рендеринга.


//--------------------------------------------------------------------------------------

// Рисование кадра

//--------------------------------------------------------------------------------------

void Render()

{

// Просто очищаем задний буфер

float ClearColor[4] = { 0.0f, 0.0f, 1.0f, 1.0f }; // красный, зеленый, синий, альфа-канал

g_pImmediateContext->ClearRenderTargetView( g_pRenderTargetView, ClearColor );

// Выбросить задний буфер на экран

g_pSwapChain->Present( 0, 0 );

}

Ну очень просто. 1. Создаем цвет, которым будем очищать задний буфер. 2. Очищаем задний буфер (напоминаю: мы всегда рисуем в буфере при помощи контекста, для этого он и создавался). 3. Показываем задний буфер зрителям (еще напоминание: SwapChain у нас как раз и занимается связью буфера с экраном)!

Готово! Компилируем… Ошибка. И правильно. Заходим в свойства проекта, Свойства конфигурацииaКомпоновщикaВвод. В «Дополнительные зависимости» вставляем «d3d11.lib d3dcompiler.lib d3dx11d.lib d3dx9d.lib dxerr.lib dxguid.lib winmm.lib comctl32.lib» (без кавычек). Теперь должно скомпоноваться нормально.

Быстрее запускаем и наслаждаемся восхитительным темно-синим цветом – пожалуй, лучшей находкой Микрософта со времен DX8… упс! А вот нет! Из тоски по старым временам я заменил цвет на тот самый классический жутко-синий, который так манил меня много лет назад. В моих уроках вам придется смириться с этим цветом. Хотите насладиться микрософтовским – просто откройте любой пример из их туториала.

Первая глава окончена. Все просто, так ведь? Вы внимательно рассмотрели код и комментарии, поэтому никаких вопросов не возникло. Еще совсем немного, несколько уроков, и вперед – к созданию собственного шедевра!

Первая глава окончена. Боже мой, какая муть! Если вам так кажется, не отчаивайтесь. Просто идите дальше. Изучите вторую главу, третью, а потом вернитесь к первой. Я уверен, она покажется вам очень простой.

Мы создали главное – возможность рисовать средствами DirectX. В следующем уроки мы воспользуемся этими инструментами, чтобы нарисовать банальный (или, если хотите, классический) треугольник.

До встречи!

                                                        Следующий урок >>


Яндекс.Метрика