Урок DirectX11 на C++ №6: «Текстурирование (наложение текстур)»

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

В реальном мире текстур как таковых не существует. Любая поверхность состоит из миллионов разных молекул, которые и придают ей неповторимые особенности отражения света. Но в трехмерной графике у нас нет возможности смоделировать по вершине на каждую молекулу. Поэтому еще на заре (хотя правильнее сказать – на рассвете) развития 3D-технологий и изобрели текстуры.

Чтобы понять, что мы имеем в виду, представьте себе обычную коробку и оберточную бумагу. Коробка – это наша модель, а бумага – текстура. Можно обернуть обычную серую коробку и превратить ее в красивый подарок. Неточность только в том, что в DX текстура натягивается не на объекты, а на примитивы (треугольники), из которых эти объекты состоят.

1. Текстура в DirectX.

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

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

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

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

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

struct CustomVertex

{

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

    XMFLOAT2 Tex;   // Координаты текстуры tu, tv

    ...

};

В этом примере у нас есть прямоугольный параллелепипед, на верхнюю грань которого мы хотим «натянуть» кирпичную текстуру. В четырех вершинах этой грани (0, 1, 2, 3) нужно указать соответствующие координаты текстуры (написаны в скобках). Если бы нам захотелось вместить все кирпичи из картинки на верхнюю грань, пришлось бы просто в координатах 0,6 заменить на 1. Более того, можно на один примитив вместить и две, и три текстуры.

Хорошо. Я уже говорил, что текстуры хранятся в обычных файлах. Эти файлы нужно как-то загружать в оперативную память и в каком-то виде там хранить, чтобы в нужный момент можно было вытянуть текстуру и пришлепнуть к нашему кубу или межзвездному шаттлу (хотя тут я вру, ведь шаттлы не бывают межзвездными: у них нет двигателя, способного летать со сверхсветовой скоростью). Для этих целей используется интерфейс ID3D11ShaderResourceView.

ID3D11ShaderResourceView* g_pTextureRV = NULL; // Объект текстуры

ID3D11SamplerState*       g_pSamplerLinear = NULL;    // Параметры наложения текстуры

Второй интерфейс устанавливает правила наложения текстур на объекты, с которыми мы разберемся позже.

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

2. Еще немного об HLSL.
Раньше мы писали код шейдеров не особо задумываясь над тем, как мы это делаем. Нас больше интересовало, что скрывается за нашими действиями. Однако теперь пришел момент узнать, что мы так вот незатейливо начали изучать HLSL – высокоуровневый язык шейдеров (High Level Shader Language).

[shadres.fx]

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

cbuffer CBMatrixes : register( b0 )

Как и в C++, перед названием переменной задается ее тип: cbuffer.  Мы уже знаем, что он представляет собой что-то вроде структуры константных данных. Кроме него мы уже встречали типы matrix (понятно, это матрица) и некоторые скалярные: float4, int, bool и т. д. В шестом уроке нам понадобятся еще два типа. Это тип текстуры (Texture2D или texture) и тип образца (SamplerState). В следующей главе мы разберемся с образцами, а зачем нужны текстуры, думаю, понятно.
Ключевое слово register связывает созданную переменную с номером регистра, через который мы передаем данные из программы C++ в шейдер:

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

Символ “b” перед номером регистра означает, что мы связываем регистр с константным буфером. Есть и другие символы:

b константый буфер

t текстура или буфер текстур

c смещение буфера

s образец

Поэтому чтобы создать объект текстуры в шейдере, мы напишем такую строку:

[shadres.fx]

Texture2D txDiffuse : register( t0 );  // Буфер текстуры

SamplerState samLinear : register( s0 ); // Буфер образца

Если нам нужно хранить в памяти одновременно две текстуры, мы сделаем так:

[shadres.fx]

Texture2D txFirst : register( t0 );     // Буфер текстуры 1

Texture2D txSecond : register( t1 );    // Буфер текстуры 2

SamplerState samLinear : register( s0 ); // Буфер образца

Если вспомните, парой страниц выше мы создали объект ID3D11SamplerState. В шейдере ему соответствует объект типа SamplerState.
3. Образец рисования текстур.
Сейчас чтобы избежать эффекта «больших пикселей» используются фильтры сглаживания текстур, и самый простой из них – линейный. Пространство между двумя пикселями просто заполняется градиентом. Чтобы задать эти фильтры и другие параметры как раз и используются образцы. Создание образца для текстур очень напоминает создание буфера:

