Урок DirectX 11 на C++ №2: «Рисуем треугольник»

Итак, здравствуйте. Вы – один из тех, кого не напугал первый урок и кто готов двигаться дальше. Давайте на этот раз обойдемся без философских вступлений, тем более что работы у нас очень много.

Будем рисовать. Почему именно треугольник? Просто потому, что это базовая модель для трехмерной графики – примитив. Примитивами называют фигуры, из которых все и создается. Кроме треугольников можно рисовать линиями и точками, но кому нужен герой или монстр,  состоящие из линий и живущие в мире точек? Поэтому обычно, когда говорят о примитивах, имеют в виду треугольники. Представьте, если удачно совместить два треугольника, получится квадрат. А если совместить 6 квадратов, можно создать уже куб. Но придавать нашим творениям объем будем немного позже.

1. Геометрия.

Мы выяснили, для чего нам нужны треугольники. Теперь давайте подумаем, как можно нарисовать такой восхитительный объект. Тут ничего сложного: достаточно задать координаты трех точек и дать понять Direct3D, что рисовать мы хотим именно треугольник а не точки и не линии). Выглядит это примерно так:

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

 struct SimpleVertex

{

    XMFLOAT3 Pos;

};

Нагоняющее ужас XMFLOAT3 – это всего лишь структура из трех значений типа float, задающих координаты вершины по осям X, Y и Z. Координата Z не понадобится, потому что наш треугольник пока что плоский, как лепешка.

Разобравшись с форматом вершин, мы создадим буфер. Как несложно догадаться, мы создадим массив из трех вершин. Потом загрузим эти данные в буфер формата DirectX ID3D11Buffer.

Проблема в том, что DirectX не имеет представления о формате наших вершин. Как я уже говорил, для каждой вершины можно хранить много разной информации, следовательно, мы должно рассказать DirectX о содержимом структуры SimpleVertex. Здесь нам поможет функция CreateInputLayout(…) (Создать шаблон ввода), находящаяся в интерфейсе Device (устройство, ID3D11Device). Она создаст объект интерфейса Input Layout (шаблон ввода, ID3D11InputLayout), который мы подключим к устройству рисования:


g_pImmediateContext->IASetInputLayout( g_pVertexLayout );

Мы рассмотрим этот процесс подробно уже при написании программы.

2. Шейдеры.

Собственно говоря, в шейдерах нет ничего сложного. С другой стороны, именно шейдеры я так и не смог одолеть много лет назад, когда изучал DirectX8. Однако тогда они были лишь подающим надежды нововведением, теперь же шейдеры стали неотъемлемой частью трехмерного программирования. Шейдер – это всего лишь подпрограмма (функция), используемая для определения/изменения параметров любого объекта или изображения. Проще всего сравнить его с человечком, стоящим у конвейера. Скажем, по ленте Конвейера Отрисовки Треугольника несется наша вершина №1. Где-то ближе к концу лента замедляется, шейдер вносит небольшое изменение в вершину и отправляет ее дальше, прямо в функцию, рисующую вершину на экране. Такой же путь проделают вершины 2 и 3. В этом примере мы рассмотрели работу вершинных шейдеров. Существуют еще геометрические и пиксельные. Геометрические обрабатывают целый примитив (треугольник), а пиксельные, как легко догадаться, каждый пиксел. Например, для покрытия треугольника текстурой мы в пиксельном шейдере могли бы вычислить цвет текстуры в точке с координатами пикселя и вернуть этот цвет.

Как я уже  сказал, шейдеры – это всего лишь функции. Вершинный шейдер возвращает объект, описывающий вершину (у нас – SimpleVertex), а пиксельный шейдер возвращает цвет. Но пишутся шейдеры не на C++, а на похожем языке, который называется HLSL. Шейдеры будут храниться в отдельном файле Urok2.fx, который не входит в программу. Мы загрузим и скомпилируем этот файл динамически в процессе загрузки программы (не бойтесь, DirectX выполнит основную работу за нас).

