Урок DirectX11 на C++ №3: » Матрицы и 3D-трансформации»


Здравствуй-здравствуй, поредевшая аудитория. Кажется, в прошлый раз я обещал более простой урок? Вы, наверное, уже поняли, что DirectX – достаточно низкоуровневая библиотека. Это обеспечивает гибкость и скорость работы с графикой, но усложняет процесс программирования. В этом уроке вы еще раз почувствуете это. Так что, как вы наверняка догадались, простой урок откладывается на следующий раз.

Чтобы немного ободрить, приоткрою тайну последнего, седьмого урока: мы создадим почти настоящий мир с домиками, шоссе с автомобилями и камерой от первого лица. Здорово звучит, а? Однако вернемся к теме.

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

1. Координаты в трехмерном пространстве.

Чтобы создать объект в трехмерном пространстве, нам понадобятся система координат и система отображения. Это понятно: в реальном мире любую точку можно задать координатами по трем осям. Эти три оси можно называть по-разному и по-разному располагать, главное – не в одной плоскости, но в DirectX используется правосторонняя картезианская система координат. Это означает, что все оси перпендикулярны, и ось X направлена вправо, ось Y влево, а ось Z прямо на нас (за спину).

рис. 2. пространство координат объекта

Давайте представим трехмерный объект, скажем, кубик размерами 2х2х2 единицы. Центр куба (рис. 2) совпадает с координатой (0, 0, 0). У нас может быть 3 куба или 152, и у каждого будет своя точка отсчета, совпадающая с центром куба. Все художники, создающие модели для игр, располагают их в центре координат, потому что так их проще трансформировать. Такое пространство называется пространством координат объекта.

Но когда речь идет не об отдельном объекте, а о целой сцене, существует пространство координат мира. В этом случае точка отсчета принимается в одном месте, и все объекты располагаются в некоторых координатах относительно этой точки. Например, куб 1 можно сдвинуть на 5 единиц вправо от нас в координату (5, 0, 0), а куб №2 задвинуть подальше и вверх в (0, 2, -10).

С этим все просто. Однако в трехмерной графике также выделяется пространство вида (система координат вида), или камеры. Оно немного напоминает пространство мира. Но здесь все рассчитывается относительно виртуальной камеры. Для пространства вида задается позиция камеры, точка, в которую «смотрит» камера, и направление вертикали (понятно, ведь камеру можно наклонить, как голову). Наконец, есть система координат проекции (пространство проекции). В этой системе координат все видимые объекты уже трансформируются в двухмерное пространство и получают координаты X и Y от -1 до +1 и координату Z от 0 до 1. Вот так Микрософт иллюстрирует связь системы координат мира и вида (камеры):

Выходит, при рисовании трехмерных объектов нам надо преобразовывать все координаты в следующей последовательности: [система объекта à] система мира à система вида à система проекции. В сегодняшнем примере система координат объекта будет совпадать с системой координат мира, поэтому первое преобразование не понадобится.

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

 // Трансформация позиции вершины при помощи умножения на матрицу

    output.Pos = mul( Pos, MatWorld ); // сначала в пространство мира

    output.Pos = mul( output.Pos, MatView ); // затем в пространство вида

    output.Pos = mul( output.Pos, MatProjection ); // в проекционное пространство

Преобразования матрицы объекта в матрицу мира мы рассмотрим уже в следующем уроке. А так, например задается матрица вида:

// Инициализация матрицы вида (камеры)

    XMVECTOR Eye = XMVectorSet( 0.0f, 1.0f, -5.0f, 0.0f );  // Откуда смотрим

    XMVECTOR At  = XMVectorSet( 0.0f, 1.0f,  0.0f, 0.0f );  // Куда смотрим

    XMVECTOR Up  = XMVectorSet( 0.0f, 1.0f,  0.0f, 0.0f );  // Направление верха

    XMMATRIX g_MatrixView = XMMatrixLookAtLH( Eye, At, Up );

Работу выполняет встроенная функция XMMatrixLookAtLH(…).

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

2. Опять шейдеры.