// Создание сэмпла (описания) текстуры

    D3D11_SAMPLER_DESC sampDesc;

    ZeroMemory( &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( &sampDesc, &g_pSamplerLinear );

    if (FAILED(hr)) return hr;

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

[urok6.fx]

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

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

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

Texture2D txDiffuse : register( t0 );        // Буфер текстуры

SamplerState samLinear : register( s0 );     // Буфер образца

// Буфер с информацией о матрицах

cbuffer ConstantBufferMatrixes : register( b0 )

{

    matrix World;            // Матрица мира

    matrix View;             // Матрица вида

    matrix Projection;       // Матрица проекции

}

// Буфер с информацией о свете

cbuffer ConstantBufferLight : register( b1 )

{

    float4 vLightDir[2];    // Направление источника света

    float4 vLightColor[2];  // Цвет источника света

    float4 vOutputColor;    // Активный цвет

}

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

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

Теперь я буду выделять цветом те строки, на которые следует обратить внимание.

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

struct VS_INPUT                   // Входящие данные вершинного шейдера

{

    float4 Pos : POSITION;        // Позиция по X, Y, Z

    float2 Tex : TEXCOORD0;       // Координаты текстуры по tu, tv

    float3 Norm : NORMAL;         // Нормаль по X, Y, Z

};

struct PS_INPUT                   // Входящие данные пиксельного шейдера

{

    float4 Pos : SV_POSITION;     // Позиция пикселя в проекции (экранная)

    float2 Tex : TEXCOORD0;       // Координаты текстуры по tu, tv

    float3 Norm : TEXCOORD1;      // Относительная нормаль пикселя по tu, tv

};

Как видите, в формат вершины мы добавили координаты текстуры. Напомню, что в каждой вершине хранятся координаты точки на исходном рисунке, которая должна находиться в этой вершине.

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

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

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.Norm = mul( input.Norm, World );

    output.Tex = input.Tex;

    return output;

}
// Пиксельный шейдер для куба

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

float4 PS( PS_INPUT input) : SV_Target

{

    float4 finalColor = 0;

    // складываем освещенность пикселя от всех источников света

    for(int i=0; i<2; i++)

    {

        finalColor += saturate( dot( (float3)vLightDir[i], input.Norm) * vLightColor[i] );

    }

    finalColor *= txDiffuse.Sample( samLinear, input.Tex );

    finalColor.a = 1.0f;

    return finalColor;

}

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

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

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

float4 PSSolid( PS_INPUT input) : SV_Target

{

    return vOutputColor;

}

Метод Sample(образец, координаты_на_текстуре) возвращает цвет точки с рисунка текстуры. Очень просто, да? Самому вычислениями заниматься не надо. Чтобы объединить свет и цвет, мы их умножаем. Для интереса попробуйте заменить знак умножения на плюс или минус.

С шейдерами мы разобрались быстро. А вот в коде программы нам придется

1) создать объекты текстуры (из файла) и образца;

и

2) добавить в вершины нашего куба текстурные координаты;

// Урок 6. Наложение текстур. Основан на примере из DX SDK (c) Microsoft Corp.

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

#include <windows.h>

#include <d3d11.h>

#include <d3dx11.h>

#include <d3dx11effect.h>

#include <d3dcompiler.h>

#include <xnamath.h>

#include "resource.h"

#define SAFE_RELEASE(x) if (x) { x->Release(); x = NULL; }

#define MX_SETWORLD 0x101

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

// Структуры

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

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

struct SimpleVertex

{

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

    XMFLOAT2 Tex;     // Координаты текстуры

    XMFLOAT3 Normal;  // Нормаль вершины

};

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

struct ConstantBufferMatrixes

{

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

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

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

};

struct ConstantBufferLight

{

    XMFLOAT4 vLightDir[2]; // Направление света

    XMFLOAT4 vLightColor[2];      // Цвет источника

    XMFLOAT4 vOutputColor; // Активный цвет (для второго PSSolid)

};

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

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

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

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; // Объект вида, задний буфер

ID3D11Texture2D*        g_pDepthStencil = NULL;     // Текстура буфера глубин

ID3D11DepthStencilView* g_pDepthStencilView = NULL; // Объект вида, буфер глубин

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

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