Вот пример пиксельного шейдера, который в нашей программе закрасит треугольник в желтый цвет:

 float4 PS( float4 Pos : SV_POSITION ) : SV_Target

{

    // Возвращаем желтый цвет, непрозрачный (альфа = 1)

    return float4( 1.0f, 1.0f, 0.0f, 1.0f );

}

Функция рисования для каждой точки треугольника вызовет шейдер PS(…) (аббревиатура от Pixel Shader) и закрасит ее полученным цветом.

3. Программа.

Я думаю, будет проще изучить сложные места уже по ходу написания программы.

Общая схема такая: Создаем окно a Создаем устройства DX a Загружаем шейдеры, создаем буфер вершин и формат вершин (объект Input Layout) a Готово! В цикле сообщений рисуем треугольник из нашего буфера.
Давайте опять создадим пустой объект и добавим в него файл Urok2.cpp и файл иконки с идентификатором IDI_ICON1. Сразу добавляем библиотеки в командную строку компоновщика. Значительная часть кода не будет отличаться от предыдущего урока, так что можно взять за основу старый проект.

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

// Урок 2. Рисование треугольника. Основан на примере из DX SDK (c) Microsoft Corp.

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

#include <windows.h>

#include <d3d11.h>

#include <d3dx11.h>

#include <d3dcompiler.h>   // Добавились новые заголовки

#include <xnamath.h>

#include "resource.h"

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

// Структуры

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

struct SimpleVertex

{

    XMFLOAT3 Pos;

};

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

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

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

HINSTANCE               g_hInst = NULL;

HWND                    g_hWnd = NULL;

D3D_DRIVER_TYPE         g_driverType = D3D_DRIVER_TYPE_NULL;

D3D_FEATURE_LEVEL       g_featureLevel = D3D_FEATURE_LEVEL_11_0;

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

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

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

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

ID3D11VertexShader*     g_pVertexShader = NULL;             // Вершинный шейдер

ID3D11PixelShader*      g_pPixelShader = NULL;        // Пиксельный шейдер

ID3D11InputLayout*      g_pVertexLayout = NULL;             // Описание формата вершин

ID3D11Buffer*         g_pVertexBuffer = NULL;         // Буфер вершин

Для динамического создания шейдеров добавился заголовок d3dcompiler.h. С четырьмя новыми объектами проблем возникнуть не должно, мы их уже обсудили.

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

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

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

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

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

HRESULT InitGeometry();    // Инициализация шаблона ввода и буфера вершин

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

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

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

Как видите, добавилась функция InitGeometry(). В ней мы загрузим шейдеры и создадим буфер вершин. Эта функция останется с нами до конца.

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

// Точка входа в программу. Инициализация всех объектов и вход в цикл сообщений.

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

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

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