Несколькими строками выше я упомянул, что трансформации координат вершин проводятся в шейдере. Это и логично, ведь шейдер как раз обработает все вершины перед выводом. Но ведь шейдер – это совсем другая программа, помните? Я хочу, чтобы эта мысль вбуравилась вам в глаза и прочно закопалась в мозг. Значит, необходим способ передавать матрицы из программы в шейдер во время выполнения. Для этого придумали константные буферы. Боже, опять буферы! К тому же, скажу скажу, никакие они не константные на самом деле, а очень даже изменяемые.
Сейчас покажу, о чем мы говорим. Вот так выглядит объявление буфера в шейдере:

// Константные буферы

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

cbuffer ConstantBuffer : register( b0 )

{

    matrix MatWorld; // Тут могут быть любые данные, понимаете? Не обязательно матрицы.

    matrix MatView;

    matrix MatProjection;

}

Затем переменные из буфера можно спокойно использовать в функции шейдера, как показано в предыдущей вырезке из выдуманного файла shadres.fx. Опять же, register(b0) – это числовой идентификатор (номер слота) константного буфера. Если мы создадим еще один буфер, ему надо присвоить номер b1. Позже поймете, зачем это нужно.
Итак, константный буфер есть. Как загрузить в него данные из программы. Тут тоже ничего сложного, хотя без возни не обойтись. Вспомните последовательность действий при создании буфера вершин. Создается структура с описанием вершин, потом массив элементов этой структуры запихивается в объект буфера Direct3D. С константным буфером все так же, только нам не нужно создавать массив элементов. Первый шаг: объявление.

// Структура константного буфера (совпадает со структурой в шейдере)

struct ConstantBuffer

{

    XMMATRIX mWorld;              // Матрица мира

    XMMATRIX mView;        // Матрица вида

    XMMATRIX mProjection;  // Матрица проекции

};

ID3D11Buffer* g_pConstantBuffer = NULL; // Константный буфер

Второй шаг: инициализируем объект буфера.

// Создание константного буфера

    [...]

    bd.ByteWidth = sizeof(ConstantBuffer);            // размер буфера = размеру структуры

    bd.BindFlags = D3D11_BIND_CONSTANT_BUFFER; // тип - константный буфер

    g_pd3dDevice->CreateBuffer( &bd, NULL, &g_pConstantBuffer );

Затем при необходимости можно обновлять данные в объекте буфера таким способом:

ConstantBuffer cb;

    cb.mWorld = XMMatrixTranspose( g_MatWorld );

    cb.mView = XMMatrixTranspose( g_MatView );

    cb.mProjection = XMMatrixTranspose( g_MatProjection );

    // загружаем временную структуру в константный буфер g_pConstantBuffer

    g_pImmediateContext->UpdateSubresource( g_pConstantBuffer, 0, NULL, &cb, 0, 0 );

Мы загружаем информацию в структуру, а затем копируем данные из структуры в буфер при помощи функции UpdateSubresource(…) из интерфейса ID3D11DeviceContext.
И последнее. Пока что наш буфер существует в программе параллельно с константным буфером в шейдере. Необходимо каким-то образом их связать. Для этого в интерфейсе ID3D11DeviceContext существует специальная функция, которая копирует буфер Direct3D в шейдер:

g_pImmediateContext->VSSetConstantBuffers( 0, 1, &g_pConstantBuffer );

2. Массивы вершин и буферы индексов.

В прошлом уроке мы разобрались с буферами вершин. Возможно, это я разобрался, а вы только почесали в затылке и плюнули на все. В этот раз у нас появится еще один вид буферов – индексный буфер (index buffer). Он тесно связан с буфером вершин, можно даже сказать, является его второй половиной. Если продолжить метафору, то буфер вершин из нашего прошлого урока был гермафродитом.

Мы уже говорили, что способ рисования «список треугольников» (Triangle List) заставляет хранить много избыточной информации. Поскольку оперативная память у компьютеров не является бесконечным ресурсом, это являлось в свое время очень серьезным недостатком. И создатели DirectX придумали такой выход: почему бы не отделить информацию о собственно вершинах от порядка их загрузки при рисовании? Пусть в один буфер помещается массив из вершин в любом порядке, а в другой буфер – индексы этого массива вершин, уже в том порядке, который используется при рисовании списка или ленты треугольников. Тогда, например, для кубика нам понадобится буфер вершин, состоящий только из восьми элементов и буфер индексов, состоящий из 36 индексов (по 3 на треугольник), но индексы занимают очень мало места.
Вот пример для кубика:

// Данные для буфера вершин

    SimpleVertex vertices[] =

    {

        { XMFLOAT3( -1.0f,  1.0f, -1.0f ) },

        { XMFLOAT3(  1.0f,  1.0f, -1.0f ) },

        { XMFLOAT3(  1.0f,  1.0f,  1.0f ) },

        { XMFLOAT3( -1.0f,  1.0f,  1.0f ) },

        { XMFLOAT3( -1.0f, -1.0f, -1.0f ) },

        { XMFLOAT3(  1.0f, -1.0f, -1.0f ) },

        { XMFLOAT3(  1.0f, -1.0f,  1.0f ) },

        { XMFLOAT3( -1.0f, -1.0f,  1.0f ) }

    };

И индексы:

// Данные для буфера индексов

    WORD indices[] =

    {

        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

    };

Буфер индексов создается аналогично буферу вершин или любому другому.

Еще одно важное замечание. Если переставить местами индексы одного треугольника в массиве индексов, казалось бы, ничего измениться не должно. Например, построим первый треугольник не в порядке vertices[3]-vertices[1]-vertices[0], а в порядке 3-0-1. Если откомпилировать такую программу, этого треугольника мы не увидим. Куда же он девается?
А никуда! Он остается на том же месте, просто мы его не видим. Дело в том, что у любого треугольника есть две стороны: лицевая и задняя. В трехмерной графике задняя сторона обычно не рисуется. То есть если нарисовать куб по примеру выше, не внося изменений, но смотреть на него не снаружи, а изнутри, то мы не увидим ничего. И под «ничего» я подразумеваю, конечно, «ничего кроме синего экрана». Это называется куллингом (culling). Следует иметь в виду: лицевая сторона – та, которая находится лицом к нам при рисовании по часовой стрелке.

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

Я решил в этом уроке все-таки перейти от рисования плоских объектов к объемным. Вместо треугольника сегодня родим пирамиду! Более того, заставим ее вращаться вокруг оси Y и раскрасим все вершины в разные цвета.

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

#include <windows.h>

#include <d3d11.h>

#include <d3dx11.h>

#include <d3dcompiler.h>

#include <xnamath.h>

#include "resource.h"

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

// Структуры

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

// Структура вершины

struct SimpleVertex

{

    XMFLOAT3 Pos;   // Координаты точки в пространстве

    XMFLOAT4 Color; // Теперь каждая вершина будет содержать информацию о цвете

};

// Структура константного буфера (совпадает со структурой в шейдере)

struct ConstantBuffer

{

    XMMATRIX mWorld;              // Матрица мира

    XMMATRIX mView;        // Матрица вида

    XMMATRIX mProjection;  // Матрица проекции

};

Как видите, в структуру, описывающую вершины, мы добавили цвет в формате 4-float, т. е. R, G, B, A. Константный буфер в этом уроке состоит только из трех матриц.

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

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

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;             // Буфер вершин

ID3D11Buffer*           g_pIndexBuffer = NULL;        // Буфер индексов вершин

ID3D11Buffer*           g_pConstantBuffer = NULL;           // Константный буфер

XMMATRIX                g_World;                      // Матрица мира

XMMATRIX                g_View;                       // Матрица вида

XMMATRIX                g_Projection;                 // Матрица проекции

Во-первых, добавились два новых буфера. Мы уже обсудили их. Во-вторых, три переменных-матрицы. Их мы будем модифицировать при необходимости и затем загружать в константный буфер.

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

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

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

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

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

HRESULT InitMatrixes();    // Инициализация матриц

void SetMatrixes();        // Обновление матрицы мира

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

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

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

Ага, появились две новый функции. InitMatrixes() вызывается в wWinMain(…) при запуске приложения. Сейчас у нас последовательность инициализации такая:

Создание окна à Создание устройств DirectX à Создание геометрии (вершин, их описания, загрузка шейдеров) à Инициализация матриц à Переход в цикл сообщений.

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

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

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

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;

    }

    // Инициализация матриц

    if( FAILED( InitMatrixes() ) )

    {

        CleanupDevice();

        return 0;

    }

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

    MSG msg = {0};

    while( WM_QUIT != msg.message )

    {

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

        {

            TranslateMessage( &msg );

            DispatchMessage( &msg );

        }

        else

        {

            SetMatrixes(); // Обновить матрицу мира

            Render();

        }

    }

    CleanupDevice();

    return ( int )msg.wParam;

}

Дальше вообще кусок кода, который не изменился. Не пугайтесь, в следующий раз придется менять и функцию InitDevice().

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

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

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"Urok3WindowClass";

    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"Urok3WindowClass", L"Урок 3. Матрицы", 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;

}

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

// Вспомогательная функция для компиляции шейдеров в 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;

}

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

// Создание устройства 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;

}

Ну вот. Только начали урок, а уже половина программы готова. Впрочем, не радуйтесь слишком рано. Функцию InitGeometry() придется рассмотреть по кускам.

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

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

HRESULT InitGeometry()

{

    HRESULT hr = S_OK;

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

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

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

    if (FAILED(hr))

    {

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

        return hr;

    }

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

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

    if (FAILED(hr))

    {

        pVSBlob->Release();

        return hr;

    }

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

    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 },

    };

    UINT numElements = ARRAYSIZE( layout );

В шаблоне вершин появилась дополнительная строка. Ключевое слово “COLOR” указывает, что второй член структуры задает цвет. Формат R32G32B32A32 – размер переменной цвета, 16 байт. Пятый параметр, равный 12, уже упоминался в прошлом уроке. Это смещение в байтах переменной (SimpleVertex::Color) относительно начала структуры. Размер переменной координат составляет 12 байт (R32G32B32), поэтому и смещение равно 12.

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

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

                                          pVSBlob->GetBufferSize(), &g_pVertexLayout );

    pVSBlob->Release();

    if (FAILED(hr)) return hr;

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

    g_pImmediateContext->IASetInputLayout( g_pVertexLayout );

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

    ID3DBlob* pPSBlob = NULL;

    hr = CompileShaderFromFile( L"urok3.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[] =

    {  /* координаты X, Y, Z                          цвет R, G, B, A     */

        { XMFLOAT3(  0.0f,  1.5f,  0.0f ), XMFLOAT4( 1.0f, 1.0f, 0.0f, 1.0f ) },

        { XMFLOAT3( -1.0f,  0.0f, -1.0f ), XMFLOAT4( 0.0f, 1.0f, 0.0f, 1.0f ) },

        { XMFLOAT3(  1.0f,  0.0f, -1.0f ), XMFLOAT4( 1.0f, 0.0f, 0.0f, 1.0f ) },

        { XMFLOAT3( -1.0f,  0.0f,  1.0f ), XMFLOAT4( 0.0f, 1.0f, 1.0f, 1.0f ) },

        { XMFLOAT3(  1.0f,  0.0f,  1.0f ), XMFLOAT4( 1.0f, 0.0f, 1.0f, 1.0f ) }

    };

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

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

    bd.Usage = D3D11_USAGE_DEFAULT;

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

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

    bd.CPUAccessFlags = 0;

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

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

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

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

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

    if (FAILED(hr)) return hr;

Понятно: требуется всего пять точек. Основание пирамиды будет квадратом с координатами от (-1, 0, -1) до (1, 0, 1), высота пирамиды – 1,5. Для каждой вершины мы устанавливаем свой цвет, и не забываем при создании буфера указать размер с учетом количества вершин. Теперь создадим буфер индексов.

// Создание буфера индексов:

    // Создание массива с данными

    WORD indices[] =

    {  // индексы массива vertices[], по которым строятся треугольники

        0,2,1,      /* Треугольник 1 = vertices[0], vertices[2], vertices[1] */

        0,3,4,      /* Треугольник 2 = vertices[0], vertices[3], vertices[4] */

        0,1,3,      /* и т. д. */

        0,4,2,

        1,2,3,

        2,4,3,

    };

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

    bd.ByteWidth = sizeof( WORD ) * 18; // для 6 треугольников необходимо 18 вершин

    bd.BindFlags = D3D11_BIND_INDEX_BUFFER; // тип - буфер индексов

    bd.CPUAccessFlags = 0;

    InitData.pSysMem = indices;         // указатель на наш массив индексов

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

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

    if (FAILED(hr)) return hr;

Как видите, абсолютно ничего сложного. Точно так же создаем массив данных и загружаем его в буфер DirectX. Обратите внимание на тип буфера, BindFlags - D3D11_BIND_INDEX_BUFFER.

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

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

    UINT stride = sizeof( SimpleVertex );

    UINT offset = 0;

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

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

    g_pImmediateContext->IASetIndexBuffer( g_pIndexBuffer, DXGI_FORMAT_R16_UINT, 0 );

    // Установка способа отрисовки вершин в буфере (в данном случае - TRIANGLE LIST,

    // т. е. точки 1-3 - первый треугольник, 4-6 - второй и т. д.

    g_pImmediateContext->IASetPrimitiveTopology( D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST );

Наконец, осталось создать константный буфер. Буфер есть буфер, мы уже сотни таких сделали. Главное – знать размер (BytesWidth) и тип (BindFlags).

// Создание константного буфера

    bd.Usage = D3D11_USAGE_DEFAULT;

    bd.ByteWidth = sizeof(ConstantBuffer);            // размер буфера = размеру структуры

    bd.BindFlags = D3D11_BIND_CONSTANT_BUFFER; // тип - константный буфер

    bd.CPUAccessFlags = 0;

    hr = g_pd3dDevice->CreateBuffer( &bd, NULL, &g_pConstantBuffer );

    if (FAILED(hr)) return hr;

    return S_OK;

}