ID3D11PixelShader*      g_pPixelShaderSolid = NULL; // Пиксельный шейдер для источников света

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

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

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

ID3D11Buffer*           g_pCBMatrixes = NULL;       // Константный буфер с информацией о матрицах

ID3D11Buffer*           g_pCBLight = NULL;          // Константный буфер с информацией о свете

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

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

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

FLOAT                   t = 0.0f;                    // Переменная-время

XMFLOAT4                vLightDirs[2];        // Направление света (позиция источников)

XMFLOAT4                vLightColors[2];              // Цвет источников

ID3D11ShaderResourceView* g_pTextureRV = NULL;        // Объект текстуры

ID3D11SamplerState*       g_pSamplerLinear = NULL;    // Параметры наложения текстуры

Секция объявлений изменилась мало, и разжевывать новые строки даже как-то неловко. В структуру вершин добавились координаты текстуры (XMFLOAT2, т. к. это 2 значения типа FLOAT). Константные буферы разделились на два, плюс добавилось два интерфейса для текстуры и образца.

Теперь перепрыгнем сразу в середину функции InitGeometry().

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

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

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

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

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

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

void UpdateLight();        // Обновление параметров света

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

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

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

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

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

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

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

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

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

        {

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

        }

    }

    // Освобождаем объекты DirectX

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

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

    if( !RegisterClassEx( &wcex ) )

        return E_FAIL;

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

    g_hInst = hInstance;

    RECT rc = { 0, 0, 800, 600 };

    AdjustWindowRect( &rc, WS_OVERLAPPEDWINDOW, FALSE );

    g_hWnd = CreateWindow( L"Urok6WindowClass", L"Урок 6. Наложение текстур", 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_SRGB;    // формат пикселя в буфере

    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;

    // Переходим к созданию буфера глубин

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

    D3D11_TEXTURE2D_DESC descDepth;    // Структура с параметрами

    ZeroMemory( &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( &descDepth, NULL, &g_pDepthStencil );

    if (FAILED(hr)) return hr;

    // Теперь надо создать сам объект буфера глубин

    D3D11_DEPTH_STENCIL_VIEW_DESC descDSV;    // Структура с параметрами

    ZeroMemory( &descDSV, sizeof(descDSV) );

    descDSV.Format = descDepth.Format;        // формат как в текстуре

    descDSV.ViewDimension = D3D11_DSV_DIMENSION_TEXTURE2D;

    descDSV.Texture2D.MipSlice = 0;

    // При помощи заполненной структуры-описания и текстуры создаем объект буфера глубин

    hr = g_pd3dDevice->CreateDepthStencilView( g_pDepthStencil, &descDSV, &g_pDepthStencilView );

    if (FAILED(hr)) return hr;

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

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

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

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

    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"urok6.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 },

        { "TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0 },

        { "NORMAL", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 20, D3D11_INPUT_PER_VERTEX_DATA, 0 },

    };

    UINT numElements = ARRAYSIZE( layout );

На втором месте в структур вершин теперь у нас стоят текстурные координаты. Константа DXGI_FORMAT_R32G32_FLOAT указывает, что они состоят из двух значений (на самом деле на R и G, но у программистов Микрософта было туго с фантазией). Дважды по 4 байта = 8, поэтому нормаль теперь находится со смещением 12+8 = 20.

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

    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"urok6.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;

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

    pPSBlob = NULL;

    hr = CompileShaderFromFile( L"urok6.fx", "PSSolid", "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_pPixelShaderSolid );

    pPSBlob->Release();

    if (FAILED(hr)) return hr;

    // Создание буфера вершин (по 4 точки на каждую сторону куба, всего 24 вершины)

    SimpleVertex vertices[] =

    {    /* координаты X, Y, Z            координаты текстры tu, tv   нормаль X, Y, Z        */

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    };

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

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

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

    bd.Usage = D3D11_USAGE_DEFAULT;

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

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

    bd.CPUAccessFlags = 0;

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

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

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

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

    if (FAILED(hr)) return hr;

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

    // 1) cоздание массива с данными

    WORD indices[] =

    {

        3,1,0,

        2,1,3,

        6,4,5,

        7,4,6,

        11,9,8,

        10,9,11,

        14,12,13,

        15,12,14,

        19,17,16,

        18,17,19,

        22,20,21,

        23,20,22

    };

    // 2) cоздание объекта буфера

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

    bd.ByteWidth = sizeof( WORD ) * 36;    // 36 вершин для 12 треугольников (6 сторон)

    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;

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

    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 );

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

    g_pImmediateContext->IASetPrimitiveTopology( D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST );

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

    bd.Usage = D3D11_USAGE_DEFAULT;

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

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

    bd.CPUAccessFlags = 0;

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

    if (FAILED(hr)) return hr;

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

    bd.Usage = D3D11_USAGE_DEFAULT;

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

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

    bd.CPUAccessFlags = 0;

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

    if (FAILED(hr)) return hr;

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

// Загрузка текстуры из файла

    hr = D3DX11CreateShaderResourceViewFromFile( g_pd3dDevice, L"seafloor.dds", NULL, NULL, &g_pTextureRV, NULL );

    if (FAILED(hr)) return hr;

    // Создание сэмпла (описания) текстуры

    D3D11_SAMPLER_DESC sampDesc;

    ZeroMemory( &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( &sampDesc, &g_pSamplerLinear );

    if (FAILED(hr)) return hr;

    return S_OK;

}

Ага, ради этих строк все и затевалось! И опять DirectX радует нас простотой. Одна функция из библиотеки d3dx11 создает объект текстуры и загружает в него данные из файла с изображением! Все как в старые добрые времена D3D8. А образец создается аналогично буферу: заполняется структура-описание sampDesc, в которой мы указываем тип фильтрации (линейная), и с помощью этой структуры методом объекта g_pd3dDevice инициализируется наш образец.

Идем дальше.

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

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

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, 4.0f, -10.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 );

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

    // Параметры: 1) ширина угла объектива 2) "квадратность" пикселя

    // 3) самое ближнее видимое расстояние 4) самое дальнее видимое расстояние

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

    return S_OK;

}

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