{

    UNREFERENCED_PARAMETER( hPrevInstance );

    UNREFERENCED_PARAMETER( lpCmdLine );

       // Создание окна приложения

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

        return 0;

       // Создание объектов DirectX

    if( FAILED( InitDevice() ) )

    {

        CleanupDevice();

        return 0;

    }

       // Создание шейдеров и буфера вершин

    if( FAILED( InitGeometry() ) )

    {

        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;

}

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

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

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

HRESULT InitWindow( HINSTANCE hInstance, int nCmdShow )

{

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

    WNDCLASSEX wcex;

    wcex.cbSize = sizeof( WNDCLASSEX );

    wcex.style = CS_HREDRAW | CS_VREDRAW;

    wcex.lpfnWndProc = WndProc;

    wcex.cbClsExtra = 0;

    wcex.cbWndExtra = 0;

    wcex.hInstance = hInstance;

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

    wcex.hCursor = LoadCursor( NULL, IDC_ARROW );

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

    wcex.lpszMenuName = NULL;

    wcex.lpszClassName = L"Urok2WindowClass";

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

    if( !RegisterClassEx( &wcex ) )

        return E_FAIL;

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

    g_hInst = hInstance;

    RECT rc = { 0, 0, 400, 300 };

    AdjustWindowRect( &rc, WS_OVERLAPPEDWINDOW, FALSE );

       g_hWnd = CreateWindow( L"Urok2WindowClass", L"Урок 2: Рисование треугольника",

                           WS_OVERLAPPEDWINDOW,

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

                           NULL );

    if( !g_hWnd )

        return E_FAIL;

    ShowWindow( g_hWnd, nCmdShow );

    return S_OK;

}

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

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

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

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

{

    PAINTSTRUCT ps;

    HDC hdc;

    switch( message )

    {

        case WM_PAINT:

            hdc = BeginPaint( hWnd, &ps );

            EndPaint( hWnd, &ps );

            break;

        case WM_DESTROY:

            PostQuitMessage( 0 );

            break;

        default:

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

    }

    return 0;

}

В этом большом куске кода ничего не изменилось, только добавился вызов уже упомянутой функции InitGeometry() из wWinMain(…) сразу после вызова функции инициализации устройств Direct3D.

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

// Вспомогательная функция для компиляции шейдеров в D3DX11

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

HRESULT CompileShaderFromFile( WCHAR* szFileName, LPCSTR szEntryPoint, LPCSTR szShaderModel, ID3DBlob** ppBlobOut )

{

    HRESULT hr = S_OK;

    DWORD dwShaderFlags = D3DCOMPILE_ENABLE_STRICTNESS;

    ID3DBlob* pErrorBlob;

    hr = D3DX11CompileFromFile( szFileName, NULL, NULL, szEntryPoint, szShaderModel,

        dwShaderFlags, 0, NULL, ppBlobOut, &pErrorBlob, NULL );

    if( FAILED(hr) )

    {

        if( pErrorBlob != NULL )

            OutputDebugStringA( (char*)pErrorBlob->GetBufferPointer() );

        if( pErrorBlob ) pErrorBlob->Release();

        return hr;

    }

    if( pErrorBlob ) pErrorBlob->Release();

    return S_OK;

}

Эта функция загружает и компилирует шейдер из файла «на лету». Мы на ней не будем останавливаться, хотя здесь все очень просто. По сути, всю работу выполняет вызов D3DX11CompileFromFile(…), а остальной код – обработка возможных ошибок. Поскольку придется загружать два шейдера, микрософтовцы решили вынести эту обработку в отдельную функцию.

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

// Создание устройства Direct3D (D3D Device), связующей цепи (Swap Chain) и

// контекста устройства (Immediate Context).

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

HRESULT InitDevice()

{

    HRESULT hr = S_OK;

    RECT rc;

    GetClientRect( g_hWnd, &rc );

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

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

    UINT createDeviceFlags = 0;

#ifdef _DEBUG

    createDeviceFlags |= D3D11_CREATE_DEVICE_DEBUG;

#endif

    D3D_DRIVER_TYPE driverTypes[] =

    {

        D3D_DRIVER_TYPE_HARDWARE,

        D3D_DRIVER_TYPE_WARP,

        D3D_DRIVER_TYPE_REFERENCE,

    };

    UINT numDriverTypes = ARRAYSIZE( driverTypes );

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

    D3D_FEATURE_LEVEL featureLevels[] =

    {

        D3D_FEATURE_LEVEL_11_0,

        D3D_FEATURE_LEVEL_10_1,

        D3D_FEATURE_LEVEL_10_0,

    };

       UINT numFeatureLevels = ARRAYSIZE( featureLevels );

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

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

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

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

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

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

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

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

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

    sd.BufferDesc.RefreshRate.Denominator = 1;

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

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

    sd.SampleDesc.Count = 1;

    sd.SampleDesc.Quality = 0;

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

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

    {

        g_driverType = driverTypes[driverTypeIndex];

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

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

            break;

    }

    if (FAILED(hr)) return hr;

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

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

    // Извлекаем описание заднего буфера

    ID3D11Texture2D* pBackBuffer = NULL;

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

    if (FAILED(hr)) return hr;

    // По полученному описанию создаем поверхность рисования

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

    pBackBuffer->Release();

    if (FAILED(hr)) return hr;

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

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

    // Установки вьюпорта (масштаб и система координат). В предыдущих версиях он создавался

    // автоматически, если не был задан явно.

    D3D11_VIEWPORT vp;

    vp.Width = (FLOAT)width;

    vp.Height = (FLOAT)height;

    vp.MinDepth = 0.0f;

    vp.MaxDepth = 1.0f;

    vp.TopLeftX = 0;

    vp.TopLeftY = 0;

    g_pImmediateContext->RSSetViewports( 1, &vp );

    return S_OK;

}

Второй урок функция инициализации устройств, как видите, перенесла без хирургического вмешательства.

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

// Создание буфера вершин, шейдеров (shaders) и описания формата вершин (input layout)

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

HRESULT InitGeometry()

{

    HRESULT hr = S_OK;

       // Компиляция вершинного шейдера из файла

    ID3DBlob* pVSBlob = NULL; // Вспомогательный объект - просто место в оперативной памяти

    hr = CompileShaderFromFile( L"urok2.fx", "VS", "vs_4_0", &pVSBlob );

    if (FAILED(hr))

    {

        MessageBox( NULL, L"Невозможно скомпилировать файл FX. Пожалуйста, запустите данную программу из папки, содержащей файл FX.", L"Ошибка", MB_OK );

        return hr;

    }

А вот и что-то новенькое. Давайте разбираться. Интерфейс ID3DBlob – это просто место в памяти. У него есть два метода-члена: один возвращает адрес в оперативной памяти, другой – размер занятой памяти. Функция CompileShaderFromFile(…) загружает из файла urok02.fx (мы его потом создадим) функцию под названием “VS” (это функция нашего вершинного шейдера, vertex shader) в объект ID3DBlob.  “vs_4_0” – это версия шейдеров. Например, шейдеры первой версии писались на ассемблере. Поехали дальше.

 // Создание вершинного шейдера

    hr = g_pd3dDevice->CreateVertexShader( pVSBlob->GetBufferPointer(), pVSBlob->GetBufferSize(), NULL, &g_pVertexShader );

    if( FAILED( hr ) )

    {

        pVSBlob->Release();

        return hr;

    }

Надеюсь, не надо напоминать, что все объекты Direct3D создаются при помощи девайса? Тут все очень просто. В случае ошибки не забываем освободить память. Шейдер создан, теперь опишем формат наших вершин.

 // Определение шаблона вершин

    D3D11_INPUT_ELEMENT_DESC layout[] =

    {

        { "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 },

    /* семантическое имя, семантический индекс, размер, входящий слот (0-15), адрес начала данных в буфере вершин, класс входящего слота (не важно), InstanceDataStepRate (не важно) */

    };

    UINT numElements = ARRAYSIZE( layout );

Как уже говорилось, вершины могут иметь различные параметры - координаты в пространстве, нормаль, цвет, координаты текстуры. Шаблон вершин указывает, какие именно параметры содержат вершины, которые мы собираемся использовать. Наши вершины (SimpleVertex) содержат только информацию о координатах в пространстве. Поэтому и описывающая формат вершин структура состоит из одной строки. В этой строке нас интересуют следующие параметры:

1)      “POSITION” – семантическое имя. Понятно, мы описываем позицию вершины, и теперь DirectX будет это знать.

2)     DXGI_FORMAT_R32G32B32_FLOAT – формат, или размер. Обратите внимание на часть R32G32B32: мы имеем три значения по 32 бита (float), или в сумме 12 байт. Мне сложно понять образ мыслей микрософтовских программистов, решивших: «Как здорово! Позиция в пространстве задается тремя координатами, а цвет – тремя компонентами. Какое совпадение! Давайте-ка сэкономим на константах, пусть позиция тоже задается буквами R, G, B!» И эти же люди придумывают такие константы, названия которых не вмещаются в ширину экрана!
3)     Смещение в байтах (пятый параметр, равный нулю). У нас смещение равно нулю, потому что SimpleVertex::Pos стоит в самом начале SimpleVertex.

// Создание шаблона вершин

    hr = g_pd3dDevice->CreateInputLayout( layout, numElements, pVSBlob->GetBufferPointer(),

                                          pVSBlob->GetBufferSize(), &g_pVertexLayout );

    pVSBlob->Release();

    if (FAILED(hr)) return hr;

    // Подключение шаблона вершин

    g_pImmediateContext->IASetInputLayout( g_pVertexLayout );

В объекте pVSBlob, напомню, все еще находится скомпилированный вершинный шейдер. Опять все примитивно: создаем объект шаблона ввода (Input Layout) и подключаем его к устройству рисования. Теперь небольшое deja vu: повторим недавние действия (кроме создания шаблона ввода) для пиксельного шейдера.

// Компиляция пиксельного шейдера из файла

    ID3DBlob* pPSBlob = NULL;

    hr = CompileShaderFromFile( L"Urok2.fx", "PS", "ps_4_0", &pPSBlob );

    if( FAILED( hr ) )

    {

        MessageBox( NULL, L"Невозможно скомпилировать файл FX. Пожалуйста, запустите данную программу из папки, содержащей файл FX.", L"Ошибка", MB_OK );

        return hr;

    }

    // Создание пиксельного шейдера

    hr = g_pd3dDevice->CreatePixelShader( pPSBlob->GetBufferPointer(), pPSBlob->GetBufferSize(), NULL, &g_pPixelShader );

    pPSBlob->Release();

    if (FAILED(hr)) return hr;

 

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

 // Создание буфера вершин (три вершины треугольника)

    SimpleVertex vertices[3];

    vertices[0].Pos.x =  0.0f;  vertices[0].Pos.y =  0.5f;  vertices[0].Pos.z = 0.5f;

    vertices[1].Pos.x =  0.5f;  vertices[1].Pos.y = -0.5f;  vertices[1].Pos.z = 0.5f;

    vertices[2].Pos.x = -0.5f;  vertices[2].Pos.y = -0.5f;  vertices[2].Pos.z = 0.5f;

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

D3D11_BUFFER_DESC bd;  // Структура, описывающая создаваемый буфер

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

    bd.Usage = D3D11_USAGE_DEFAULT;

    bd.ByteWidth = sizeof( SimpleVertex ) * 3; // размер буфера = размер одной вершины * 3

    bd.BindFlags = D3D11_BIND_VERTEX_BUFFER;          // тип буфера - буфер вершин

    bd.CPUAccessFlags = 0;

    D3D11_SUBRESOURCE_DATA InitData; // Структура, содержащая данные буфера

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

    InitData.pSysMem = vertices;               // указатель на наши 3 вершины

    // Вызов метода g_pd3dDevice создаст объект буфера вершин ID3D11Buffer

    hr = g_pd3dDevice->CreateBuffer( &bd, &InitData, &g_pVertexBuffer );

    if (FAILED(hr)) return hr;

Старый приятель – описывающая буфер структура. Буферы ведь бывают разные, а вот интерфейс для всех одинаковый – ID3D11Buffer. К счастью, нас выручает параметр BindFlags, задающий тип буфера. Еще тут используется вспомогательная структура-указатель на массив вершин. С ее помощью мы создаем объект буфера. И последнее действие с буфером вершин: установим его как источник вершин для рисования в устройстве рисования (Device Context).

// Установка буфера вершин:

    UINT stride = sizeof( SimpleVertex );

    UINT offset = 0;

    g_pImmediateContext->IASetVertexBuffers( 0, 1, &g_pVertexBuffer, &stride, &offset );

Следующая строка нас надолго задержит, так что приготовьтесь. Можете сесть поудобнее или, наоборот, прогуляться. Я даже картинку для этого случая приготовил.

// Установка способа отрисовки вершин в буфере

    g_pImmediateContext->IASetPrimitiveTopology( D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST );

Здесь мы устанавливаем топологию примитивов как Triangle List – список треугольников. Если говорить по-человечески, мы устанавливаем способ (последовательность) рисования примитивов по вершинам из буфера. Так какие у нас здесь есть возможности?
Во-первых, давайте вспомним, что рисовать можно не только треугольники, но еще точки и линии. Поэтому перечисление D3D11_PRIMITIVE_TOPOLOGY содержит члены (…)_POINTLIST и (…)_LINELIST. Но это не главное. Помните, что я говорил про квадраты, космический корабль и бесконечно большое ведро? Давайте представим, что у нас есть 7 точек (A-G) в пространстве, изображенные ниже:

Мы очень хотим нарисовать по этим точкам 5 треугольников. Список треугольников – это такой способ рисования, при котором отдельно задаются три вершины каждого треугольника. То есть нам придется создать массив SimpleVertex v[15] = { A, B, C, B, C, D, C, D, E, D, E, F, E, F, G }. Много избыточной информации, правда? Но у этого способа рисования есть преимущества, которые позже станут понятны.

Существует второй распространенный способ рисования – это лента треугольников (D3D11_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP). При рисовании ленты алгоритм каждый раз использует три последние точки для рисования треугольника. Массив вершин будет выглядеть так: SimpleVertex v[7] = { A, B, C, D, E, F, G }.

Функция Direct3D, рисующая треугольники из буфера, будет рассматривать его так: v[0]-v[1]-v[2] – первый треугольник, v[1]-v[2]-v[3] – второй треугольник и т. д.

В нашем примере всего один треугольник, так что замена способа рисования со списка на ленту ничего не изменит.

Урок получился очень длинным, а рассказать надо еще много. Поэтому давайте двинемся дальше.

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

// Освобождение всех созданных объектов

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

void CleanupDevice()

{

    // Сначала отключим контекст устройства

    if( g_pImmediateContext ) g_pImmediateContext->ClearState();

    // Потом удалим объекты

    if( g_pVertexBuffer ) g_pVertexBuffer->Release();

    if( g_pVertexLayout ) g_pVertexLayout->Release();

    if( g_pVertexShader ) g_pVertexShader->Release();

    if( g_pPixelShader ) g_pPixelShader->Release();

    if( g_pRenderTargetView ) g_pRenderTargetView->Release();

    if( g_pSwapChain ) g_pSwapChain->Release();

    if( g_pImmediateContext ) g_pImmediateContext->Release();

    if( g_pd3dDevice ) g_pd3dDevice->Release();

}

Закрываем функцию InitGeometry(). Дальше следует реализация функции, удаляющей объекты DirectX. В коде приложения осталась только функция рисования:

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

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

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

void Render()

{

    // Очистить задний буфер

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

    g_pImmediateContext->ClearRenderTargetView( g_pRenderTargetView, ClearColor );

    // Подключить к устройству рисования шейдеры

    g_pImmediateContext->VSSetShader( g_pVertexShader, NULL, 0 );

    g_pImmediateContext->PSSetShader( g_pPixelShader, NULL, 0 );

    // Нарисовать три вершины

    g_pImmediateContext->Draw( 3, 0 );

    // Вывести в передний буфер (на экран) информацию, нарисованную в заднем буфере.

    g_pSwapChain->Present( 0, 0 );

}

Здесь появилось три новые строчки. Первые две подключают шейдеры к объекту рисования, вторая рисует три вершины из буфера вершин (который мы подключили  к объекту g_pImmediateContext еще в середине функции InitGeometry()).

4. И снова шейдеры.

Ура! Теперь быстренько создаем в блокноте файл urok2.fx в папке нашего проекта. Здесь будет храниться код шейдеров. Для удобства давайте подключим этот файл (Проект a Добавить существующий элемент). Теперь редактировать шейдеры можно будет прямо из проекта в редакторе C++, хотя они никакого отношения к программе не имеют. Программа отлично скомпилируется и сейчас, но выдаст сообщение об ошибке при загрузке шейдеров в функции InitGeometry().
Скажу еще раз, шейдеры – это всего лишь программки, обрабатывающие вершины и пикселы. Вот так выглядит наш вершинный шейдер:

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

// Вершинный шейдер

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

float4 VS( float4 Pos : POSITION ) : SV_POSITION

{

    // Оставляем координаты точки без изменений

    return Pos;

}

Не забывайте, это не C++, хоть и очень похоже. Язык называется HLSL (высокоуровневый язык шейдеров). Функция вершинного шейдера получает координаты вершины и передает их дальше без изменений. Здесь float4, как легко догадаться, – структура из 4 переменных типа float (x, y, z и a). Последняя переменная вообще не нужна. Семантическое имя “POSITION”, отделенное от названия параметра двоеточием, дает понять, что речь идет о координатах вершины (ведь вершина может содержать и другие параметры). Именно POSITION мы указали при создании шаблона ввода. Семантическое имя “SV_POSITION” показывает, что функция шейдера возвращает готовую позицию вершины.

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

// Пиксельный шейдер

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

float4 PS( float4 Pos : SV_POSITION ) : SV_Target

{

    // Возвращаем желтый цвет, непрозрачный (альфа == 1, альфа-канал не включен).

    return float4( 1.0f, 1.0f, 0.0f, 1.0f );

}

Пиксельный шейдер возвращает цвет в формате RGBA.

Сохраняем файл, компилируем проект и запускаем.

Чтобы вы хорошо поняли, как работают шейдеры, проведем два эксперимента.

1. В вершинный шейдер добавим первую строку:

Pos.x *= 0.5;

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

2. Немного модифицируем код пиксельного шейдера.

 float fLimiter = 500.0f;

    float dist = Pos.x*Pos.x + Pos.y*Pos.y;

    dist = (dist % fLimiter) / fLimiter;

    return float4( dist, 0.0f, dist, 1.0f );

Пусть цвет пикселя зависит от его координат!

И вот что получилось у меня:

3. Что, если вместо треугольника мы захотим нарисовать квадрат? Нам понадобится изменить некоторые строки. Во-первых, необходимо добавить вершину в массив с координатами вершин:

SimpleVertex vertices[] = {

        XMFLOAT3( -0.5f,  0.5f,  0.5f ), /*  v[0]      v[1]  */

        XMFLOAT3(  0.5f,  0.5f,  0.5f ), /*                  */

        XMFLOAT3( -0.5f, -0.5f,  0.5f ), /*                  */

        XMFLOAT3(  0.5f, -0.5f,  0.5f ) /*  v[2]      v[3]  */

    };

Во-вторых, изменится строчка, задающая размер буфера вершин перед его созданием:


bd.ByteWidth = sizeof( SimpleVertex ) * 4; // размер буфера

Как вы, наверное, догадались, у нас изменился способ вывода примитивов:


g_pImmediateContext->IASetPrimitiveTopology( D3D11_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP );

И в функции рендеринга теперь надо рисовать не три точки, а четыре:


g_pImmediateContext->Draw( 4, 0 );

Вот и все. В качестве упражнения можете нарисовать круг при помощи функций sinf и cosf (подсказка: круг – это обычный многоугольник с очень большим количеством углов).

Тяжело? Могу обрадовать: просто этот урок был очень тяжелым. В следующий раз мы переселим треугольник из двухмерного пространства в 3D. А может даже заменим плоский треугольник на что-то более интересное.

До встречи!

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

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