Следующая функция создает матрицы. Она маленькая, поэтому рассмотрим ее сразу всю.

// Инициализация матриц

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

HRESULT InitMatrixes()

{

    RECT rc;

    GetClientRect( g_hWnd, &rc );

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

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

    // Инициализация матрицы мира

    g_World = XMMatrixIdentity();

    // Инициализация матрицы вида

    XMVECTOR Eye = XMVectorSet( 0.0f, 1.0f, -5.0f, 0.0f );  // Откуда смотрим

    XMVECTOR At = XMVectorSet( 0.0f, 1.0f, 0.0f, 0.0f );    // Куда смотрим

    XMVECTOR Up = XMVectorSet( 0.0f, 1.0f, 0.0f, 0.0f );    // Направление верха

    g_View = XMMatrixLookAtLH( Eye, At, Up );

    // Инициализация матрицы проекции

    g_Projection = XMMatrixPerspectiveFovLH( XM_PIDIV4, width / (FLOAT)height, 0.01f, 100.0f );

    return S_OK;

}

Здесь используются три встроенные функции DirectX. Первая из них, XMMatrixIdentity(), просто инициализирует матрицу без применения к ней трансформаций (трансформации – перемещение, вращение, масштабирование – тема следующего урока). Кстати, если любопытно, посмотрите на члены матрицы. Ее основу составляют переменные _11 - _44 типа float. Как я уже говорил, матрица – это двухмерный массив.

Насчет функции XMMatrixLookAtLH(…) мы поговорили в теоретической части урока. Она устанавливает камеру (матрицу вида). Мы смотрим почти в центр мира прямо на нашу пирамиду в (0, 1, 0) из точки, сдвинутой по оси Z на -5 единиц.

Наконец, устанавливающая матрицу проекции функция XMMatrixPerspectiveFovLH(…) принимает следующие параметры:

1) ширина угла объектива (π/4), можно имитировать и широкоугольный объектив

2) "квадратность" пикселя (ширина/высота экрана)

3) самое ближнее видимое расстояние (0.01 ед.)

4) самое дальнее видимое расстояние (100 ед.)

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

// Обновление матриц

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

void SetMatrixes()