// Вычисляем направление света

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

void UpdateLight()

{

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

    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;

    }

    // Задаем начальные координаты источников света

    vLightDirs[0] = XMFLOAT4( -0.577f, 0.577f, -0.577f, 1.0f );

    vLightDirs[1] = XMFLOAT4(  0.0f,   0.0f,   -1.0f,   1.0f );

    // Задаем цвет источников света, у нас он не будет меняться

    vLightColors[0] = XMFLOAT4( 1.0f, 1.0f, 1.0f, 1.0f );

    vLightColors[1] = XMFLOAT4( 1.0f, 0.0f, 0.0f, 1.0f );

    // При помощи трансформаций поворачиваем второй источник света

    XMMATRIX mRotate = XMMatrixRotationY( -2.0f * t );

    XMVECTOR vLightDir = XMLoadFloat4( &vLightDirs[1] );

    vLightDir = XMVector3Transform( vLightDir, mRotate );

    XMStoreFloat4( &vLightDirs[1], vLightDir );

    // При помощи трансформаций поворачиваем первый источник света

    mRotate = XMMatrixRotationY( 0.5f * t );

    vLightDir = XMLoadFloat4( &vLightDirs[0] );

    vLightDir = XMVector3Transform( vLightDir, mRotate );

    XMStoreFloat4( &vLightDirs[0], vLightDir );

}

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

// Устанавливаем матрицы для текущего источника света (0-1) или мира (MX_SETWORLD)

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

void UpdateMatrix(UINT nLightIndex)

{

     // Небольшая проверка индекса

    if (nLightIndex == MX_SETWORLD) {

        // Если рисуем центральный куб: его надо просто медленно вращать

        g_World = XMMatrixRotationAxis( XMVectorSet(1.0f, 1.0f, 1.0f, 0.0f), t );

        nLightIndex = 0;

    }

    else if (nLightIndex < 2) {

        // Если рисуем источники света: перемещаем матрицу в точку и уменьшаем в 5 раз

        g_World = XMMatrixTranslationFromVector( 5.0f * XMLoadFloat4( &vLightDirs[nLightIndex] ) );

        XMMATRIX mLightScale = XMMatrixScaling( 0.2f, 0.2f, 0.2f );

        g_World = mLightScale * g_World;

    }

    else {

        nLightIndex = 0;

    }

    // Обновление содержимого константного буфера

    ConstantBufferMatrixes cb1;    // временный контейнер для первого буферв

    ConstantBufferLight cb2;    // временный контейнер для второго буфера

    cb1.mWorld = XMMatrixTranspose( g_World );    // загружаем в него матрицы

    cb1.mView = XMMatrixTranspose( g_View );

    cb1.mProjection = XMMatrixTranspose( g_Projection );

    cb2.vLightDir[0] = vLightDirs[0];            // загружаем данные о свете

    cb2.vLightDir[1] = vLightDirs[1];

    cb2.vLightColor[0] = vLightColors[0];

    cb2.vLightColor[1] = vLightColors[1];

    cb2.vOutputColor = vLightColors[nLightIndex];

    g_pImmediateContext->UpdateSubresource( g_pCBMatrixes, 0, NULL, &cb1, 0, 0 );

    g_pImmediateContext->UpdateSubresource( g_pCBLight, 0, NULL, &cb2, 0, 0 );

}

Небольшие изменения произошли только в функции UpdateMatrix(…). Отдельно заполняем данными объекты обоих константных буферов.

// Рендеринг кадра

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

void Render()

{

    // Очищаем задний буфер в синий цвет

    float ClearColor[4] = { 0.0f, 0.0f, 1.0f, 1.0f };

    g_pImmediateContext->ClearRenderTargetView( g_pRenderTargetView, ClearColor );

    // Очищаем буфер глубин до едицины (максимальная глубина)

    g_pImmediateContext->ClearDepthStencilView( g_pDepthStencilView, D3D11_CLEAR_DEPTH, 1.0f, 0 );

    UpdateLight();    // Установка освещения

    // Рисуем центральный куб

    // 2) Устанавливаем шейдеры и константные буферы

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

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

    g_pImmediateContext->VSSetConstantBuffers( 1, 1, &g_pCBLight );

    g_pImmediateContext->PSSetConstantBuffers( 0, 1, &g_pCBMatrixes );

    g_pImmediateContext->PSSetConstantBuffers( 1, 1, &g_pCBLight );

    g_pImmediateContext->PSSetShaderResources( 0, 1, &g_pTextureRV );

    g_pImmediateContext->PSSetSamplers( 0, 1, &g_pSamplerLinear );

Обратите внимание на то, как мы загружаем константные буферы. Буфер с матрицами носит гордый индекс b0, а буфер света – b1. Жирным шрифтом в коде выделены как раз индексы буферов, ведь устройству Direct3D надо знать, куда именно загружать данные. Текстура и образец у нас в единичном экземпляре и носят нулевые индексы – t0 и s0.
Текстура относится к ресурсам, поэтому для отправки ее в шейдер используется особая функция установки ресурсов, а для образца существует собственный метод PSSetSamplers(…).

// Рисуем все источники света

    // 1) Устанавливаем пиксельный шейдер

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

    for( int m = 0; m < 2; m++ )

    {

        // 2) Устанавливаем матрицу мира источника света

        UpdateMatrix( m );

        // 3) Рисуем в заднем буфере 36 вершин

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

    }

    // 1) Установка матрицы центрального куба

    UpdateMatrix(MX_SETWORLD);

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

    // 3) Рисуем в заднем буфере 36 вершин

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

    // Копируем задний буфер в передний (на экран)

    g_pSwapChain->Present( 0, 0 );

}

Ничего нового, рисуем наши кубики.

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

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

void CleanupDevice()

{

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

    if( g_pImmediateContext ) g_pImmediateContext->ClearState();

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

    SAFE_RELEASE (g_pSamplerLinear);

    SAFE_RELEASE (g_pTextureRV);

    SAFE_RELEASE (g_pCBMatrixes);

    SAFE_RELEASE (g_pCBLight);

    SAFE_RELEASE (g_pVertexBuffer);

    SAFE_RELEASE (g_pIndexBuffer);

    SAFE_RELEASE (g_pVertexLayout);

    SAFE_RELEASE (g_pVertexShader);

    SAFE_RELEASE (g_pPixelShaderSolid);

    SAFE_RELEASE (g_pPixelShader);

    SAFE_RELEASE (g_pDepthStencil);

    SAFE_RELEASE (g_pDepthStencilView);

    SAFE_RELEASE (g_pRenderTargetView);

    SAFE_RELEASE (g_pSwapChain);

    SAFE_RELEASE (g_pImmediateContext);

    SAFE_RELEASE (g_pd3dDevice);

}

Осталось откомпилировать и скомпоновать проект (не забываем подключить библиотеки DirectX). Запускаем и любуемся результатом!

<&lt Предыдущий урок

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