DirectX


HYPERLINK "http://gameinstitute.ru/resources/lessons/directx11/urok-1-prosteyshee-prilozhenie-directx11/" \o "Постоянная ссылка на Урок 1. Простейшее приложение DirectX11" Урок 1. Простейшее приложение DirectX11
В этом уроке вы узнаете: как установить среду разработки для DirectX; как создавать окно; как инициализировать DirectX и получать доступ к интерфейсу Direct3Ddevice11; как отобразить наше первое окно DirectX.
Установка среды разработки DirectX11
Для того, чтобы работать с DirectX, необходимо иметь минимальные базовые навыки программирования под Windows. Более того, нужно быть неплохим программистом под Windows и хорошо знать C++, чтобы освоить DirectX и уже иметь определенный опыт написания программ под эту операционную систему. Для разработки подойдет компилятор MicrosoftVisualStudio2010. В наших примерах мы будем пользоваться MicrosoftDirectXSDK, этот SDK свободно распространяется Microsoft, так что вы легко сможете его найти в Интернете. После установки SDK в ProgramFiles вы найдете папку MicrosoftDirectX SDK. Для того, чтобы работать с нашими примерами, загрузите архив с примером, расположенный тут. Затем распакуйте папку с примером в любое место.  Откройте VisualC++  и загрузите в него проект. Вы также можете создать новый проект VisualC++ на основе одних файлов .cpp. О том как это сделать, будет сказано в конце этого урока. Сейчас же разберем наш пример простейшего приложения.
Архитектура нашей программы
Наше приложение Direct3D как и последующие приложения будет иметь следующую архитектуру: Для каждой отдельной операции мы будем использовать функцию. Большинство из этих функций будут вызываться из WinMain, некоторые из MsgProc. В наших примерах мы будем рассматривать отдельно каждую функцию и её содержимое.

Главная функция приложения: WinMain
Каждое приложение Windows должно иметь функцию WinMain. Мы будем использовать эту функцию с той-же целью, что и всегда – как главную функцию программы. Рассмотрим исходный текст нашей функции WinMain:
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;
}
Первое что нужно сделать, это создать главное окно программы. Мы просто сделаем вызов функции InitWindow и создадим окно. В следующих разделах этой статьи мы ознакомимся более детально со всеми функциями, которые мы вызываем из WinMain. Следующая операция – это инициализация устройства: InitDevice. В этой функции мы установим глобальные переменные а также инициализируем объекты, и наконец – в этой функции уже включится наш видеоускоритель. В данном примере мы не будем рассматривать функцию
Каждое приложение Windows имеет цикл обработки сообщений. Обработка сообщений нужна для того чтобы отслеживать поступающие в окно сообщения от системы – клавиши, сворачивание и разворачивание окна и т.п. На самом деле отслеживание сообщений будет выполнятся в функции WndProc, однако цикл обработки сообщений – это совершенно необходимая надстройка и часть любого приложения Windows и DirectX. Из этого цикла выполняется функция Render, то есть периодически будет обновятся динамически меняющееся содержимое сцены. Вы можете взглянуть на содержимое Render ниже по тексту и получить уже на 75% представление о том, вообщем, как работает наше приложение, однако архитектура DirectX11 изменена по сравнению с предыдущими версиями, так что нам нужно будет изучить все более детально. Итак, начнем.
Инициализация окна:InitWindow
Итак, откройте исходный текст нашего первого приложения и найдите функцию InitWindow. Эта функция просто создает окно для нашей программы. Не забываем, что так как мы пользуемся функциями Windows, то мы должны сделать в начале программы #include . Эта строка подключит необходимые функции из Windows API
Внутри функции InitWindow существует два основных вызова – это регистрация класса окна:
if( !RegisterClassEx( &wcex ) )        return E_FAIL;
И создание окна:
g_hInst = hInstance;
RECT rc = { 0, 0, 640, 480 };
AdjustWindowRect( &rc, WS_OVERLAPPEDWINDOW, FALSE );
g_hWnd = CreateWindow( LWCaption,LWName , WS_OVERLAPPEDWINDOW,
                           CW_USEDEFAULT, CW_USEDEFAULT, rc.right - rc.left, rc.bottom -                  rc.top, NULL, NULL, hInstance,NULL );
Обратите внимание, что при создании окна мы можем указать различные параметры, в том числе его размер.
Глобальные переменные
Для того, чтобы приложение функционировало, нужно объявить типы объектов и переменные для их хранения, так что в начале программы (сразу после #include)
вы видите глобальные переменные:
HINSTANCE               g_hInst = NULL;
HWND                    g_hWnd = NULL;
D3D_DRIVER_TYPE         g_driverType = D3D_DRIVER_TYPE_NULL;
ID3D11Device*           g_pd3dDevice = NULL;
ID3D11DeviceContext*    g_pImmediateContext = NULL;
IDXGISwapChain*         g_pSwapChain = NULL;
ID3D11RenderTargetView* g_pRenderTargetView = NULL;
Первые две строки нужны для хранения указателя на экземпляр окна. Дальнейшие переменные, которые мы заполним уже в следующем разделе относятся к Direct3D. Это –главный объект, через который мы будем обращаться к устройству Direct3D — g_pd3dDevice, а также второй главный наследуемый объект g_pImmediateContext. Также мы видим здесь объект для цепочки-обменаи рендер–таргет, то есть поверхности для рендеринга. В следующем разделе мы познакомимся поближе с этими объектами и их назначением.
Инициализация DirectX:InitDevice
Итак, продолжаем наполнять наше приложение новым кодом. Теперь нужно инициализировать собственно DirectX, так чтобы в нашем окне вообще что-то отображалось. Сечас нам предстоит сделать InitDevice. Хорошая  новость заключается в том, что ранее мы просто сделали окно, а вот дальнейший код приведет к тому, что включится ваш видео ускоритель.
Начнем рассмотрение функции InitDevice. Прежде чем получить доступ к корневому объекту ID3D11Device– Direct3D устройству, а также к второму корневому объекту ID3D11DeviceContext – Direct3D контексту, разберемся вообще вархитектуреDirect3D версии 11. В этой версии рисование при вызове функций ID3D11DeviceContext::Draw осуществляется не куда-то, что задано по умолчанию – то есть на экран или в окно, — в версии DirectX11 нужно прежде всего задать куда именновыполнять рисование. Вы можете догадаться, что рендер осуществляться может на различные поверхности, не обязательно на экран. Так что, нам нужно указать Direct3D на рендер-таргетRenderTarget – то есть куда рисовать – а также указать как это потом «перелистывать» — то есть осуществлять замену старого кадра новым при помощи цепочки-обмена IDXGISwapChain. На этом рисунке показывается то какие объекты используются Direct3D11 для вывода на экран:

Итак, при инициализации устройства Direct3D11 мы должны сделать несколько операций: создать объект g_pd3dDevice, создать объект g_pImmediateContext, создать рендер-таргетg_pRenderTargetView, указать 3d устройству что именно этот рендер-таргет используется для рисования в него любых примитив, а также создать объект «копировалку» g_pSwapChain с рендер-таргета на окно, то есть на экран. В последующем в программе мы уже не будем для рисования обращаться к рендер-таргету, для рисования будем использовать только ID3D11DeviceContext::Draw и затем отображать на экране при помощи g_pSwapChain::Present. Итак, создадим главные объекты, устройство и контекст:
DXGI_SWAP_CHAIN_DESC sd;
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 = 60;
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;
 
for( UINT driverTypeIndex = 0; driverTypeIndex < numDriverTypes; driverTypeIndex++ )
{
g_driverType = driverTypes[driverTypeIndex];
hr = D3D11CreateDeviceAndSwapChain( NULL, g_driverType, NULL, createDeviceFlags, featureLevels, numFeatureLevels,D3D11_SDK_VERSION, &amp;sd, &amp;g_pSwapChain, &amp;g_pd3dDevice, &amp;g_featureLevel, &amp;g_pImmediateContext );
}
Обратите внимание, что при создании объекта функцией D3D11CreateDeviceAndSwapChain также автоматически создается контекст и цепочка обмена. Обратите внимание на объект g_pImmediateContext, данный объект имеет очень важно значение, так как многие методы перенесены в него, так что вы будете часто в приложении обращаться к этому объекту для выполнения тех или иных действий Direct3D. Также обратите внимание на обширное количество параметров структуры sd. Эти параметры, как нетрудно догадаться, и есть параметры инициализации адаптера и Direct3D – то есть, формат пикселей, геометрические размеры окна, частота кадров и т.п. Вообще, в структуре DXGI_SWAP_CHAIN_DESC еще больше параметров, и возможно, какие-то вам пригодятся для того чтобы устанавливать различные режимы отображения и работы DirectX. В более ранних версиях DirectX также были многочисленные параметры, используемые при инициализации Direct3D устройства. Итак, мы создали главный объект, контекст и цепочку обмена, осталось только создать рендер-тергет и указать DirectX, что он предназначен для рисования. Создаем рендер-таргет:
// Создание рендер-таргета
ID3D11Texture2D* pBackBuffer = NULL;
hr = g_pSwapChain->GetBuffer( 0, __uuidof( ID3D11Texture2D ), ( LPVOID* )&amp;pBackBuffer );
 
hr = g_pd3dDevice->CreateRenderTargetView( pBackBuffer, NULL, &amp;g_pRenderTargetView );
pBackBuffer->Release();
Указываем Direct3D, что этот рендер-таргет надо использовать по умолчанию и что именно в него и надо всё рисовать:
g_pImmediateContext->OMSetRenderTargets( 1, &amp;g_pRenderTargetView, 0 );
Теперь настало время узнать о самом интересном, как собственно рисовать, то есть разберем функцию Render. А потом посмотрим на то, как перехватывать сообщения от клавиатуры, чтобы мы имели возможность управления нашим приложением, и главное, монстрами.
Отображение сцены:Render
Теперь нужно что-нибудь отобразить в нашем окне. Функция Render обычно вызывается в играх несколько десятков раз в секунду. Но нам не нужно заботится о том, чтобы устанавливать конкретные цифры. Функция g_pSwapChain::Present
сама вносит необходимую задержку, в соответствии с тем, какую частоту мы поставили в предыдущем разделе в структуре sd. Так что мы будем просто вызывать Render так часто, как получиться, а задержка выставиться автоматически. Рассмотримфункцию:
void Render()
{
// Очистка рендер-таргета
float ClearColor[4] = { 0.0f, 0.9f, 0.5f, 1.0f }; // цвет
g_pImmediateContext->ClearRenderTargetView( g_pRenderTargetView, ClearColor );
 
// Рендер
 
// Вывод на экран содержимого рендер-таргета
g_pSwapChain->Present( 0, 0 );
}
В нашей функции Render мы сначала очищаем рендер-таргет, затем рисуем на нем, — об том как это делать – в дальнеших уроках, — а затем выведем изображение на экран. В данном примере мы просто заполняем рендер-таргет зеленым цветом и сразу-же выводим изобажение на экран. Обратите внимение, что функции g_pSwapChain::Present не передаются никакие параметры о том, какой рендер-таргет нужно выводить на экран. В функцииInitDevice мы уже дали знать Direct3D какойрендер-таргет мы используем для рисования. Так что все операции рисования, сколько бы их не было Direct3D будет осуществлять на один раз установленном в начале RenderTarget — е.
Цикл обработки сообщений
Итак, возвратимся на некоторое время к функции WinMain. Каждое приложение Windows имеет так называемый цикл обработки сообщений. В этом цикле мы должны получить сообщения от системы Windows. В идеале конечно мы могли бы просто запускать в цикле функцию Render для отрисовки всего что будет происходить в окне несколько десятков раз в секунду, не заботясь об остальном. Однако нужно еще и перехватывать сообщения от мыши и клавиатуры, да и собственно если нажата кнопка закрытия окна – это нужно тоже как-то обрабатывать. Так что для этих целей нужен цикл обработки сообщений.
// Цикл обработки сообщений
MSG msg = {0};
while( WM_QUIT != msg.message )
{
        if( PeekMessage( &amp;msg, NULL, 0, 0, PM_REMOVE ) )
        {
            TranslateMessage( &amp;msg );
            DispatchMessage( &amp;msg );
        }
        else
        {
            Render();
        }
}
CleanupDevice();
return ( int )msg.wParam;
}
На этом функция WinMain завершена. Обратите внимание, что внутри цикла выполняется функция Render, что не удивительно, так как периодически нужно перерисовывать динамически изменяющуюся в играх сцену. Цикл обработки сообщений завершается тогда когда поступает сообщение WM_QUIT, прерывающий цикл. Функция WinMain завершается и также завершается программа и закрывается окно.
Очистка объектов:CleanupDevice
Когда окно закрывается, нужно очистить все ранее инициализированные объекты Direct3D. Для этого предназначена небольшая функция очистки, вызываемая из MsgProc. То есть, когда приходит сообщение о закрытии окна (а также когда приходят и другие особщения) Windows вызывает MsgProc, и если окно закрывается, то мы делаем из MsgProc очистку объектов и завершение приложения.
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();
}
Цикл обработки сообщений
Перед нашей главной и глобальной функцией WinMain должна быть еще одна функция, MsgProc. Это так называемся CallBack функция, и Windows вызывает её даже самостоятельно, когда ей это потребуется. Что же касается наполнения функции MsgProc то в неё приходят различные сообщения Windows, и нужно на это как то реагировать. В дальнейшем внутрь неё вы конечно же вставите обработку клавиш «вперед, назад, влево вправо» а также обязательно «пробел» или «ctrl» для того, чтобы эффективно и быстро нажав на них уничтожать ваших ненавистных 3d– монстров из плазмомета.
LRESULT CALLBACK WndProc( HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam )
{
    PAINTSTRUCT ps;
    HDC hdc;
 
    switch( message )
    {
        case WM_PAINT:
            hdc = BeginPaint( hWnd, &amp;ps );
            EndPaint( hWnd, &amp;ps );
            break;
 
        case WM_DESTROY:
            PostQuitMessage( 0 );
            break;
 
        default:
            return DefWindowProc( hWnd, message, wParam, lParam );
    }
 
    return 0;
}
Кстати, как бонус, можно рассказать собственно как обрабатывать указанные выше клавиши, для этого нужно немного изменить пример. Скачать измененный пример можно тут.
На этом наше первое приложение завершено, с чем мы можем поздравить себя а также будущих пользователей наших Direct3D программ.
Обзор приложения
Теперь остается только скомпилировать и запустить приложение.  Исходный код приложения вы можете скачать тут. Не забываем, что в системе должен быть установлен DirectX версии не ниже 11. В текущих версиях Windows, например Windows 7, а также Windows 8 всё уже установлено, так что заботиться об установке DirectX не надо, и наше приложение успешно запуститься на них и будет прекрасно функционировать.

Данное приложение всего-лишь отображает поверхность, заполненную зеленым цветом. Однако, если это произошло, то это значит что все ок, и это не мало, так как мы создали окно, инициализировали DirectX, очистили поверхность окна синим цветом, вообщем, сделали достаточно для того, чтобы приступать к отображению чего-то трехмерного, чем и займемся в дальнейших уроках.
Дополнение к уроку. Настройка среды разработки
Помните, что для того чтобы работать с DirectX11 для начала нужно установить MicrosoftDirectXSDK. Для того, чтобы работать с файлами .cpp в MicrosoftVisualC++ нужно либо открыть существующий проект, либо создать новый. Если вы решите создать новый проект DirectX11 на основе существующих файлов C++, то необходимо воспользоваться следующей процедурой.
Открыть MicrosoftVisualC++ 2010
Выбрать пункт меню «Файл->Создать->Проект из существующего кода»
Далее нужно выбрать папку проекта, в этой папке уже должны быть будут .cpp файлы, назначить имя проекта, например, MyProject1и далее нажать копку «Создать».
VisualC++ создаст проект, при этом в обозревателе решений будет ваш проект MyProject1. В нем будут следующие папки:
Header Files
Resource Files
Source Files
Внешние зависимости
Прежде всего, убедитесь что в папке «SourceFiles» расположен ваш код .cpp. После этого нужно настроить собственно проект. Для этого нужно нажать в обозревателе решений на MyProject1, то есть перейти на проект, и затем выбрать в меню «Проект->Свойства».
После этого нужно нажать на «Свойства конфигурации».
Обратите внимание, что существует несколько конфигураций: Debug, Release и Все конфигурации. Перед любым дальнейшим изменением выберите в выпадающем списке конфигурации «все конфигурации».Это нужно сделать обязательно, иначе опции, которые вы будете устанавливать применятся не ко всему, а к чему-то отдельному, что будет мешать. Итак, настроим наш проект.
Во вкладке «Общее» выберите «Набор символов->Использовать набор символов Юникода»
Во вкладке «Каталоги VC++» выберите «Каталоги включения», далее добавьте следующий элемент (нужно впечатать текст, который идет далее): $(DXSDK_DIR)Include
Во вкладке «Каталоги VC++» выберите «Каталоги библиотек», далее добавьте следующий элемент (нужно впечатать текст, который идет далее): $(DXSDK_DIR)Lib\x86
Во вкладке «Компоновщик» выберите пункт «Ввод», затем добавьте следующий список:
d3d11.lib
d3dcompiler.lib
d3dx11d.lib
d3dx9d.lib
dxerr.lib
dxguid.lib
winmm.lib
comctl32.lib
Итак, на этом конфигурация MiscrosoftVisualC++ завершена и можно редактировать код C++ вашего проекта. Чтобы вы поняли, для чего нужны были настройки, описанные выше, можно пару слов сказать о них. Самое главное, что мы сделали это прописали пути к файлам Include а также пути к библиотекам Lib. Однако все те библиотеки, которые есть в Lib, либо часть из них, еще нужно подключить к проекту. Для этого и нужно было прописывать список файлов с расширением lib – тем самым, мы подключили библиотеки .Lib к проекту.
Заключение
В этом уроке мы научились создавать окно и инициализировать устройство Direct3D11. Также мы знаем, как обрабатывать сообщенияWindows и в каком месте программы расположена функция Render. В следующем уроке мы научимся рисовать 3d примитиву на подготовленной поверхности.

Урок 2. Вывод 3d геометрии на экран в DirectX11В этом уроке мы изучим, какие средства используются в DirectX для отображения примитив; также мы научимся использовать индексный и вершинный буфер; и узнаем каким образом отображение примитив связано с шейдерами.
Обзор архитектуры для вывода геометрии в DirectX11
В отличие от предыдущих версий, в DirectX11 отображение чего-либо на экране означает не просто вызов некого метода интерфейса, который сделает и подготовит видеоадаптер для нас. Чтобы отобразить что-то в DirectX11 нужно самостоятельно подготовить все данные для отправки их в видеоадаптер, а потом уже вызывать данный метод. Итак, рассмотрим из чего состоит графический конвейер:

Все данные, которые нужно нарисовать, то есть каркас 3d объекта загружается в вершинный и индексный буфер видеоадаптера – VertexBuffer и соответственно IndexBuffer. Каждый 3d объект состоит из вершин, эти вершины группируются по три, образуя набор полигонов – то есть треугольников, фейсов. Из наборов полигонов и состоит 3d модель.
Далее в адаптер загружается шейдер, то есть программа которая поможет видеоадаптеру обработать наши наборы вершин и полигонов. Шейдеры бывают двух видов, вершинный и пиксельный, и должны быть одновременно загружены в адаптер.Вершинныйшейдер служит промежуточным звеном между пиксельным шейдером и входящими данными вершинного буфера. В основном вершинный шейдер нужен для поворота всех точек модели таким образом чтобы модель правильно отобразилась на экране с учетом точки расположения камеры. Также, осуществляется расчет перспективные — если модель далеко, то она маленькая, если близко – большая. Словом, вершинныйшейдер отвечает за правильное расположение модели на экране и обрабатывает 3d-координаты вершин.
После того как данные обработаны, они поступают из вершинного шейдера в пиксельный. В нем происходит обработка каждой точки полигона. То есть он закрашивается. При этом цвет закраски задается в пиксельном шейдере. Также пиксельный шейдер можно запрограммировать так, чтобы цвета брались из текстуры – так и происходит в 3d играх если модель имеет текстуру. Но предположим, что вершинный шейдер выдал для каждой из трех вершин свой цвет, каким должен получиться треугольник? Правильно, трехцветным. Пиксельный шейдер интерполирует цвета для всех входящих вершин, так что они будут плавно переходить от одной вершины к другой. Для этого не нужно добавлять в пиксельный шейдер специальный код, треугольник станет цветным только потому что выходящими данными вершинного шейдера является полигон – то есть набор из трех точек, а входящими данными для пиксельного шейдера уже не является полигон и координаты этих точек. Сам адаптер плавно передвигает «точку» рисования между вершинами постепенно заполняя треугольник(полигон) сплошным цветом и при этом для каждой маленькой суб-точки закраски вызывая пиксельный шейдер. Надо сказать что отсюда следует непреложный вывод: никогда не грузите пиксельный шейдер чрезмерно кодом – нескольких строк кода почти всегда достаточно, а при лишней загрузке пиксельного шейдера могут быть серьезные тормоза – точек на экране очень много.
Теперь немного о константах. На самом деле одного лишь шейдера и подготовленного вершинного и индексного буферов зачастую недостаточно. Нужно еще и на ходу, динамически, немного «подкручивать» параметры работы шейдера. Например, изменяется положение камеры, освещение, и т.п. Для этого существуеют константы шейдера. То есть перед тем как шейдер задействуется, мы устанавливаем константы.
Инициализация геометрии: InitGeometry
Теперь разберемся, что же нужно сделать чтобы вывести геометрию на экран в DirectX11. Перед тем как приступать к изучении примера, вы можете скачать его исходный код, расположенный тут. Напомним, что в предыдущем уроке мы создали функцию InitDevice. В этой программе также есть эта функция а также остальные, которые мы рассматривали в предыдущем примере, но мы их заново рассматривать не будем а просто включим в нашу программу. Функция InitGeometry будет выполняться перед циклом обработки сообщений, то есть после того, как мы произвели инициализацию Direct3D устройства. Итак, подготовим и создадим геометрию в функции InitGeometry. Мы разобьем все наше рисование на две части – на инициализацию – сначала мы подготовим модель для рисования и на рендеринг – далее мы можем рисовать модель столько раз сколько нужно. В данной части урока мы рассмотрим инициализацию. Первое что мы сделаем в нашей функции InitGeometry — это загрузим код шейдера. На самом деле это просто, просто укажем имя файла шейдера в формате .fx и вызовем фукнцию для загрузки:
// Загружаем шейдеры
ID3DBlob* pVSBlob = NULL;
HRESULT hr;
hr = CompileShaderFromFile( L"Article2.fx", "VS", "vs_4_0", &amp;pVSBlob );
 
// Вершинныйшейдер
hr = g_pd3dDevice->CreateVertexShader( pVSBlob->GetBufferPointer(), pVSBlob->GetBufferSize(), NULL, &amp;g_pVertexShader );
 
// Определение формата вершинного буфера
D3D11_INPUT_ELEMENT_DESClayout[] =
{
{ "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 },
{ "COLOR", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0 },
};
UINT numElements = ARRAYSIZE( layout );
 
// Созданиеформатабуфера
hr = g_pd3dDevice->CreateInputLayout( layout, numElements, pVSBlob->GetBufferPointer(), pVSBlob->GetBufferSize(), &amp;g_pVertexLayout );
pVSBlob->Release();
 
// Установкаформатабуфера
g_pImmediateContext->IASetInputLayout( g_pVertexLayout );
 
// Пиксельныйшейдер
ID3DBlob* pPSBlob = NULL;
hr = CompileShaderFromFile( L"Article2.fx", "PS", "ps_4_0", &amp;pPSBlob );
 
// Пиксельныйшейдер
hr = g_pd3dDevice->CreatePixelShader( pPSBlob->GetBufferPointer(), pPSBlob->GetBufferSize(), NULL, &amp;g_pPixelShader );
pPSBlob->Release();
Так как код шейдера состоит из пиксельного шейдера и вершинного шейдера, то функцию загрузки мы должны выполнить два раза – сначала для вершинного шейдера а затем для пиксельного. Затем, перед тем как отобразить объект на экране, мы будем также два раза назначать шейдерDirect3D устройству текущим: сначала назначим текущий вершинный шейдер, затем пиксельный; после этого отобразим модель. После того как шейдеры загружены, они представляются в приложении как два объекта: g_pVertexShader и g_pPixelShader соответственно.
Вторым этапом инициализации модели будет установка формата вершинного буфера. То, что мы будем передавать в вершинный шейдер может иметь различные форматы, и мы должны быть уверены, что в вершинный шейдер мы передаем данные нужного формата. Откроем файл Article02.fx и посмотрим на то, что принимает вершинный шейдер:
VS_OUTPUT VS( float4 Pos : POSITION, float4 Color : COLOR ) { ... }
Данное объявление переменных входящих параметров вершинногошейдера соответствует структуре:
struct VS_INPUT
{
float4 Pos   : POSITION;
float4 Color : COLOR;
};
Итак, мы должны передать в вершинныйшейдер для каждой вершины по два числа формата float4.
// Определение формата вершинного буфера
D3D11_INPUT_ELEMENT_DESC layout[] =
{
{ "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 },
{ "COLOR", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0 },
};
Теперь осталось только установить этот формат для D3DDevice. Если в вашей программе все вершины имеют один и тот-же формат, код ниже можно выполнять только один раз в начале программы. Далее Direct3D везде будет использовать этот формат. Установим ранее объявленный формат вершинного буфера:
// Создание формата буфера
hr = g_pd3dDevice->CreateInputLayout( layout, numElements, pVSBlob->GetBufferPointer(), pVSBlob->GetBufferSize(), &amp;g_pVertexLayout );
pVSBlob->Release();
 
// Установка формата буфера
g_pImmediateContext->IASetInputLayout( g_pVertexLayout );
Обратите внимание, что установка формата вершинного буфера выполняется совместно с загрузкой шейдеров, это необходимо потому, что объявление формата вершинного буфера напрямую связано с форматом входящих данных вершинного шейдера, собственно, эти форматы должны совпадать.
Инициализация геометрии – создание вершин: InitGeometry
Теперь зададимся вопросом: предположим у нас имеется задумка о том, что будет из себя представлять наш будущий объект, короче – мы имеем конкретные координаты 3d вершин, имеем их количество а также знаем номера вершин которые будет группироваться в треугольники. Но как все эти данные установить в 3d адаптер в качестве вершинного и индексного буфера?

Ответ несложный. Нужно сначала определить эти данные, потом создать на их основе объект вершинного буфера, и затем просто установить этот вершинный буфер в Direct3D (одновременно может быть установлен только один вершинный буфер – то есть устанавливаем буфер для одной модели, рисуем её, потом устанавливаем для другой, рисуем и т.д. Итак, создадим вершинный буфер:
// Создание геометрии для вершинного буфера
SimpleVertexvertices[] =
{
        { XMFLOAT3( -1.0f, 1.0f, -1.0f ), XMFLOAT4( 0.0f, 0.0f, 1.0f, 1.0f ) },
        { XMFLOAT3( 1.0f, 1.0f, -1.0f ), XMFLOAT4( 0.0f, 1.0f, 0.0f, 1.0f ) },
        { XMFLOAT3( 1.0f, 1.0f, 1.0f ), XMFLOAT4( 0.0f, 1.0f, 1.0f, 1.0f ) },
        { XMFLOAT3( -1.0f, 1.0f, 1.0f ), XMFLOAT4( 1.0f, 0.0f, 0.0f, 1.0f ) },
        { XMFLOAT3( -1.0f, -1.0f, -1.0f ), XMFLOAT4( 1.0f, 0.0f, 1.0f, 1.0f ) },
        { XMFLOAT3( 1.0f, -1.0f, -1.0f ), XMFLOAT4( 1.0f, 1.0f, 0.0f, 1.0f ) },
        { XMFLOAT3( 1.0f, -1.0f, 1.0f ), XMFLOAT4( 1.0f, 1.0f, 1.0f, 1.0f ) },
        { XMFLOAT3( -1.0f, -1.0f, 1.0f ), XMFLOAT4( 0.0f, 0.0f, 0.0f, 1.0f ) },
};
D3D11_BUFFER_DESC bd;
ZeroMemory( &amp;bd, sizeof(bd) );
bd.Usage = D3D11_USAGE_DEFAULT;
bd.ByteWidth = sizeof( SimpleVertex ) * 8;
bd.BindFlags = D3D11_BIND_VERTEX_BUFFER;
bd.CPUAccessFlags = 0;
D3D11_SUBRESOURCE_DATA InitData;
ZeroMemory( &amp;InitData, sizeof(InitData) );
InitData.pSysMem = vertices;
hr = g_pd3dDevice->CreateBuffer( &amp;bd, &amp;InitData, &amp;g_pVertexBuffer );
if( FAILED( hr ) )
returnhr;
Вы наверняка обратили внимание, что входящие данные для буфера – это обычный массив. Так что входящие данные для буфера можно не только заполнять константами, как в примере – а также можно генерировать их в массив динамически или загружать из файла.  Теперь установим этот буфер как вершинный буфер Direct3D по умолчанию:
// Установка вершинного буфера
UINT stride = sizeof( SimpleVertex );
UINT offset = 0;
g_pImmediateContext->IASetVertexBuffers( 0, 1, &amp;g_pVertexBuffer, &amp;stride, &amp;offset );
Теперь создадим индексный буфер чтобы знать какие вершины группируются в треугольники (полигоны):
// Создание индексного буфера
WORDindices[] =
{
        3,1,0,
        2,1,3,
 
        0,5,4,
        1,5,0,
 
        3,4,7,
        0,4,3,
 
        1,6,5,
        2,6,1,
 
        2,7,6,
        3,7,2,
 
        6,4,5,
        7,4,6,
};
bd.Usage = D3D11_USAGE_DEFAULT;
bd.ByteWidth = sizeof( WORD ) * 36;        // 36 vertices needed for 12 triangles in a triangle list
bd.BindFlags = D3D11_BIND_INDEX_BUFFER;
bd.CPUAccessFlags = 0;
InitData.pSysMem = indices;
hr = g_pd3dDevice->CreateBuffer( &amp;bd, &amp;InitData, &amp;g_pIndexBuffer );
Установим индексный буфер и его топологию (она всегда будет одинаковой).
 
// Установка индексного буфера
g_pImmediateContext->IASetIndexBuffer( g_pIndexBuffer, DXGI_FORMAT_R16_UINT, 0 );
// Установкатипапримитив
g_pImmediateContext->IASetPrimitiveTopology( D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST );
Теперь для того, чтобы отобразить вершины нужно будет всего лишь вызывать каждый кадр метод g_pImmediateContext::DrawIndexed.  Этот код пригодиться дальше в программе, и здесь он приведен лишь для примера:
g_pImmediateContext->DrawIndexed( 36, 0, 0 );
В параметрах процедуры DrawIndexed передается количество вершин в модели.
На этом функция InitGeometry завершена и мы можем смело приступать к функции Render, где мы каждый кадр отобразим нашу модель. В нашем уроке модель представляет из себя куб. У него 8 вершин, однако чтобы образовать сплошной куб их надо соеденить 12-ю полигонами. Также не будем забывать что в Direct3D версии 11 не обойтись без шейдеров и мы уже загрузили код шейдера в нашу программу, так что бегло рассмотрим этот код, после того как разберемся с содержимым Render.
Создание буфера глубины: ZBuffer
Несмотря на то, что мы отлично создали геометрию и заполнили данными геометрии вершинный и индексный буфер, геометрия не будет отображаться правильно, если не будет создан ZBuffer. Еще более очевидно будет влияние ZBuffera, если на экране будет две примитивы, одна впереди другой. Если ZBuffer не будет создан, то один объект может некорректно перекрывать другой, то есть задний объект может неожиданно отобразиться на переднем плане.
// Создание поверхности для Z-буфера
D3D11_TEXTURE2D_DESC descDepth;
ZeroMemory( &amp;descDepth, sizeof(descDepth) );
descDepth.Width = width;
descDepth.Height = height;
descDepth.MipLevels = 1;
descDepth.ArraySize = 1;
descDepth.Format = DXGI_FORMAT_D24_UNORM_S8_UINT;
descDepth.SampleDesc.Count = 1;
descDepth.SampleDesc.Quality = 0;
descDepth.Usage = D3D11_USAGE_DEFAULT;
descDepth.BindFlags = D3D11_BIND_DEPTH_STENCIL;
descDepth.CPUAccessFlags = 0;
descDepth.MiscFlags = 0;
hr = g_pd3dDevice->CreateTexture2D( &amp;descDepth, NULL, &amp;g_pDepthStencil );
 
// Создание z-буфреа
D3D11_DEPTH_STENCIL_VIEW_DESC descDSV;
ZeroMemory( &amp;descDSV, sizeof(descDSV) );
descDSV.Format = descDepth.Format;
descDSV.ViewDimension = D3D11_DSV_DIMENSION_TEXTURE2D;
descDSV.Texture2D.MipSlice = 0;
hr = g_pd3dDevice->CreateDepthStencilView( g_pDepthStencil, &amp;descDSV, &amp;g_pDepthStencilView );
Чтобы этого не произошло, в функции InitDevice мы добавим код для создания объекта типа ID3D11DepthStencilView, представляющий из себяZBuffer и далее назначим этот объект Direct3D устройству. Еще одна важная часть операций с буфером глубины заключается в том, что в функции Render мы должны очищать его содержимое.
Отображение геометрии: Render
На самом деле код Render небольшой. Это потому, что все подготовительные данные уже сделаны и каждый кадр ничего изменять не нужно, нужно просто отображать то что имеется. Посмотримнакод:
void Render()
{
// Установка трансформации для куба
g_World = XMMatrixRotationY( 3.14159f/4.0f );
 
// Очистка рендер-таргета и буфера глубины
float ClearColor[4] = { 0.0f, 0.9f, 0.5f, 1.0f }; // цвет
g_pImmediateContext->ClearRenderTargetView( g_pRenderTargetView, ClearColor );
g_pImmediateContext->ClearDepthStencilView( g_pDepthStencilView, D3D11_CLEAR_DEPTH, 1.0f, 0 );
 
// Установка констант шейдера
ConstantBuffercb;
cb.mWorld = XMMatrixTranspose( g_World );
cb.mView = XMMatrixTranspose( g_View );
cb.mProjection = XMMatrixTranspose( g_Projection );
g_pImmediateContext->UpdateSubresource( g_pConstantBuffer, 0, NULL, &amp;cb, 0, 0 );
 
// Рендер куба
g_pImmediateContext->VSSetShader( g_pVertexShader, NULL, 0 );
g_pImmediateContext->VSSetConstantBuffers( 0, 1, &amp;g_pConstantBuffer );
g_pImmediateContext->PSSetShader( g_pPixelShader, NULL, 0 );
g_pImmediateContext->DrawIndexed( 36, 0, 0 );       // 36 вершин образуют 12 полигонов, по три вершины на полигон
 
// Вывод на экран содержимого рендер-таргета
g_pSwapChain->Present( 0, 0 );
}
В этом коде нужно обратить внимание на то, что перед вызовом метода DrawIndexed необходимо установить вершинный и пиксельный шейдеры. Причем это делается раздельно, сначала устанавливается вершинный и затем пиксельный шейдер. Это очень полезно, так как мы можем назначать различные шейдеры, наподобие материалов. Причем мы можем использовать один вершинный шейдер и несколько различных пиксельных, или наоборот.
Обзор приложения
Теперь остается только скомпилировать и запустить приложение.

В отличие от предыдущего урока, мы видим более конкретный результат. Данное приложение не только отображает поверхность, заполненную зеленым цветом, оно также отображает нашу первую Direct3D11 модель. Исходный текст приложения к данному уроку вы можете найти тут. Не забывайте, что так как это Direct3D11, то здесь все сразу делается через шейдеры.
Обзор шейдера для нашего приложения
Практически при любом отображении чего-либо на экран в DirectX11 необходимо использовать шейдеры. Шейдерпредставляет из себя программу на языке HLSL. Этот язык подобен языку C. В предыдущих разделах мы загрузили шейдер, но совершенно его не рассмотрели. Сейчас мы займемся этим. Шейдеры делятся на два типа – вершинный и пиксельный. Рассмотрим вершинный шейдер. Его назначение – просто преобразовывать координаты вершин. Если мы не будем их преобразовывать, то все координаты будут выведены на экран в таком пространстве:

То есть, если мы отобразим треугольник, находящейся в плоскости XY, то он предстанет в неизмененном виде. Однако если треугольник будет в плоскости XZ, то он будет в виде тонкой линии или не будет виден совсем. Если же мы установим камеру, то мы сможем вращать её вокруг треугольника, при этом также будут пересчитываться его координаты для вывода в вышеуказанное пространство экрана(-1,1),(-1,1). Пересчетом координат и занимается вершинныйшейдер – для этого каждая координата умножается на несколько матриц преобразования.
Стоит отметить, что если мы не будем умножать координаты точек на матрицы, или будем умножать их на еденичные матрицы – что соответствует отсутствию всякого преобразования, то это также можно использовать, например, для отображения всевозможных надписей, элементов интерфейса и другой плоской графики поверх основной 3d графики сцены. Также такая техника подойдет для игр где графика выполнена в виде спрайтов. На самом деле она настолько востребована, что системы частиц (партиклы) в какой то мере также используеют только частичное преобразование 3d координат, в результате чего частицы всегда выводятся на экран плоско, однако облака этих частиц, конечно же находятся в некотором объеме.
Но мы отступили от темы. Рассмотрим код нашегошейдера. Обратимся к вершинному шейдеру:
//------------------------------------------------------------------------------------
// Constant Buffer Variables
//------------------------------------------------------------------------------------
cbuffer ConstantBuffer : register( b0 )
{
      matrix World;
      matrix View;
      matrix Projection;
}
 
//------------------------------------------------------------------------------------
struct VS_OUTPUT
{
    float4 Pos : SV_POSITION;
    float4 Color : COLOR0;
};
 
//------------------------------------------------------------------------------------
// Vertex Shader
//------------------------------------------------------------------------------------
VS_OUTPUT VS( float4 Pos : POSITION, float4 Color : COLOR )
{
    VS_OUTPUT output = (VS_OUTPUT)0;
    output.Pos = mul( Pos, World );
    output.Pos = mul( output.Pos, View );
    output.Pos = mul( output.Pos, Projection );
    output.Color=float4(0.5f,0.5f,0.5f,1.0f);
    returnoutput;
}
Обратите внимание на процедуру VS в нижней части кода. Это и есть вершинный шейдер. Входящими данными нашего вершинного шейдера являются параметры функции VS. Они соответствует формату, который мы рассматривали ранее в уроке, каждая вершина содержит координату и цвет. Сама процедура вершинногошейдера называется VS. Как вы можете видеть, шейдер имеет на выходе практически тот-жеформат что и на входе. В него входят координаты и выходят тоже координаты, только измененные. В текущем уроке мы умножаем координаты на единичные матрицы, что соответствует отсутствию всякого преобразования. (Для наглядности урока матрицы все же установлены так чтобы куб был повернут на 450 относительно вертикальной оси). Фактически, мы могли бы и просто приравнять output.Pos к input.Pos, тогда бы куб отображался плоским. Однако в следующих уроках мы установим матрицы для камеры и трансформаций и немного оживим сцену, поворачивая камеру на любые углы и рассматривая модель с различных сторон, а также приближая и отдаляя камеру.
Теперь рассмотрим пиксельный шейдер. Он совсем небольшой и входящими данными являются координаты и цвет. Но координаты нам теперь уже не нужны, так как видеоадаптер уже поставил точку в нужное место экрана, и спрашивает каким цветом её закрасить.
//------------------------------------------------------------------------------------
// Pixel Shader
//------------------------------------------------------------------------------------
float4 PS( VS_OUTPUT input ) : SV_Target
{
    returninput.Color;
}
Следовательно, мы возвращаем просто цвет. Однако в дальнейших уроках мы изучим, каким образом осуществлять выборку этого цвета из текстур здесь же, в пиксельномшейдере.
И последним разделом нашего шейдера является техника. Техника(technique) это уже как бы не код C, это что-то вроде материала. То есть для каждого вида материала мы можем выбрать произвольный вершинный и пиксельный шейдеры. То есть, в одном и том-же файле .fx может находиться много различных вершинных и пиксельных шейдеров, назначая определенный материал (но в DirectX он называется техникой).
technique11 Render
{
pass P0
{
SetVertexShader( CompileShader( vs_4_0, VS() ) );
SetGeometryShader( NULL );
SetPixelShader( CompileShader( ps_4_0, PS() ) );
}
}
Обратите внимание где именно в технике указывается название функций для вершинного и пиксельного шейдера. Указанием имен функций мы даем понять данному материалу (технике) что именно этот вершинный и пиксельный шейдер нужно установить.
Заключение
В данном уроке вы изучили как выводить 3d геометрию на экран и познакомились с шейдерами, которые нужны для её отображения. В следующем уроке мы познакомимся уустановкой камеры, но сначала мы немного погрузимся в обширный мир 3d математики, и вплотную столкнемся с матрицами.

Урок 3. Математические основы Direct3DПрежде чем приступать к дальнейшему изучению DirectX необходимо ознакомится с тем, какими данными мы будем в дальнейшем оперировать. Конечно, вы уже знаете что одна вершина модели характеризуется тремся координатами XYZ, однако для того чтобы работать в DirectX этого недостаточно.
Знакомство с базовыми 3d представлениями данных
Расстояние от начала координатной системы и положение точки в пространстве называется координатой.

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


Компонентами вектора A называются длины проекций этого вектора на каждую из осей координат.
Если вкратце, то компоненты вектора – это просто координаты конечной точки вектора. Задание трехмерного вектора в виде компонент математически выглядит как:

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

Преобразования векторов и операции с ними
Вектора, можно рассматривать как обычные числа, ведь одной переменной можно присвоить целый вектор:

Математические операции с векторами
Так как теперь вектор является своего рода числом, то с ним можно осуществлять различные математические опреции, а именно:
Сложение векторов
Вычитание векторов
Умножение векторов
Деление вектора на число
Математически это выглядит следующим образом:

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

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

Результатом векторного умножения вектокторов является вектор,

перпедникулярныйобеим исходным векторам, то есть перпендикулярной плоскости, в которой лежат эти вектора, этот вектор по модулю равен синусу угла между  векторами умноженному на произведение модулей этих векторов.
Норма вектора
Нормой вектора называется модуль вектора.
Нормализованный вектор
Любой вектор, модуль которого равен 1 называется нормализованным на 1 вектором, или просто нормализованным вектором. Такой вектор имеет длину равную единице.

Единичные вектора понадобятся довольно часто. При умножении двух нормализованных векторов мы получаем непосредственно синус или косинус угла между ними, соответственно осуществив операцию арксинус или или арккосинус, мы можем найти угол между векторами.
Также в DirectX есть такое понятие как нормали к поверхности. Нормалью к поверхности называется единичный вектор, исходящий из определенной вершины поверхности и перпендикулярный этой поверхности. Если поверхность не плоская а вогнутая или выпуклая, то нормали будут подобны в первом случае ежику из векторов, расходящихся в разные стороны, а во втором случае, нормали (если продолжить эти вектора в бесконечность) будут сходится в некую фокусную точку. В любом случае, каждая нормаль будет по возможности перпендикулярна части поверхности, лежащей в непосредственной близости от вершины из которой исходит нормаль.
Символические обозначения векторов
В данных уроках мы иногда будем применять следующие обозначения для векторов. Обычный вектор мы будем обозначать символом стрелки:Нормализованный вектор, то есть вектор, длинна которого приведена к единице, мы будем обозначать символом, с двумя треугольными стрелками:Такие обозначения потребуется для сокращенного обозначения векторов при использовании их в формулах.
Преобразования матриц и операции с ними
Матрицей называется объект, состоящий из наборов чисел, сгруппированных в столбцы и строки. Матрицу можно рассматривать как обычное число, ведь одной переменной можно присвоить целую матрицу:

Математические операции с матрицами
Так как теперь матрица является своего рода числом, то с ним можно осуществлять различные математические опреции, а именно:
Сложение матриц
Вычитание матриц
Умножение матриц
Деление матрицы на число
Математически это выглядит следующим образом:

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

Соответственно, такое же неравенство выполняется для любого количества умножений матриц.
Единичная матрица
В матрицах особую роль играет еденичная матрица. Такая матрица имеет следующий вид:

Если умножить какую-то матрицу на единичную, то исходная матрица не изменится. Единичная матрица выполняет такую-жероль как и единица в обычных числах. Единичная матрица является нормализованной, то есть имеет модуль равный единице.
Модуль матрицы
Для матрицы, так же как и для вектора определена операция вычисления модуля. В какой-то мере матрицу можно представлять как псевдо-вектор в некотором четырехмерном пространстве. Хотя представления матрицы как вектор не для всех случаев верны, — так как в одних случаях матрица может соответствовать такому представлению, а в других – нет, — но можно вычислить длину такого вектора, в таком случае модуль матрицы будет представлять из себя длину псевдо-вектора, находящегося в четырехмерном пространстве.

Модуль матрицы обозначается также, как и модуль вектора.
Нормализованная матрица
Матрица может быть нормализована на 1, в таких случаях она будет называться нормализованной и её модуль будет равен 1.

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

Матрицы трансформаций
Трансформации бывают трех видов это:
Вращение
Масштабирование
Перемещение
Матрицы трансформаций пригодятся для помещения объекта в нужное место 3d пространства. Причем последовательность таких операций следующая: Сначала мы масштабируем объект, затем поворачиваем объект на нужный угол, потом перемещаем. Если последовательность будет другая, то результат будет непредсказуем, и где объект окажется в 3d пространстве, вы сможете определить сами, если, конечно найдете этот объект. Матрицы вращения бывают для нескольких осей. В основном нужна матрица для вращения вокруг вертикальной оси, она такая:

Матрица мастшабирования может быть симметричной и несимметричной. Для второго случая матрица выглядит так:

Несимметричное масштабирование означает, что мы вытягиваем объект в одну сторону больше чем в другие. Если вы хотите пропорционально масштабировать объект, то сделайте все компоненты матрицы x, y, z равными друг другу. Матрица для перемещения выглядит так:

Матрицы, используемые в DirectX
Для того, чтобы осуществить все необходимые преобразования с объектом, в DirectX существуют три матрицы. Первая из них располагает в нужном месте 3d пространства объект, остальные две используются для правильного размещения объекта с учетом расположения камеры и перспективы. Итак существуют три матрицы:
Мировая матрица
Матрица камеры
Матрица проекций
В системе DirectX для этих матриц сделаны следующие обозначения: matrix World, View, Projection. Мировая матрица соответственно равна трем матрицам трансформаций, рассмотренным в предыдущем разделе, умноженным друг на друга.

На рисунке представлено влияение матрицы трансформаций: объект был масштабирован и перемещен из начала координат в нужную точку 3d пространства.

Влияение матрицы камеры: все объекты были повернуты таким образом, как будто на них осущствляется взгляд из определенной точки, находящейся в том месте где находится камера.

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

Урок 4. Установка матриц трансформаций и камеры в DirectX11В этом уроке мы продолжим изучать Direct3D11. В этом уроке вы узнаете: какие переменные существуют для установки матриц камеры; какие есть средства для установки координат и параметров камеры; для чего существуют константы шейдера и как их устанавливать; а также для чего нужна матрица трансформаций и перспективная матрица.
Типы данных для хранения параметров камеры
Целью текущего урока является установка в нашу сцену камеры. Камера в играх используется везде. От правильной установки камеры зависит эффектность игры или приложения, кроме того вы можете установить несколько камер и переключаться между ними. Также камера может быть привязана к главному персонажу или транспортному средству.

Существует два вида представления камеры: ненаправленная,

И целевая, направленная камера. Для того, чтобы установить камеру, вообще говоря, нужно знать её координаты, направление, и угол обзора. Таким образом, для любой вашей камеры вы можете определить объект, хранящий эти данные:
class CameraDirectional
{
        XMFLOAT3 pos;
        XMFLOAT3 direction;
        float FOV;
};
Такая камера будет называться ненаправленной, так как не существует определенной точки в которую она смотрит. В качестве направления вы можете определить любой нормализованный вектор. Умножив вектор direction на одну или несколько матриц поворота вы сможете поворачивать камеру. Преимущество ненаправленной камеры в том, что для неё не обязательно иметь точку, в которую она смотрит – а постоянно следить за тем, какую конкретно точку направить камеру и к чему эту точку привязать – весьма проблематично.
Если вы точно или приблизительно знаете точку, в которую направлена камера, то вы можете определить другой тип камеры, это направленная камера:
class CameraTarget
{
        XMFLOAT3 pos;
        XMFLOAT3 target;
        float FOV;
}
Типы данных Direct3D
В предыдущем уроке мы рассмотрели различные математические операции и представления чисел. Давайте немного отвлечемся от установки камеры и рассмотрим, какие типы данных понадобятся в DirectX11:
Для хранения скалярных значений — чисел с плавающей точкой используется тип данных float
Для хранения трехкомпонентных векторов используется тип данных XMFLOAT3
Для хранения информации о цвете используется XMFLOAT4
Матрицы в DirectX описываются типом данных XMMATRIX
Также существуют и другие типы данных DirectX, однако они гораздо менее популярны.
Типы данных для хранения параметров камеры
Вернемся к представлению камеры. Итак, камера может быть ненаправленной и направленной. Однако если существует набор примитив, и в сцене и их надо повернуть под нужным углом по отношению к камере, то сама по себе координата камеры не очень то много даст. Вы можете убедиться в этом сами, попробовав задать трехмерный объект а затем повернуть его под нужным углом к камере. Вы убедитесь что понадобиться довольно много расчетов чтобы все вершины исходного объекта встали на экране так, как будто их видно из камеры. Однако есть способ проще осуществить все преобразования над вершинами. Предположим, мы имеем набор вершин:
struct Vertex
{
        XMFLOAT3 coord;
} Vertices[];
Чтобы осуществить нужные преобразования для камеры, достаточно лишь задать две матрицы:
XMMATRIX g_View;
XMMATRIX g_Projection;
И затем умножить координату каждой из вершин на обе матрицы:
FromViewVertices[].coord=Vertices[].coord*g_View*g_Projection;
Вы видите, что нужное преобразование осуществилось крайне просто. Таким образом, вы можете хранить камеру в любом формате, например в таком, который приведен выше:
class CameraTarget
{
        XMFLOAT3 pos;
        XMFLOAT3 target;
        float FOV;
}
Однако, перед тем как использовать камеру, вы должны преобразовать её в соответствующие матрицы камеры и проекций.
Установка матрицы камеры и проективной матрицы в Direct3D
Итак, продолжим. Перед тем как установить камеру типа CameraTarget вам потребуется преобразовать её координаты в матрицы DirectX, для этого в Direct3D11 существует следующая функция:
XMMATRIX XMMatrixLookAtLH(
  __in     const XMVECTOR pEye,
  __in     const XMVECTOR pAt,
  __in     const XMVECTOR pUp
);
Выходящими данными функции будет являться матрица. Входящие параметры определяют положение и направление камеры, что подходит для нашего объекта типа CameraTarget.
Таким образом, для того, чтобы преобразовать объект из CameraTarget в нужные нам матрицы нужно сделать следующее. Сначала заполним CameraTarget какими-либо данными:
XMVECTOR Eye = XMVectorSet( MyCamera.pos );
XMVECTOR At =  XMVectorSet( MyCamera.target );
XMVECTOR Up =  XMVectorSet( 0.0f, 1.0f, 0.0f, 0.0f );
Обратите внимание, что вместе с заданием координат камеры мы также задали параметр FOV. Это fieldofview, то есть угол обзора. Просто камера может узкоугольной и широкоугольной – по вашему выбору. Оказывается, в DirectX тоже можно менять линзы! Зададим в виде глобальных переменных матрицы
XMMATRIX                  g_World;
XMMATRIX                  g_View;
XMMATRIX                  g_Projection;
Теперь зададим нужные матрицы трансформаций, камеры и проекций:
g_View     = XMMatrixLookAtLH( Eye, At, Up );
g_World    = XMMATRIXIdentity();
g_Projection=XMMATRIXPerspectiveFovLH( MyCamera.FOV,1.6f.0f,0.1f,100.0f);
Обратите внимание, что хотя нам нужны были две матрицы мы установили все три требуемые матрицы DirectX. Это сделано для того, чтобы просто чем-то заполнить необходимую матрицу g_World. Мы заполнили g_World единичной матрицей, что означает отсутствие предварительных преобразований, таких как масштабирование и перемещение.
Установка матриц камеры, трансформаций и проективной матрицы в качестве констант шейдера
Вы отлично помните, что все отображение любых объектов осуществляется в Direct3D11 через шейдеры. Таким образом, перед тем как вывести наши объекты, хранящиеся в вершинных и индексных буферах необходимо установить константы для вершинногошейдера. Необходимыми константами являются матрицы камеры, проекций и трансформаций.
Любые константы шейдера в DirectX11 представляются в виде буфера. Таким образом, создав глобальный буфер для шейдеров можно затем просто устанавливать константы для шейдера, записывая в буфер новые значения констант и затем передавая в шейдер сразу весь буфер, содержащий все константы. Буфер может содержать как переменные для хранения матриц, также переменные для хранения векторов. Сначала объявим в приложении структуру, для хранения всех констант шейдера а также глобальный объект – константный буфер:
//------------------------------------------------------------------------------------
// Структуры
//------------------------------------------------------------------------------------
struct ConstantBuffer
{
        XMMATRIX mWorld;
        XMMATRIX mView;
        XMMATRIX mProjection;
};
//------------------------------------------------------------------------------------
// Глобальные переменные
//------------------------------------------------------------------------------------
ID3D11Buffer*           g_pConstantBuffer = NULL;
Обратите внимание что пока у нас объект имеет значение NULL, то есть объект еще не инициализирован. Также обратите внимание, что существует отдельно структура для хранения значений буфера ConstantBuffer и собственно объект константного буфера g_pConstantBuffer. В функции инициализации геометрии мы должны создать константный буфер, точно также, как мы создавали вершинный и индексный буферы:
// Создание буфера констант шейдера
bd.Usage = D3D11_USAGE_DEFAULT;
bd.ByteWidth = sizeof(ConstantBuffer);
bd.BindFlags = D3D11_BIND_CONSTANT_BUFFER;
bd.CPUAccessFlags = 0;
hr = g_pd3dDevice->CreateBuffer( &amp;bd, NULL, &amp;g_pConstantBuffer );
Таким образом вы можете создать любые глобальные переменные, существующие в шейдере, а не только матрицы камеры, проекций и трансформаций. Дополнив структуру ConstantBuffer соответствующей переменной, вы устанавливаете новую переменную для шейдера, что довольно удобно, так как не нужно отдельно устанавливать каждую переменную шейдера, достаточно заполнить структуру, храняющую все переменные, а потом скинуть эту структуру целиком, установив сразу все константы шейдера.
Итак, в процедуре Render установим глобальные переменные шейдера для наших матриц (вы помните, что раньше мы получили нужные матрицы, преобразовав их из объекта CameraTarget).
// Установка констант шейдера
ConstantBuffer cb;
сb.mWorld = XMMatrixTranspose( g_World);
cb.mView  = XMMatrixTranspose( g_View );
cb.mProjection = XMMatrixTranspose( g_Projection );
g_pImmediateContext->UpdateSubresource( g_pConstantBuffer, 0, NULL, &amp;cb, 0, 0 );
g_pImmediateContext->VSSetShader( g_pVertexShader, NULL, 0 );
g_pImmediateContext->PSSetShader( g_pPixelShader, NULL, 0 );
g_pImmediateContext->VSSetConstantBuffers( 0, 1, &amp;g_pConstantBuffer );
Выможетеспросить, зачемдваразаустанавливатьматрицы? Сначала мы установили матрицы при помощи кода
g_View     = XMMatrixLookAtLH( Eye, At, Up );
g_World    = XMMATRIXIdentity();
g_Projection=XMMATRIXPerspectiveFovLH( MyCamera.FOV,1.6f.0f,0.1f,100.0f);
А потом установили еще раз, при помощи установки переменных для шейдера (см. выше).
Ответ заключается в том, что на самом деле, матрицы устанавливались один раз. Просто второй раз, в процедуре Render мы передали значения, находящиеся в матрицах в шейдер. Если бы мы этого не сделали, то матрицы вместе с переменными для их хранения остались бы неиспользованными.
Полезные мелочи: матрица трансформаций
Итак, мы установили матрицы для камеры, однако невыясненным остался вопрос, зачем нужна переменная g_World, заполнявшееся единичной матрицей. Это матрица трансформаций. На самом деле этой переменной можно присваивать значение, отличное от единицы. И это полезная матрица, так как перед тем как отобразить объект мы его можем правильно расположить в пространстве.
Как правило, все объекты, загружаемые в игру, созданы в 3d редакторах так, чтобы их центр находился в начале координат. Существует два способа расположения центра объектов: прямо в центре объекта или внизу и в центре. Второй способ подходит для объектов, расположенных на ландшафте, так как достаточно указать высоту на которой будет находится объект и он поместится ровно так, чтобы коснуться своей нижней частью поверхности ландшафта.
При помощи матриц трансформаций g_World мы можем переместить объекты куда угодно перед тем как сделать их Render. При этом предварительно мы можем повернуть объект вокруг своей оси на нужный угол а также увеличить или уменьшить объект. Зачастую объекты «приходят» из 3d редактора в таком виде, что их надо подгонять по размеру. В редких случаях в редакторе можно точно угадать и подобрать нужную величину создаваемого объекта, так как системы измерений всегда могут отличаться.
Пример приложения
Теперь установим камеру непосредственно в приложении. Теперь мы же знакомы с технической частью вопроса, и приступим к реализации. Во первых, определим, какой результат хочется достигнуть. В нашем случае предположим, что в центре координат находится планета, вокруг которой вращается наша камера. Вместо круговой орбиты мы выберем эллиптическую, так как в этом случае мы сможем отследить перспективные преобразования, ведь камера будет в одних случаях приближаться к центру координат, и к объекту, а в других удалятся от него.

В качестве целевой точки для камеры будет выберем центр координат. Значение FOV для камеры установим по умолчанию.
Обзор приложения
Теперь остается скомпилировать и запустить приложение.

Исходный текст приложения к данному уроку вы можете найти тут. Данное приложение устанавливает камеру в соответствии с одной из точек эллиптической траектории и динамически изменяет положение камеры.
Заключение
В данном уроке вы изучили как устанавливать камеру, какие типы данных и объекты можно использовать для хранения информации о камере. Также вы узнали каким образом передавать в шейдер данные о положении камеры. Следующий урок мы посвятим несколько необычной теме – процедурной генерации 3d поверхности. То есть, столкнемся с конкретным примером практического применения Direct3D.

Урок 5. Процедурная генерация моделей для DirectX11В этом уроке вы узнаете: для чего нужна процедурная генерация; что нужно для того, чтобы правильно определить исходные данные для вершинного и индексного буфера; каким образом правильно задать вершинный и индексный буфер с произвольным количеством вершин; разберем процедурно генерируемый объект: сетку вершин.
Процедурная генерация
В дальнейшем, в восьмом уроке, мы разберем, как загружать внешнюю модель. Вообщем, необходимые навыки для этого вы уже имеете, так как знаете как создавать вершинный и индексный буфер, а 3d модель, сохраненная в виде файла как раз и представляет из себя дамп вершинных и индексных буферов редактируемой 3d модели.

Однако есть немало примеров того, когда использование внешних моделей для каких-то конкретных целей может только замедлить процесс разработки игры. Когда использование внешнего 3d редактора оказывается неэффективным, используется процедурная генерация. В число таких задач входит генерация ландшафта. На самом деле, разрабатывать ландшафт в 3d редакторе – очень утомительно. Начать хотя бы с того, что ландшафт очень большой, так что подготовить правильный каркас 3d ландшафта сложно.
Проще всего ландшафт сгенерировать на ходу. Для этого нужно иметь определенную карту высоты – то есть, картинку. Светлые места будут обозначать горы и холмы а более темные – более низкие участки ландшафта. Надо отметить, что кроме генерации ландшафта существует немало и других примеров процедурной генерации определенных объектов. Такая методика позволяет оживить саму игру и процесс разработки, внося в игру определенную динамику – вместоодин раз и навсегда определенных внешних мешей мы можем получить динамически меняющиеся 3d модели и поверхности. Нам же процедурная генерация понадобится в качестве закрепления навыков работы с вершинными и индексными буферами, так как это пожалуй, один из ключевых аспектов работы с DirectX.
Определение формата исходных данных
Плоскость для нашего ландшафта мы определим как сетку из точек. Количество вершин в сетке будет определять разрешение нашего ландшафта. Чем больше вершин в ландшафте, тем детальнее он будет. Однако не стоит увлекаться излишней детализацией, так как при большом количестве обрабатываемых вершин вершинный шейдер окажется перегружен. Так, мы имеем для сетки ландшафта размером 512×512 вершин 524000 отображаемых полигонов в сцене, что чрезмерно много, нас интересует количество около 40000 полигонов, с учетом моделей сцены.

Для нашего текущего примера мы создадим совсем небольшой тестовый ландшафт. Исходными данными для ландшафта будет количество вершин по горизонтали u и по вертикали v а также размер ландшафта size. Зададим размер ландшафта:
const u=8;
const v=8;
Определим формат вершинного буфера для ландшафта. Обратите внимание, что в вершинный буфер добавлена новая переменная normal. Эта переменная потребуется для правильного освещения ландшафта. О том, как работать с освещением и устанавливать его мы поговорим уже в следующем уроке. Итак, определим формат вершинного буфера для сетки ландшафта:
struct VERTEX{
      XMFLOAT3 pos;
      XMFLOAT4 color;
      XMFLOAT3 normal;
} vertices[u*v];
Теперь определим формат индексного буфера:
DWORD indices[IndicesCount];
Теперь посмотрим какой код потребуется для генерации сетки и заполнения вершинного буфера:
for (int i=0; i<u; i++)
for (int j=0; j<v; j++)
{
      float x=(float)i/(float)u-0.5f;
      float y=(float)j/(float)v-0.5f;
      vertices[j*u+i].pos  = XMFLOAT3(x,hmap[i][j],y)*7.5f;
      vertices[j*u+i].color= XMFLOAT4(1,1,1,1);
      vertices[j*u+i].normal=XMFLOAT3(0,1,0);
}
Сетка сгенерирована правильно. Однако обратите внимание, что хотя координаты x иz просчитаны правильно, высота элемента ландшафта взята из пока еще неопределенного массива hmap[][]. В этом массиве будет храниться карта высот для нашего ландшафта. Для индексного буфера мы должны сгенерировать номера вершин в следующем виде:
indexes[] = {
      0,1,9,            0,9,8
      1,2,10,     1,10,9
      2,3,11,     2,11,10
      ....
}
Таким образом, существует определенная несложная формула длянахождение каждой пары тройки чисел, определяющих индексы для использования в индексном буфере ландшафта.
for (int i=0; i<(u-1); i++)
for (int j=0; j<(v-1); j++)
{
      unsigned int indexa=j*(u-1)+i;
      unsigned int indexb=j*u+i;
      indices[indexa*6+0]=indexb;
      indices[indexa*6+1]=indexb+1+u;
      indices[indexa*6+2]=indexb+1;
 
      indices[indexa*6+3]=indexb;
      indices[indexa*6+4]=indexb+u;
      indices[indexa*6+5]=indexb+u+1;
}
Обзор приложения
В данном приложении мы рассмотрели генерацию меша из произвольного набора вершин и индексов. Исходный код текст данного приложения а также само приложение вы можете скачать тут. Сетка из вершин, генерируемая данным приложением иллюстрирует возможность генерации ландшафта используя имеющуюся карту высот.

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

Урок 6. Установка источников освещения DirectX11В этом уроке вы узнаете: какие бывают типы источников света; каким образом устанавливается освещение в Direct3D и почему для создания источников света обязательны шейдеры; каким образом вычисляется освещение с точки зрения математики; как правильно написать шейдер для освещения объектов; какие еще бывают типы освещения и как это связано с шейдерами.
Типы источников освещения
Освещение является важной частью вашего проекта. Например, в некоторых играх в очень темных локациях можно включить ручной фонарь. От него идет реалистичное узконаправленное пятно света, круглой формы, которое высвечивает все на своем пути. Подобный вид источника света типа прожектора несколько сложен для создания, но он демонстрирует какие богатые возможности представляетDirectX и шейдеры для создания реалистичного света. Более детально о различных типах источников освещения мы поговорим в конце данного урока. Сейчас рассмотрим то, как создать источник освещения в Direct3D11.
Создание источников света в DirectX11
Итак, вы уже догадываетесь, что свет в DirectX11 не обойдется без шейдеров. На самом деле, дело обстоит еще серьезней. В DirectX11 нельзя просто создать источник света указав его координаты и цвет и забыв об этом, поручив все расчеты света системе DirectX.
Но нас не смущает столь острая постановка вопроса, так как шейдеры не только не помешают нам создать освещение объекту но и помогут добиться в будущем некоторых эффектов.

Итак, разберем обычный случай освещения – единичный источник света. Если вы изучали рисование то вы знаете, что чтобы правильно закрасить объект, надо его часть, обращенную к свету сделать светлой, а ту, куда свет не попадает темной. При этом есть четкая граница где начинается тень. Давайте переведем эту проблему на язык математики. Имеется поверхность X произвольной формы и источник света с координатами L.

Необходимо определить угол между каждой из множества точек поверхности и лучем, проходящим от этой точки поверхности к источнику света. Чем меньше будет угол, тем светлее будет часть поверхности. На самом деле, это упрощенная постановка вопроса. Более точная постановка вопроса такая: для каждой точки поверхности необходимо определить угол между нормалью к поверхности и вектором, проходящим через центр источника света и точкой поверхности.
Определив нужный угол, мы можем задать функцию от этого угла, результатом функции будет цвет точки объекта. Чем угол меньше (острее), тем ярче цвет. При угле 90 градусов объект перестает быть освещен. Если угол больше 90 градусов, то это темные, теневые участки объекта. Решим задачу.  Обозначим каждую точку поверхности X символом n, нормаль к поверхности обозначим N. Искомая формула будет следующая:

Треугольными скобками здесь мы обозначим операцию нормализации, то есть приведения вектора к длинне 1. Прямыми скобками обозначается операция определения модуля вектора, то есть величины вектора. Таким образом, мы имеем два вектора – вектор нормали к поверхности N для каждой точки и второй вектор – вектор направления луча от точки поверхности к источнику света – вектор L-n. Кстати, исходные данные для операции L-n является координаты двух точек, результат же операции – не координаты, а вектор. На самом деле, в этой формуле даже arcsin не обязателен, так как мы всего-лишь вычисляем цвет, а не траекторию полета на луну. И дело не в точности вычислений, а в том что просто нет необходимости вычислять каждый раз arcsin, к тому-же это может и замедлить шейдер. Так что формула расчета интенсивности цвета упрощается:

Ну а что делать если источников света несколько? На самом деле, эта формула будет прекрасно работать и в этом случае. Просто просуммируем все данные:

То есть мы просто просуммируем данные, подставляя для каждого источника света новые координаты и затем поделим результат на количество источников света. А вот информация к размышлению. Мы все сделали правильно, однако чем острее угол a, тем меньше результат, а значит и темнее цвет… Стоп. Что-то здесь не так… Исходные данные правильны и формулы правильны, но результат чуть не такой, как хотелось бы. Нам ведь нужно точку в освещенных местах делать светлой, а не темной. Всем известно, что любые формулы хороши тогда, когда их можно еще больше упростить. И если нечего уже упрощать, значит формула имеет окончательный вид и её нужно высечь на глиняной дощечке. Сделаем так:

Но 1-a это как раз то, что нужно, ведь если а малое, то 1-а самое большое, а значит и цвет большой. Так что окончательно имеем:

и

Мы заменили операцию векторного умножения векторов, операцией скалярного умножения (crossproduct на dotproduct). К тому-же при скалярном умножении векторов получается не вектор а число, так что и модуль вектора вычислять не нужно. Вектор и так самостоятельно сократился, потеряв все данные о направлении, и оставив нужную нам длину. Получившаяся формула является хорошей информацией к размышлению, ведь одной небольшой формулой мы просчитали освещение объекта, освещаемого произвольным количеством источников. Неужели и правда всё так просто? Да, именно так просто. Теперь у нас есть данные для того, чтобы написать шейдер для освещения. Можно быть уверенным, что даже хорошо неподготвленный к шейдерам программист с легкостью поймет его с учетом текущего контекста беседы об освещении. Определим тип входящих данных вершинного буфера:
struct VS_INPUT
{
float4 Pos : POSITION;
float3 Normal : NORMAL;
float2 Tex: TEXCOORD;
}
Обратите внимание, что теперь в пиксельныйшейдер мы должны передавать дополнительую информацию для каждой вершины – нормаль к поверхности объекта. Теперь определим константы шейдера для хранения информации об источниках освещения, предположим у нас имеется максимально пять источников:
float4 LigthPosition[5];
float4 LightColor[5];
Теперь определим фрагмент вершинного шейдера, в котором мы будем рассчитывать освещенность объекта по формуле.
for (int i=0; i<5; i++)
output.Color+=dot(normalize(Normal),normalize((float3)vLightPosition[i]-(float3)Pos) )*vLightColor[i];
Если исходные данные нормалей представляют из себя нормализованные вектора, то есть если их модуль равняется единица то вместо normalize(Nomal) можно написать просто Normal.
Определение нормалей к поверхности
Остался нерешенным вопрос, каким образом в пиксельный шейдер передавать данные о нормали к поверхности. Точнее передавать их несложно, но откуда их брать? Дело в том, что данные нашей модели содержат лишь координаты вершин, нормалей там нет. Чтобы было более понятно посмотрим на рисунок части поверхности:

Наша задача для каждой вершины поверхности найти нормаль N. Видно, что каждая точка поверхности соединяется с шестью соседними. Эти соседние вершины соединяются треугольниками таким образом, что образуют сложную выпуклую структуру и провести к ней точно перпендикуляр несколько проблематично.
Для решения этой задачи зададимся вопросом: а насколько просто или сложно провести нормаль не к вершине, а к полигону, то есть к произвольному треугольнику поверхности. Решим эту задачу математически. Итак, имеется треугольник с набором вершин n1n2 и n3. Он образует плоскость. Необходимо определить перпендикуляр N к этой плоскости .

Решим задачу:

Таким образом, чтобы вычислить нормаль к полигону, нужно векторно умножить вектор n1n2 на вектор n1n3. Результатом такой операции получается вектор. Результирующий вектор перпендикулярен двум исходным. Чтобы окончательно решить задачу, остается только нормализовать нормали, так как полигоны могут быть различные по размеру и мы не хотим чтобы вектора нормалей тоже «скакали» по размеру. Приведенную выше формулу можно сразу перевести в код для нашей программы. Этот код мы будем использовать в приложении, а не в шейдере, так как в шейдере каждый раз рассчитывать нормали не имеет смысла. Мы можем один раз рассчитать их для нашей модели и затем использовать столько раз сколько понадобиться в шейдере.
XMFLOAT3 n[3];
XMFLOAT3 polynormal;
polynormal=XMVec3Cross(n[0]-n[1],n[0]-n[2]);
polynormal=XMVec3Normalize(polynormal);
Исходными данными является массив из трех точек n[]. Это координаты полигона. Выходящими данными является нормализованный вектор polynormal– искомая нормаль к полигону. Теперь вернемся к рисунку нашей поверхности X. Как вы думаете, будет ли нормаль из вершины поверхности смотреть в туже сторону что и нормали из центров полигонов? Ответ – безусловно. Кроме того, Если вычислить среднее направление всех соседних с вершиной нормалей к полигонам, мы и получим искомую нормаль к вершине.
Здесь и пригодится то, что мы предварительно нормализовали вектора нормалей – если бы они были разные по размеру, вычисление среднего вектора дало бы непредсказуемый результат.
Итак, окончательно имеем, что нормаль к каждой вершине равна среднему значению из нормалей всех прилегающих к вершине полигонов:

Хотя исходные данные для формулы несколько векторов N, вычисление среднего значения можно сделать также как и с обычными числами. Из урока 3 мы помним, что если вектор представить как одно число, то обнаружится много общего между числами и векторами. Таким образом, у векторов тоже определена операция вычисления среднего значения.  Остается отметить, что прилегающих к произвольной вершине полигонов не всегда ровно 6, это видно из рисунка поверхности X, если обратить внимание на вершины по краям фигуры. Если количество прилегающих полигонов меньше, то мы просто в формулу вместо 6 ставим количество прилегающих к вершине полигонов.

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

Неправда ли, если у ежика некоторые иголки растут в обратную сторону, это крайне неприятная ситуация для ежика. Чтобы избежать такой ситуации воспользуемся таким алгоритмом. Сначала маркируем все полигоны как непомеченные. После этого возьмем любой полигон с правильной нормалью. Теперь найдем соседний к нему непомеченный полигон. Сравним правильную нормаль с той, что у полигона. Если направление оказалось противоположным – просто перевернем вектор нормали. Теперь пометим полигон как обработанный, затем возьмем любой обработанный полигон и соседний к нему необработанный. Так до тех пор, пока не перевернем все нормали в одну сторону.
Приложения для установки источников освещения
Рассмотрим наше приложение, демонстрирующее установку освещения. В приложении используется модель тора, кольца, загружаемая из внешнего файла. Данная модель уже имеет набор нормалей, так что дополнительно их рассчитывать не нужно. В приложении устанавливается несколько источников освещения, координаты их динамически меняются и передаются в шейдер. Рассмотрим все вхождения в коде, касающиеся источников освещения. В начале установим переменную для буфера  констант шейдера.
ID3D11Buffer*           g_pConstantBuffer = NULL;
Далее мы создадим структуру ConstantBuffer. Эта структура будут хранить вместе с остальными константами шейдера, нужные нам массивы координат и цветов для нескольких источников освещения:
struct ConstantBuffer
{
      ...
      XMFLOAT4 vLightColor[5];
      XMFLOAT4 vLightPos[5];
};
Теперь всё готово, чтобы при каждом вызове функции Render устанавливать наши источники освещения. Сначала заполним их значениями. В качестве координат источников  света используем динамически меняющиеся орбитальные координаты:
XMFLOAT4 LightPos[5];
XMFLOAT4 LightColor[5];
LightPos[0]=XMFLOAT4(sin(orbit*f0)*r,cos(orbit*f0)*r,cos(orbit*f0)*r,0);
LightPos[1]=XMFLOAT4(sin(orbit*f1)*r,-sin(orbit*f1)*r,-cos(orbit*f1)*r,0);
LightPos[2]=XMFLOAT4(cos(orbit*f2)*r,sin(orbit*f2)*r,sin(orbit*f2)*r,0);
LightColor[0]=XMFLOAT4(1,1,0,1);
LightColor[1]=XMFLOAT4(0,0,1,1);
LightColor[2]=XMFLOAT4(0,1,1,1);
Всего установим три источника освещения. Координаты рассчитаны, теперь осталось предать значения координат в шейдер:
// Установка констант шейдера
ConstantBuffer cb;
...
memcpy(&amp;cb.vLightColor,&amp;LightColor,sizeof(LightColor));
memcpy(&amp;cb.vLightPos,&amp;LightPos,sizeof(LightPos));
g_pImmediateContext->UpdateSubresource( g_pConstantBuffer, 0, NULL, &amp;cb, 0, 0 );
Теперь перейдем от нашего приложения к коду шейдера. Рассмотрим вершинныйшейдер для нашего приложения.
//------------------------------------------------------------------------------------
// Constant Buffer Variables
//------------------------------------------------------------------------------------
cbuffer ConstantBuffer : register( b0 )
{
matrix World;
      matrix View;
      matrix Projection;
      float4 vLightColor[5];
      float4 vLightPos[5];
}
//------------------------------------------------------------------------------------
// Vertex Shader
//------------------------------------------------------------------------------------
VS_OUTPUT VS( float4 Pos : POSITION, float4 Normal: NORMAL, float2 Tex: TEXCOORD )
{
VS_OUTPUT output = (VS_OUTPUT)0;
output.Pos = mul( Pos, World );
output.Pos = mul( output.Pos, View );
output.Pos = mul( output.Pos, Projection );
output.Color=float4(0.0f,0.0f,0.0f,0.0f);
for (int i=0; i<3; i++)
output.Color+=dot(normalize(Normal),normalize((float3)vLightPos[i]-(float3)Pos) )*vLightColor[i];
return output;
}
Обратите внимание на строку, в которой в цикле рассчитывается переменная output.Color. Данная строка полностью соответствует формуле, рассмотренной ранее в данном уроке:

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

В качестве освещаемого объекта используется тор, так как эта геометрическая фигура, ввиду сложного расположения нормалей, не может быть освещена корректно, если метод и расчет освещения неверен. Но наша формула замечательно функционирует, и, таким образом, из этого урока мы точно узнали, где находится Кольцо и кто на самом деле является его Властелином; сегодня это шейдеры.
Различные типы источников освещения
В более простых сценах, например, внутри помещений определенных смоделированных зданий используются простые источники света, расположенные на потолке. Каркас 3d модели помещения изнутри освещается несколькими источниками света, которые активируются, как только то, что освещается ими попадает в область видимости камеры.
В зависимости от того, какой тип освещения используется, нужно настраивать соответственно сцену. Хотя в данном уроке нас интересовала техническая сторона вопроса, вообще существует несколько типов освещения. Перечислив их, мы дадим вам информацию для размышлений о том, как такие типы освещения можно создать и смоделировать, так как только некоторые из этих видов сейчас смоделированы с более менее правдоподобной реалистичностью, остальные же остаются темой для размышлений программистов и многочисленных экспериментов.
Итак, перечислим основные типы освещения, элементы экстерьера:
Рассеянное освещение от купола неба в туманную погоду
Ясное направленное освещение лучами солнца
Эффекты вечернего освещения (цвет солнца меняется)
Фонари и различные источники света возле и на зданиях. Источники света в виде окон.
Источники света, отражающееся в водной поверхности.
Перечислим основные типы освещения, для элементов интерьера:
Обычные фонари на потолках помещений.
Источники света от горящих и вспыхивающих предметов.
Различные фонари, светильники, световые табло.
Для элементов экстерьера характерны некоторые сложности создания тех или иных эффектов. Например, мягкий свет от купола неба совершенно необходим в сценах где есть камни и скалы, а также загромождения объектами, так как игра мягких теней в расщелинах и впадинах выглядит наиболее эффектно при мягких ненаправленных тенях. Однако рассеянный свет очень сложно смоделировать, так как лучи света идут со всех сторон и вдобавок к тому что объект нужно осветить, нужно добавить тени, и еще и сложные тени.
Более менее просто выглядит техническая сторона вопроса с направленным освещения солнца. Солнце – это единичный источник освещения. Один объект, одни координаты. Все лучи идут в одном направлении. Единственная сложность с солнцем – это добавить тени. Тени в играх — это также многогранная тема с точки зрения программирования. В некоторых случаях, если тени слишком сложно создать используется предварительно отрендеренные тени. К счастью, в DirectX очень просто комбинировать текстуры, так что если карта теней уже есть, то можно её просто поместить в отдельную текстуру и скомбинировать две текстуры – одна будет для текстуры ландшафта(или стен интерьера), а другая – для теней.
Для интерьера характерны другие технические сложности. Во первых, когда моделируется сцена интерьера первое что бросается в глаза моделерам и программистам – это наличие большого количества источников света. Например, модель здания может насчитывать 75 внутренних светильников. Если в шейдере создан массив на 12 светильников и параметры быстродействия не позволяют задействовать больше, то как эти 75 светильников поместить и просчитать в сцене?
Другая сложность с интерьером это то, что на самом деле если в сцене пусть даже 6 светильников поблизости от текущего места действия, то реалистично отобразить их тоже не просто. Дело в том, что с точки зрения физики, все эти источники света не представляются именно 6-ю источниками с шестью координатами и т.п. С точки зрения физики луч от каждого из источников сначала падает на одну стену интерьера, потом на другую и т.п., пока интенсивность многократных отражений и рассеиваний не достигнет нуля. То есть, в сцене присутствует множество теней, мягких и жестких, и вообщем игра света настолько сложная, что просчитать её в реальном времени можно лишь очень и очень условно. Таким образом, освещение в играх было и остается отличной платформой для разнообразных экспериментов.
Заключение
В данном уроке вы узнали, какие бывают типы источников света; каким образом устанавливается освещение в Direct3D и почему для создания источников света обязательны шейдеры; вы ознакомились с вычислением освещение с точки зрения математики и каким образом полученные формулы использовать в шейдере. Также какие еще бывают типы освещения и как это связано с шейдерами. В следующем уроке мы ознакомимся с тем, как устанавливать текстуры в Direct3D.

Урок 7. Текстуры в Direct3D11В этом уроке вы узнаете: какие бывают типы текстур в Direct3D; каким образом осуществляется загрузка текстур в Direct3D; каким образом устанавливаются текстуры для использования их в шейдере.
Типы данных текстур Direct3D
В основном, программистам игр и мультимедийных приложений необходимо загрузить текстуру к текущей модели, то есть как правило, если модель вообще использует текстуру, то для одного вершинного и индексного буфера используется одна текстура. Но в Direct3D существуют и другие типы данных текстур, краткий обзор которых мы проведем, прежде чем приступим к изучению того, как устанавливается текстура.

Типы текстур, используемы в Direct3D11:
Обычная 2D текстура формата R8G8B8
Текстура 2D с картой прозрачности R8G8B8A8
Кубическая 2D текстура, которая может использоваться для поверхности неба типа скайбокс, состоящая из 6 фрагментов 2D текстуры формата R8G8B8
Трехмерная 3D текстура формата R8G8B8
Пустая текстура, используемая в качестве рендер-таргета
Пустая текстура, используемая для программной генерации рисунка текстуры
Загрузка и установка текстур в Direct3D является несложным действием. В этом вы можете убедится, в разделе, расположенном чуть ниже. Для того, чтобы более детально понять почему загрузка текстур в DirectX11 осуществляется именно таким образом, вы можете ознакомится в разделе о расширенном методе загрузки текстур в конце данного урока.
Загрузка текстур в Direct3D
Итак, вы подготовили вершинный и индексный буфер, загрузив меш из файла или заполнив процедурно-сгенерированными данными. Для того, чтобы одновременно загрузить текстуру, создав при этом объект для хранения этой текстуры типа ID3D11ShaderResourceView необходимо воспользоваться следующим кодом загрузки текстуры:
// Загрузка текстуры
hr = D3DX11CreateShaderResourceViewFromFile( g_pd3dDevice, L"seafloor.dds", NULL, NULL, &amp;g_pTextureRV, NULL );
Обратите внимание, что в данном примере используется формат текстур типа .dds.Direct3D поддерживает большое разнообразие форматов, включая bmp, jpg иpng. Для текстур, использующих альфа канал подойдет формат png.
Создание объекта состояний семплера
Для того, чтобы шейдер мог правильно осуществлять выборку из текстур необходимо указать тип фильтрации, используемой шейдером при выборке из текстуры. Для того, чтобы иметь возможность указать такой тип, нужно предварительно создать объект типа ID3D11SamplerState. Такой объект нужно создать один раз, и он подойдет для всех загружаемых в приложении текстур.
// Создание объекта состояний семплера
D3D11_SAMPLER_DESC sampDesc;
ZeroMemory( &amp;sampDesc, sizeof(sampDesc) );
sampDesc.Filter = D3D11_FILTER_MIN_MAG_MIP_LINEAR;
sampDesc.AddressU = D3D11_TEXTURE_ADDRESS_WRAP;
sampDesc.AddressV = D3D11_TEXTURE_ADDRESS_WRAP;
sampDesc.AddressW = D3D11_TEXTURE_ADDRESS_WRAP;
sampDesc.ComparisonFunc = D3D11_COMPARISON_NEVER;
sampDesc.MinLOD = 0;
sampDesc.MaxLOD = D3D11_FLOAT32_MAX;
hr = g_pd3dDevice->CreateSamplerState( &amp;sampDesc, &amp;g_pSamplerLinear );
Теперь инициализация завершена. Все последующие действия с текстурами будут производится в функции Render.
Установка текстуры в качестве константы шейдера
Вы помните, как устанавливались матрицы в качестве констант шейдера. Также обстоит дело и с текстурами. Мы можем указать константу шейдера, и затем назначать ей новые значения (различные текстуры). В программе текстуры хранятся в виде экземпляров объекта типа ID3D11ShaderResourceView. Непосредственно в функции Render мы можем назначить этой константе шейдера новые значения точно также, как мы устанавливали константы шейдера для матриц:
g_pImmediateContext->PSSetShaderResources( 0, 1, &amp;g_pTextureRV );
Обратите внимание, что у нас может быть несколько переменных g_pTextureRV, например, массив из загруженных текстур вида g_pTextureRV[]. Таким образом, мы можем выбирать текстуру, которую устанавливаем из нескольких загруженных текстур:
g_pImmediateContext->PSSetShaderResources( 0, 1, &amp;g_pTextureRV[index] );
Для того, чтобы текстура правильно отображалась, сразу же за этой строкой, в которой мы устанавливаем шейдеру текстуры укажем состояние семплера:
g_pImmediateContext->PSSetSamplers( 0, 1, &amp;g_pSamplerLinear );
Все готово. Теперь установка текстур в функции Render успешно завершена.
Использование текстуры в шейдере
Сначала необходимо отметить, что для того, чтобы текстуры в шейдере функционировали, необходимо, чтобы во входящий формат вершинного шейдера была добавлена переменная формата , описывающая координаты текстуры. Но сначала определим константы шейдера для текстуры:
Texture2DtxDiffuse : register( t0 );
SamplerState samLinear : register( s0 );
Входящие данные пиксельногошейдера должны включать такую переменную Tex храняющую информацию о координатах текстуры.
structVS_OUTPUT
{
float4 Pos: SV_POSITION;
float4 Color: COLOR0;
float2 Tex : TEXCOORD0;
}
После того, как такая переменная установлена, вершинныйшейдер может передать эту переменную через себя в пиксельный шейдер:
VS_OUTPUT VS(float4 Pos:POSITION,float4 color: COLOR, float2 Tex: TEXCOORD)
{
    ...
    output.Tex=Tex;
}
Таким образом, пиксельный шейдер во входящей структуре PS_INPUT будет иметь переменную input.Tex типа float2.
float4 PS( PS_INPUT input) : SV_Target
{
return txDiffuse.Sample( samLinear, input.Tex ) * vMeshColor;
}
После того, как пиксельный шейдер получит данные о текстурных координатах, он может осуществить выборку цвета из текстуры, в соответствии с координатами. Для того используется функция шейдера
txDiffuse.Sample( samLinear, input.Tex )
Входящими данными является указание объекта из которого выполняется выборка txDiffuse, тип семплераsamLinear а также координаты текстуры input.Tex. Метод txDiffuse.Sample возращает значение типа float4, то есть цвет в формате R8G8B8A8.
Установка формата вершинного буфера, подходящего для использвания текстур
Для того, чтобы текстура могла использоваться в шейдере, каждая из вершин вершинного буфера должна содержать координаты текстуры. Обычно координаты текстуры обозначаются символами u и v, обозначающими координаты текстуры. Значения этих чисел лежат в диапазоне от 0 до 1. Таки образом, координаты текстуры образуют двухкомпонентный вектор.

Вы знаете, что трехмерные вектора в DirectX обозначаются как XMFLOAT3. Соответственно для двухмерных мы должны указать XMFLOAT2. Соответственно, если в нашем вершинном буфере содержатся только координаты о положении вершины, то мы должны добавить также и текстурные координаты.
structSimpleVertex
{
    XMFLOAT3 Pos; // Координата
    XMFLOAT2 Tex; // Координата текстуры
};
Также мы должны дополнить формат входящих данных вершинного буфера описанием типа вершинного буфера с учетом координат.
// Определение формата вершинного буфера
D3D11_INPUT_ELEMENT_DESC layout[] =
{
    { L"POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0,
        D3D11_INPUT_PER_VERTEX_DATA, 0 }, 
    { L"TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, 12,
        D3D11_INPUT_PER_VERTEX_DATA, 0 },
};
В данном коде каждая вершина вершинного буфера содержит лишь две переменные. Однако зачастую их больше. Более полный набор данных для вершины будет включать в себя также цвет вершины и информацию о нормали.
Обзор приложения
Теперь посмотрим, какой результат мы достигли при загрузке текстуры.

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

Урок 8. Загрузка мешей в DirectX11В этом уроке вы узнаете: для чего предназначены меши и какие бывают их форматы; каким образом представляется меш, загруженный в приложении; как загружать меши в Direct3D.
Форматы мешей
Загрузка внешних 3d моделей, — мешей — необходимая часть любого 3d приложения. В основном, каждое приложение, в частности, много игр поддерживают свои собственные форматы мешей. В основном, уникальность формата меша определяется уникальностью движка игры. Как правило, у каждого движка игры существуют свои собственный формат мешей. В нашем текущем уроке мы разберем два формата – формат .x и формат .obj.

Данные, экспортируемые при экспорте меша из 3d редактора
При экспорте меша как правило существуют определенные данные, обязательные для экспорта:
координаты вершин
индексы полигонов
нормали
материалы
Вы уже догадываетесь, что первое что экспортируется, то есть обязательные данные для экспорта – это индексные и вершинные буферы, то есть координаты и индексы вершин. Нормали используются для правильного освещения меша и могут быть также быть экспортированы из 3d редактора. Материалы используются тогда, когда меш состоит из нескольких частей, например с различными текстурами или фактурами. Например, одна из частей меша может быть металлической, в таком случае ей можно назначить отдельный материал и в последствии создать специальный шейдер для его отображения.
Правильный экспорт мешей
Существует пара простых правил для хорошего экспорта меша из редактора.
Всему мешу целиком назначайте в редакторе один материал и одну текстуру. Используйте для всего меша не больше одной текстуры и не больше одного материала.
Если функция загрузки мешей по каким-то причинам в вашей программе не подхватывает автоматическую загрузку текстуры, имя которой прописано в материале, или при экспорте меша вообще не экспортируется материал, загружайте ваши меши функцией наподобие loadmesh(“mesh.x”,”texture.jpg”); то есть, указывайте в функции загрузки меша отдельно имя текстуры. Или же загружайте отдельно, сначала меш, а затем текстуру к нему.
Если после загрузки меша меш отображается очень темным, то попробуйте осуществить либо в редакторе либо в функции загрузке меша операцию flipnormals, то есть обращение нормалей.
Если после загрузки меша в вашей программе этот меш отображается в виде маленькой точки или не виден совсем, значит подберите его масштаб при экспорте из редактора – либо увеличьте меш, чтобы он стал больше чем точка, либо уменьшите его, чтобы он поместился на экран и стал виден.
Вообще, почаще пользуйтесь такой функцией загрузкой меша, в которой вы можете указать отдельно имя файла меша и отдельно текстуру к нему, это поможет вам легко менять текстуру загружаемого меша.
Загрузка и меша из формата .x
Если вы хотите загрузить меш компактным кодом, то вы можете попробовать загрузить его с помощью кода, использующего DXUT.  Для загрузки мешей мы будем пользоваться форматом .obj. Так что, если вы хотите получить полный контроль над процессом загрузки и отображения меша, то перейдите к следующей части этого урока, в которой рассматривается загрузка меша из формата .obj.
Загрузка меша из формата .x в DirectX11 имеет некоторые не очень хорошие особенности. В целом, загрузка меша и его вывод в функции Render осуществляется несложно, однако, чтобы вся функция в целом заработала правильно, необходимо в вашей программе подключить библиотеку DXUT. Подключение этой библиотеки к вашему Direct3D11 приложению это довольно некорректный способ программирования, так как вы подключите огромное количество потенциально ненужного кода, кроме того, нет никаких гарантий что этот код будет правильно работать и функционировать вообще. Внимательно ознакомившись с примером из папки XformatLoading вы поймете, о чем здесь речь.
Внутри же DirectX11, в отличие от предыдущих версий не существует собственной функции загрузки мешей из формата .x. Разработчик DirectX, объясняет отсутствие такой функции в DirectX11 тем, что большинство компаний, занимающихся разработкой игр, имеют собственные форматы для хранения моделей. Таким образом сам формат .x поддерживается в текущих версиях DirectX весьма и весьма ограниченно, можно сказать, он не поддерживается совсем. Так как в DirectX11 формат .x не поддерживается, то в этом уроке по загрузке мешей мы будем использовать формат .obj.
Загрузка меша из формата .obj
Загрузив меш самостоятельно при помощи собственного загрузчика из формата .obj имеет много преимуществ перед загрузкой меша из формата .x. Вы можете получить полный контроль над процессом загрузки и рендера меша.
Ознакомимся с форматом obj и способом, существующим для его загрузки. Формат obj является по сути дампом вершинного и индексного буфера модели в редакторе. Формат obj текстовый, то есть вы можете любой файл .obj открыть текстовым редактором. Вот типичный пример файла формата obj:
#
mtllib ./box2.mtl
g
# object (null) to come ...
#
v  -1.968504 0.000000 1.968504
v  1.968504 0.000000 1.968504
v  -1.968504 0.000000 -1.968504
v  1.968504 0.000000 -1.968504
v  -1.968504 3.937008 1.968504
v  1.968504 3.937008 1.968504
v  -1.968504 3.937008 -1.968504
v  1.968504 3.937008 -1.968504
# 8 vertices
 
vt  0.000000 0.000000 0.000000
vt  1.000000 0.000000 0.000000
vt  0.000000 1.000000 0.000000
vt  1.000000 1.000000 0.000000
vt  0.000000 0.000000 0.000000
vt  1.000000 0.000000 0.000000
vt  0.000000 1.000000 0.000000
vt  1.000000 1.000000 0.000000
vt  0.000000 0.000000 0.000000
vt  1.000000 0.000000 0.000000
vt  0.000000 1.000000 0.000000
vt  1.000000 1.000000 0.000000
# 12 texture vertices
 
vn  0.000000 -2.000000 -0.000000
vn  0.000000 -1.000000 -0.000000
vn  0.000000 -1.000000 -0.000000
vn  0.000000 -2.000000 -0.000000
vn  0.000000 2.000000 -0.000000
vn  0.000000 1.000000 -0.000000
vn  0.000000 1.000000 -0.000000
vn  0.000000 2.000000 -0.000000
# 8 vertex normals
 
g (null)
f 1/10/1 3/12/3 4/11/4
f 4/11/4 2/9/2 1/10/1
f 5/9/5 6/10/6 8/12/8
f 8/12/8 7/11/7 5/9/5
f 1/5/1 2/6/2 6/8/6
f 6/8/6 5/7/5 1/5/1
f 2/1/2 4/2/4 8/4/8
f 8/4/8 6/3/6 2/1/2
f 4/5/4 3/6/3 7/8/7
f 7/8/7 8/7/8 4/5/4
f 3/1/3 1/2/1 5/4/5
f 5/4/5 7/3/7 3/1/3
# 12 faces
 
g
Разберем формат файла obj. Каждая строка начинается с идентификационного символа. Комментарии # не учитываются. Символы бывают следующие: v, vt, vn, f. Эти символы нам понадобятся больше всего. Символ v обозначает вершину. После этого символа в строке идут координаты этой вершины. Символ vt обозначает текстурные координаты. Так как текстурные координаты имеют только u и v координаты, то третья цифра для них будет всегда равна нулю. Символ vn обозначает нормаль.
При загрузке в программу каждой строке, начинающейся с идентификационного символа  v, vt или vn присваивается индекс, так, как если бы сам файл содержал индексы каждой строки, таким образом:
#
mtllib ./box2.mtl
g
# object (null) to come ...
#
v  -1.968504 0.000000 1.968504     [1]
v  1.968504 0.000000 1.968504[2]
v  -1.968504 0.000000 -1.968504    [3]
v  1.968504 0.000000 -1.968504     [4]
v  -1.968504 3.937008 1.968504     [5]
v  1.968504 3.937008 1.968504[6]
v  -1.968504 3.937008 -1.968504    [7]
v  1.968504 3.937008 -1.968504     [8]
# 8 vertices
 
vt  0.000000 0.000000 0.000000     [1]
vt  1.000000 0.000000 0.000000     [2]
vt  0.000000 1.000000 0.000000     [3]
vt  1.000000 1.000000 0.000000     [4]
vt  0.000000 0.000000 0.000000     [5]
vt  1.000000 0.000000 0.000000     [6]
vt  0.000000 1.000000 0.000000     [7]
vt  1.000000 1.000000 0.000000     [8]
vt  0.000000 0.000000 0.000000     [9]
vt  1.000000 0.000000 0.000000     [10]
vt  0.000000 1.000000 0.000000     [11]
vt  1.000000 1.000000 0.000000     [12]
# 12 texture vertices
 
vn  0.000000 -2.000000 -0.000000   [1]
vn  0.000000 -1.000000 -0.000000   [2]
vn  0.000000 -1.000000 -0.000000   [3]
vn  0.000000 -2.000000 -0.000000   [4]
vn  0.000000 2.000000 -0.000000    [5]
vn  0.000000 1.000000 -0.000000    [6]
vn  0.000000 1.000000 -0.000000    [7]
vn  0.000000 2.000000 -0.000000    [8]
# 8 vertex normals
 
g (null)
f 1/10/1 3/12/3 4/11/4
f 4/11/4 2/9/2 1/10/1
f 5/9/5 6/10/6 8/12/8
f 8/12/8 7/11/7 5/9/5
f 1/5/1 2/6/2 6/8/6
f 6/8/6 5/7/5 1/5/1
f 2/1/2 4/2/4 8/4/8
f 8/4/8 6/3/6 2/1/2
f 4/5/4 3/6/3 7/8/7
f 7/8/7 8/7/8 4/5/4
f 3/1/3 1/2/1 5/4/5
f 5/4/5 7/3/7 3/1/3
# 12 faces
 
g
Теперь разберемся что происходит со строками, начинающимися с символа f. Вы можете догадаться, что это индексы для индексного буфера. Но эти индексы имеют девять чисел, в то время как обычный индекс для индексного буфера содержит всего три числа, обозначающие номера трех вершин для одного полигона (треугольника).
Каждая тройка чисел в строке f формата obj обозначает следующие: номер вершины v, номер координаты текстуры vt, и номер нормали vn и так три раза. Также обратите внимание, что вершин(8) в нашем примере меньше чем текстурных координат к ним (12).  Конечно же для DirectX равенство количества вершин и текстурных координат должно соблюдаться. Таким образом, формат obj содержит только условно дамп индексного буфера. На самом деле, требуется определенный алгоритм, чтобы преобразовать данные из obj в настоящий вершинный и индексный буфер.
Этот алгоритм следующий: Возьмем первые три цифры из первой строки f. Это индексы 1/10/1. Соответственно, возьмем первую строку v, десятую строку vt и первую строку vn.
v  -1.968504 0.000000 1.968504     [1]
vt  1.000000 0.000000 0.000000     [10]
vn  0.000000 -2.000000 -0.000000   [1]
Теперь у нас образовался набор данных, как раз и соответствующих одной вершине для вершинного буфера, а именно: координата, текстурная координата и нормаль. Теперь образуем из этих данных структуру типа VERTEX:
structVERTEX {
      XMFLOAT3 pos;
      XMFLOAT3 texcoord;
      XMFLOAT3 normal;
}
VERTEX vertices[1024];
У нас существует временный массив для вершинного буфера vertices. Добавим данные вершины 1/10/1 в этот массив. Номер этой вершины массива vertices будет равен 0, так как она первая.
vertices[0]= ... v, vt, vn of 1/10/1 vertex ...
Затем возьмем следующим элемент из строки f. Это будет 3/12/3.
vertices[1]= ... v, vt, vn of 3/12/3 vertex ...
Таким же образом возьмем следующим элемент из строкиf. Это будет 4/11/4.
vertices[2]= ... v, vt, vn of 4/11/4 vertex ...
Одновременно с увеличением индекса новых элементов вершинного буфера – это числа 0, 1, 2 записываем эти индексы в индексный буфер.
indices[count++]=0;
indices[count++]=1;
indices[count++]=2;
Таким образом, при каждой новой тройке чисел из строки f мы должны собрать новый элемент типа VERTEX и добавить этот элемент в вершинный буфер, если там нет точно такого же элемента. Если во временном вершинном буфере уже содержится элемент с точно такими же координатами, текстурными координатами и нормалью мы не должны его добавлять заново а просто мы должны определить номер этого элемента. Таким образом, индексный буфер будет содержать данные наподобие тех, что на этом листинге:
indices[count++]=0[new];
indices[count++]=1[new];
indices[count++]=2[new];
indices[count++]=3[new];
indices[count++]=2[copy];
indices[count++]=4[new];
Код загрузки данных из формата obj
Алгоритм, описанный выше реализован программно. Вы можете его найти в исходном тексте meshobj.cpp, данный код содержится в методе MeshFromObj::LoadMeshFromObj. Обратите внимание, что перед тем как осуществить проход формата obj создается три временных буфера для хранения массивов v, vt и vn соответственно.
Данный код существует в виде метода объекта MeshFromObj и является частью модуляmeshobj.h. Для того, чтобы использовать объект MeshFromObj и при помощи него загружать данные из внешних мешей необходимо подключить к вашему Direct3D приложению модуль meshobj.h.
Загрузка и отображение меша при помощи модуля meshobj
Модуль meshobj.h имеет функцию загрузки и рендера меша, загружаемого из файла obj. Сначала подключите этот модуль к вашему Direct3D приложению:
#include "meshobj.h"
Вся функциональность модуля осуществляется через базовый объект типа MeshFromObj. Создав несколько экземпляров этого объекта вы можете загрузить несколько различных мешей. Сначала объявите переменную для хранения экземпляра объекта:
MeshFromObj*      g_MeshFromObj=NULL;
Затем создайте экземпляр объекта и загрузите меш:
g_MeshFromObj=new MeshFromObj(g_pd3dDevice, g_pImmediateContext, ”mesh.obj”);
Рендер меша вы можете осуществить при помощи метода MeshFromObj::Draw
g_MeshFromObj.Draw();
Метод MeshFromObj::Draw самостоятельно устанавливает вершинные и индексные буферы Direct3D– устройства. Таким образом, для рендера меша нужно просто вызвать Draw. Чтобы поместить меш в нужное место сцены, конечно, установите также матрицы трансформаций перед вызовом метода.
Обзор приложения
Загрузка внешних моделей является очень важной и частой операцией в Direct3D. Как всегда, код примера приложения и само приложение этого урока вы можете найти тут.

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

Урок 9. Шейдеры в Direct3D11В этом уроке вы узнаете: какая архитектура шейдеров; какую роль выполняют шейдеры в DirectX приложении; что такое язык HLSL и для чего он предназначен, ознакомитесь со базовыми принципами программирования HLSL.
Шейдеры
Из предыдущих уроков вы уже знакомы с шейдерами и применяли их практически. В данном уроке мы не будем использовать их как дополнение к приложению, а взглянем на сами шейдеры и на их код.

Сейчас шейдеры являются ключевым аспектом DirectX приложения. Фактически, трудно что-нибудь сделать без их использования. Вся графика игр, многочисленные спецэффекты, и тот реализм, который на сегодняшний день достигла графическая часть игр – это всё получилось благодаря использованию шейдеров. Взглянем лишь на некоторые примеры использования шейдеров:
Обычное применение: матрицы камеры и освещениями в вершинномшейдере, текстуры и определение текущего цвета пикселя в пиксельном шейдере
Композиции из нескольких текстур, использования глубоких текстур, для тонкой детализации ландшафта и моделей
Эффекты освещения и теней
Системы частиц – туман, задымление а также различные виды вспышек
На самом деле, данный список можно продолжать, кажется бесконечно. Существует слишком много примеров применения шейдеров, чтобы даже их просто перечислить. Запустите любую новую игру и существующий список применения шейдеров может быть пополнен несколькими, а то и десятками новых применений шейдеров.
Особенности компиляции и загрузки шейдера
Хотя шейдер является очень низкоуровневой структурой – он должен выполняться очень быстро, но вы можете его писать на языке, подобном C++, этот язык называется HLSL. Это потому, что шейдер на ходу компилируется в машинный код видеоускорителя. Кроме того, не нужен специальный компилятор. Язык HLSL является стандартным и его компиляция осуществляется на ходу любой 3d системой, например DirectX или OpenGL. Таким образом, вы просто указываете на исходный файл формата .fx и он автоматически загружается, компилируется и выполняется непосредственно в вашем 3d приложении.
Язык HLSL
Взглянем на пример шейдера с точки зрения программирования. Посмотрим на типичный код шейдера:
//------------------------------------------------------------------------------------
// Constant Buffer Variables
//------------------------------------------------------------------------------------
Texture2D txDiffuse : register( t0 );
SamplerState samLinear : register( s0 );
//------------------------------------------------------------------------------------
cbuffer ConstantBuffer: register( b0 )
{
    matrix World;
    matrix View;
    matrix Projection;
    float4 vMeshColor;
 
};
//------------------------------------------------------------------------------------
struct VS_INPUT
{
    float4 Pos : POSITION;
    float2 Tex : TEXCOORD0;
};
 
struct PS_INPUT
{
    float4 Pos : SV_POSITION;
    float2 Tex : TEXCOORD0;
};
//------------------------------------------------------------------------------------
// Vertex Shader
//------------------------------------------------------------------------------------
PS_INPUT VS( VS_INPUT input )
{
    PS_INPUT output = (PS_INPUT)0;
    output.Pos = mul( input.Pos, World );
    output.Pos = mul( output.Pos, View );
    output.Pos = mul( output.Pos, Projection );
    output.Tex = input.Tex;
 
    return output;
}
//------------------------------------------------------------------------------------
// Pixel Shader
//------------------------------------------------------------------------------------
float4 PS( PS_INPUT input) : SV_Target
{
    return txDiffuse.Sample( samLinear, input.Tex ) * vMeshColor;
}
Вы видите, что весь код шейдера может быть разбит на три ключевых раздела. Это: объявления глобальных переменных – констант шейдера представленных в виде константного буфера, код вершинного шейдера, и наконец,  код пиксельного шейдера. В дальнейших разделах мы поэтапно рассмотрим структуру языка HLSL.
Типы данных HLSL
В каждом языке существуют различные встроенные типы данных. В языке HLSL типы данных носят некоторый математический оттенок, то есть в нем много типов данных наподобие векторов и матриц. Итак, таблица типов данных HLSL:
Тип данных Примечание
Buffer Массив
Scalar Одно компонентная скалярная величина
Vector,Matrix Несколько компонентный вектор или матрица
Sampler, Texture Встроенные типы данных – сэмплер, текстура
Struct, User Defined Пользовательский тип данных
Рассмотрим эти типы данных. Тип данных Scalar определяет скалярную величину и может быть одним из следующих:
bool
int
uint
half
float
double
Тип данных Vector и Matrix обозначается добавлением количества компонент к типу данных scalar. Например, Float4 определяет четырехкомпонентный вектор. Для матрицы, например Float4x4 добавляется NxN, то есть указывается размеренность матрицы. Пример определения вектора и матрицы:
float3 fVector = { 0.2f, 0.3f, 0.4f };
double3x3 dMatrix;  
float2x2 fMatrix = { 0.0f, 0.1, // row 1
                     2.1f, 2.2f // row 2  };
Типы Sampler используется для описания типа фильтрации текстуры. Тип Texture для указания на используемую текстуру. Примеры определений для этих типов данных, тип данных Sampler и Texture:
Texture2D txDiffuse : register( t0 );
SamplerState samLinear : register( s0 );
ТипданныхTexture:
float4 Color=txDiffuse.Sample( samLinear, input.Tex ) * vMeshColor;
Операции ветвления кода в HLSL
В большинстве случаев код HLSL выполняется построчно, то есть одна строка, или инструкция потом следующая и так далее. Но в некоторых случаях, можно добавлять циклы и условные переходы. Обратите внимание, что так как HLSL должен работать быстро, то ветвления в коде HLSL должны быть скорее исключением, чем практикой.
Операция ветвления Примечание
break Операция прерывания цикла
continue Продолжение цикла
discard Специфическое ветвление для пиксельногошейдера
do Ветвление
for Цикл
if Условный переход
switch Операция выбора
while Ветвление
Операции ветвления в HLSL имеют тот же синтаксис, что и в языке C++.
Определения функций и блоки в HLSL
Остальная структура языка может быть сведена к следующей таблице:
Элемент Примечание
Функции Определение функций
Операции ветвления См. предыдущий раздел
Блоки Блок определяется символами {}
Выражение return Операция возвращения из блока функции
Выражения Любое математическое выражение а также оператор присваивания.
Операторы Пример символов операторов: +,-,*,/,=,+=,++,<<,>>,==,!=,>=,!
Встроенные функции Intrinsic Functions
Определение функций имеет некоторые характерные черты в HLSL. Об этом далее.
Типы данных, для вершинного и пиксельного шейдера
Для вершинного и пиксельного шейдера существуют определенные типы данных, присущие им в качестве входящих данных и выходящих функций. Итак, вершинныйшейдер принимает в себя входящие данные, которые должны полностью соответствовать формату вершинного буфера. Пример формата входящих данных для вершинного шейдера:
struct VS_INPUT
{
    float3 Pos          : POSITION;         //position
    float3 Norm         : NORMAL;           //normal
    float2 Tex          : TEXCOORD0;        //texture coordinate
};
Выходящими данные вершинныйшейдер возвращает операцией return. Они должны содержать структуру, хранящую координаты а также любые другие произвольные данные.
Выходящие данные вершинногошейдера являются входящими данными пиксельного шейдера:
struct PS_INPUT
{
    float4 Pos : SV_POSITION;
    float3 Norm : TEXCOORD0;
    float2 Tex : TEXCOORD1;
};
Пример входящего формата пиксельного шейдера эта структура PS_INPUT. Пиксельный шейдер операцией return может возвратить единственную переменную формата float4, определяющую цвет точки.
float4 color= g_txDiffuse.Sample( ...
return color;
Встроенные функции HLSL
Встроенные функции HLSL позволяют осуществлять многие операции, которые невозможно выполнить обычным кодом HLSL. Встроенные функции обозначаются IntrinsicFuncions. В число этих функций входят математические функции, операции с матрицами и векторами, операции выборки из текстур а также много другое. Вот пример некоторых из встроенных функций HLSL:
abs
sin
cos
dot
cross
fmod
log
mul
max
min
round
sqrt
tan
tex1d
tex2d
texCUBE
В этом отрывке кода из вершинного шейдера
output.Pos = mul( output.Pos, View );
output.Pos = mul( output.Pos, Projection );
output.Norm = mul( input.Norm, World );
легко найти места, в которых вызывается встроенная функция шейдера mul, соответствующая операции умножения матриц.
Заключение
В этом уроке вы познакомились непосредственно с тем, что представляет из себяшейдер. HLSL не так сложен как многие другие языки, так как программы на нем – точнее фрагменты кода для пиксельного и вершинного шейдера – получаются очень короткими.

Урок 10. Различные шейдеры DirectX11В предыдущем уроке мы произвели обзор языка HLSL. В этом уроке мы разберем пару конкретных примеров шейдеров.
Шейдеры
В этом уроке, как и в предыдущем мы не будем разбирать код приложения. Мы рассмотрим код самих шейдеров. Единственный случай, когда мы обратимся к коду приложения – это когда мы будем назначать различные шейдеры различным объектам. В приложении два вида шейдеров и вы должны представлять, как назначать их объектам, наподобие материалов.

Шейдер для эффекта bump
Эффект bump широко используется  для детализации поверхности модели текстурой выпуклостей, еще называемой normalmap. Шейдер для этого эффекта на рисунке имеет шар под номером два. Рассмотрим код пиксельного шейдера этого эффекта:
//------------------------------------------------------------------------------------
// Pixel Shader
//------------------------------------------------------------------------------------
float4 PS1( VS_OUTPUT input ) : SV_Target
{
     input.Tex.y*=4.0f;
      float3 bump=txDiffuse[1].Sample(samLinear,input.Tex)*2.0f - 1.0f;
      float shine=dot((float3)input.L0,normalize(bump)); shine+=dot((float3)input.L1,normalize(bump));
      float4 ambient=txDiffuse[0].Sample(samLinear,input.Tex)*input.Diffuse;
      return ambient*(shine*0.5f)+pow(shine,4) * input.Diffuse*0.1f+ambient*0.5f+float4(0.1f,0,0.1f,0);
}
Во первых, на что стоит обратить внимание здесь, это то, что в шейдере используется две текстуры – одна это обычная текстура поверхности txDiffuse[1] и другая текстура для образования выпуклой фактуры поверхности txDiffuse[0]. Эта текстура имеет определенный формат наложения цветов, расположенный таким образом, чтобы цвет из этой текстуры мог использоваться как вектор. Таким образом, основная операция освещения для эффекта bump это вычисление параметра shine, который получается умножением двух векторов – первый вектор input.L0 — это вектор от текущей вершины до точки камеры, — а второй вектор берется из цвета текстуры. При этом используется два источника освещения, таким образом передается два вектора для них L0 и L1.
Вершинный шейдер для двух эффектов из этого урока один и тот же. Надо сказать, что создание общего вершинного шейдера для нескольких пиксельных шейдеров это распространенная и весьма полезная практика. Для того, чтобы установить разные шейдеры различным объектам необходимо перед вызовом рендера объектов установить вершинный и пиксельный шейдеры.
Шейдер для эффекта металлической поверхности
Следующий шейдер имеет более интересный код а также более эффектный вид, хотя первое и второе напрямую не связано. Во первых, нужно сказать пару слов о том, чем металлическая поверхность отличается от обычной. Для металлических предметов характерен блеск. Это происходит потому, что кристаллическая решетка металлов плотная и таким образом, отражает свет и не пропускает его через себя. Таким образом, металлическая поверхность является зеркалом. Единственное отличие – это то, что металл может быть шероховатым, и чем больше шероховатость, тем меньше металл блестит. Однако зеркальные свойства сохраняются, только в очень рассеянном виде.
Таким образом, главное что отличает металлический эффект освещения от обычного, диффузного – это то, что правило – чем прямее угол падения луча на предмет, тем он светлее – для металлов не сохраняется. Здесь действует другое правило – участок металлического предмета светлее всего в тех местах, где луч, проведенный из точки зрения наблюдателя к поверхности металла, отражаясь попадет на источник освещения. И наоборот, если такой луч попадет не на источник света, а куда-нибудь еще, то значит в этих местах металл темный.
Зная координаты источника освещения, координаты текущей точки модели, нормаль к этой точке, и позицию наблюдателя, можно вычислить коэффициент блеска металла в текущей точке.
//------------------------------------------------------------------------------------
// Vertex Shader
//------------------------------------------------------------------------------------
VS_OUTPUT VS( float4 Pos : POSITION, float4 Normal : NORMAL, float2 Tex: TEXCOORD)
{
VS_OUTPUT output = (VS_OUTPUT)0;
output.Pos = mul( Pos, World );
output.Pos = mul( output.Pos, View );
output.Pos = mul( output.Pos, Projection );
      output.Color=float4(0.0f,0.0f,0.0f,0);
      float4 lviewvec=normalize(vView-Pos);
float4 lviewvec=mul(View,float4(0,0,1,0));    
 
      for (int i=0; i<2; i++)
      {
      float3 lpos=(float3)vLightPosition[i];
      float a=dot(normalize(Normal),normalize(lpos-(float3)Pos));
      float b=dot(normalize(Normal),(float3)lviewvec);
      float c=1.0f/(1.0f+(a-b)*(a-b)*5); c=pow(c,5);
      output.Color+=vLightColor[i]*c;
      }
output.N=Normal;
      output.L=lviewvec;
}
Разберем код вершинного шейдера для эффекта металла. Переменная, хранящая интенсивность цвета здесь – output.Color. Эта переменная наполняется в два прохода: каждый проход для одного из источников освещения. Каждый источник освещения может иметь свой цвет. Основная операция для освещения здесь – вычисление коэффициентов a и b. Коэффициент a обозначает угол между нормалью и источником освещения. Коэффициент b обозначает угол между нормалью и точкой наблюдателя. Если углы совпадают, и эти два числа равны значит угол падения равен углу отражения, и в такой точке металл должен обладать ярким свечением. Если коэффициенты различаются, значит углы разные и источник света в данной точке и под данными углами либо не отражается совсем, либо отражается лишь слегка. Теперь разберем пиксельныйшейдер для нашего эффекта:
//------------------------------------------------------------------------------------
// Pixel Shader 0
//------------------------------------------------------------------------------------
float4 PS0( VS_OUTPUT input ) : SV_Target
{
      float3 refl=reflect((float3)input.L,(float3)input.N);
      return input.Color+txCube.Sample(samLinear,refl)*0.8f+float4(0.1f,0,0.1f,0);
}
Так как эффект блеска источников освещения уже рассчитан, то, вообщем, мы могли бы просто сделать returninput.Color; Однако металл может отражать и что-то еще кроме источников света, поэтому в этом примере добавлена карта отражения, представленная в виде кубической текстуры. Для того, чтобы осуществить выборку из кубической текстуры, — в отличие от двухмерной текстуры, в которой нужно для выборки двухкомпонентный вектор, — в кубической текстуре нужно для выборки обычный трехкомпонентный вектор. Причем этот вектор должен быть рассчитан встроенной функцией reflect. В общем, наш пиксельный шейдер для металлической поверхности получился компактным и прозрачным. Это произошло благодаря тому, что большая часть эффекта металлической поверхности была рассчитана в вершинномшейдере.
Использование нескольких шейдеров в приложении
Как правило, в играх используется сразу несколько шейдеров. Некоторые группы мешей имеют один и тот же шейдер, другие группы мешей имеют другой шейдер и так далее. Всё зависит от количества требуемых эффектов и материалов. Перегружать обилием шейдеров и эффектов ваше приложение тоже не следует, правильной практикой будет подчеркнуть шейдером те части сцены, где они необходимы.
// Установка шейдеров
g_pImmediateContext->VSSetShader( g_pVertexShader, NULL, 0 );
g_pImmediateContext->PSSetShader( g_pPixelShader, NULL, 0 );
В нашем примере назначение того или иного шейдера происходит при рендере мешей.
Обзор приложения
Приложение для данного урока и исходные тексты приложения и шейдеров вы можете скачать тут. Как всегда, вы можете найти в папке с исходными кодами и само скомпилированное приложение.

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

Приложенные файлы

  • docx 10905423
    Размер файла: 1 MB Загрузок: 0

Добавить комментарий