{

    // Обновление переменной-времени

    static float t = 0.0f;

    if( g_driverType == D3D_DRIVER_TYPE_REFERENCE )

    {

        t += ( float )XM_PI * 0.0125f;

    }

    else

    {

        static DWORD dwTimeStart = 0;

        DWORD dwTimeCur = GetTickCount();

        if( dwTimeStart == 0 )

            dwTimeStart = dwTimeCur;

        t = ( dwTimeCur - dwTimeStart ) / 1000.0f;

    }

    // Вращать мир по оси Y на угол t (в радианах)

    g_World = XMMatrixRotationY( t );

    // Обновить константный буфер

    // создаем временную структуру и загружаем в нее матрицы

    ConstantBuffer cb;

    cb.mWorld = XMMatrixTranspose( g_World );

    cb.mView = XMMatrixTranspose( g_View );

    cb.mProjection = XMMatrixTranspose( g_Projection );

    // загружаем временную структуру в константный буфер g_pConstantBuffer

    g_pImmediateContext->UpdateSubresource( g_pConstantBuffer, 0, NULL, &cb, 0, 0 );

}

Не буду объяснять микрософтовскую мудрость с обновлением переменной t, в конце концов, она не относится к DirectX, да и на самом деле только на первый взгляд кажется страшной. Можно было написать одной строчкой: /t += XM_PI/0.0125f;/, так что это вообще не важно. А об остальном мы уже поговорили. Ах, да! Функция XMMatrixTranspose(…) переносит члены одной матрицы в другую (в данном случае из глобальных переменных-матриц в матрицы-члены структуры). Не пользуйтесь для копирования матриц переопределенным оператором «=».

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

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

void CleanupDevice()

{

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

    if( g_pImmediateContext ) g_pImmediateContext->ClearState();

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

    if( g_pConstantBuffer ) g_pConstantBuffer->Release();

    if( g_pVertexBuffer ) g_pVertexBuffer->Release();

    if( g_pIndexBuffer ) g_pIndexBuffer->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();

}

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

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

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

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->VSSetConstantBuffers( 0, 1, &g_pConstantBuffer );

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

    // Нарисовать 18 индексированных вершин

    g_pImmediateContext->DrawIndexed( 18, 0, 0 );

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

    g_pSwapChain->Present( 0, 0 );

}

Во-первых, измененный в функции SetMatrixes() константный буфер, надо загрузить в вершинный шейдер. С этим справляется функция VSSetConstantBuffers(…) устройства рисования. Во-вторых, теперь мы рисуем не просто вершины, а индексированные вершины. Из-за этого вместо функции Draw(…) надо использовать DrawIndexed(…). В первом параметре опять передается количество вершин.

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

// Переменные константных буферов

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

cbuffer ConstantBuffer : register( b0 ) // b0 - индекс буфера

{

    matrix World;

    matrix View;

    matrix Projection;

}

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

struct VS_OUTPUT    // формат выходных данных вершинного шейдера

{

    float4 Pos : SV_POSITION;

    float4 Color : COLOR0;

};

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

В вершинном шейдере произовдятся все трансформации позиции из одной системы координат в другую. Напомню последовательность: пространство мира à вида à проекции. Достаточно просто последовательно умножать координаты вершины на соответствующую матрицу.

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

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

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 = Color;

    return output;

}

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

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

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

float4 PS( VS_OUTPUT input ) : SV_Target

{

    return input.Color;

}

Если все сделано правильно, программа скомпилируется и изобразит вращающуюся пирамидку.

Красиво, правда? Я рекомендую в свободное время попробовать внести в программу изменения, чтобы как следует разобраться в матрицах и константных буферах. Это пригодится в следующем уроке. А пока – до встречи.

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

Комментарии

1 комментарий на “Урок DirectX11 на C++ №3: » Матрицы и 3D-трансформации»”
  1. Андрей:

    В файрфокс текст «обрезается»….поправьте,пожалуйста

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

Внимание! Не будут добавляться комментарии в виде откровенного спама или прямого анкора на свои сайты. Все спамеры будут передаваться в базу Akismet

Подтвердите, что Вы не бот — выберите человечка с поднятой рукой: