Direct3D 11 на C++ с нуля |
Урок 5. Нормали и простейший свет
Так-так,
что тут у нас…
Эх,
нет никакого настроения писать, поэтому дайте-ка мне заглянуть в
микрософтовский туториал.
Ага,
оказывается, предыдущий наш урок был скучен и уныл, потому что все объекты со
всех сторон были одинаково освещены. Теперь же нам предстоит рассмотреть
простейшую систему освещения и то, как можно ее использовать на практике в
Direct3D. Пожалуй, на этот раз последуем совету Микрософта и вместо пирамидки
нарисуем кубик. Это будет полезно, чтобы не дать вам забыть, как это делается,
тем более что буфер вершин все равно пришлось бы весь менять.
1. Свет.
Одним
из самых простых видов света в трехмерной графике является направленный (directional). Кроме него есть точечный (point
light) и,
промежуточный, фонарь (spot light).
Все их объединяет одно – система вычисления освещенности, которая хитроумно
называется ламбертианским светом (как я подозреваю, в
честь Ламберта).
Чтобы
вычислить освещенность любой поверхности, нам потребуется узнать нормаль к ней.
В математике нормаль – это просто перпендикуляр, а в трехмерной графике – не
просто, а еще и вектор единичной длины. Задаваться эти нормали (еще раз: эти
векторы длиной в единицу, перпендикулярные к поверхности) будут в вершинах.
Вот, смотрим на картинку слева:
На
ней показаны нормали к верхней стороне куба. Ладно, ладно, это не совсем куб,
но смысл остается тот же. Теперь можете сообразить: для левой боковой грани в
верхних точках нормали будут другие (хотя вершины совпадают с вершинами верхней
стороны), и это же можно сказать про переднюю грань. Я ведь сказал, что
вершинный буфер все равно пришлось бы менять? Причина как раз в этом: в общих
точках необходимо создать несколько вершин (для куба - три) с разными
нормалями.
Теперь
перейдем к направленному свету. Кроме собственного цвета он обладает всего
одной характеристикой – направлением. Directional light проникает сквозь все поверхности и
освещает всю сцену. В отличие от него точечный свет (point
light) напоминает
скорее свечу – светит во все стороны, но только в определенных границах. У него
есть координата и радиусы действия. Spot light – это фонарь. Светит не особенно далеко, но кроме
позиции и радиуса действия имеет направление. Наконец, есть фоновый (ambient) свет. Это минимальный уровень освещенности, которым
обладает любой, даже не находящийся под действием источников света объект.
Однако
вернемся к нашему свету. Направление задается координатой по трем осям. Можно
подумать, что в эту точку мы и будем светить, но такая мысль будет абсолютно
ошибочной. Я бы даже сказал, она будет противоположна истине. Направление света
задается вектором из этой точки в начало координат. Т. е. если vDirection = { 0, 3, 0}, то свет направлен вертикально
вниз. И не важно, где находится освещаемый объект, пусть хоть на высоте 1000
километров. Он спокойненько осветится сверху.
Теперь
рассмотрим, как происходит вычисление освещенности поверхности (забегая вперед
скажу, что это происходит в пиксельном шейдере). Для
удобства я вынес нормаль в центр грани (см. рис. выше). Грань ведь плоская,
поэтому в любой точке освещается одинаково. Справа даже изображена небольшая схемка с нормалью и светом. Как догадался Ламберт, уровень
освещенности зависит от угла между направлением света и нормалью. Если угол
равен нулю, то от поверхности отражается 100% света, а если угол больше или
равен 90˚, то свет на поверхность не попадает. Остается только вычислить
коэффициент освещенности и умножить цвет света на этот коэффициент. Здесь
помогает то, что наша нормаль имеет длину ровно 1. Достаточно спроецировать
вектор света (такой же длины) на нормаль – это и будет наш коэффициент.
Прибегать к заумным вычислениям вам не придется – к ним прибегает встроенная
функция языка шейдеров dot(…):
[shadres.fx]
finalColor = saturate(
dot(vLightDirection, input.Normal) * vLightColor );
Здесь
vLightDirection
– вектор-направление света, input.Normal – нормаль вершины, vLightColor – цвет, которым мы светим. Очень полезная функция saturate(…) здесь не нужна, но она пригодится
при наложении нескольких источников света. Эта функция следит за тем, чтобы
компоненты цвета R,
G, B, A не превысили
единицу, т. е. помогает избежать засвеченности.
Если
вы запутались, не расстраивайтесь. Давайте посмотрим на порядок наших действий:
Добавляем
в описание вершин нормали à Создаем куб с учетом нормалей à Задаем источник света à Рендерим, в
пиксельном шейдере вычисляя освещенность.
Конечно,
информацию о цвете и направлении источника света надо передавать через
константный буфер.
2. Шейдеры.
Давайте
разберемся с шейдерами сразу, чтобы потом не
возникало вопросов вроде «а зачем мы вдруг создаем эту штуковину?»
Во-первых,
в новом мире будет два источника направленного света. Мы заставим их крутиться
вокруг центра мира с разной скоростью и освещать находящийся там куб. Заодно в
точках, откуда исходит свет, мы нарисуем маленькие кубики (повторим прошлый
урок). Тут появляется проблема: выходит, источники света будут находиться
внутри этих наших кубиков, и угол между нормалями и направлением света для них
всегда будет больше 90 градусов. Черные источники света – это даже звучит
странно.
Выход
такой: для этих кубиков запрограммируем вторую функцию пиксельного шейдера. Давайте приступим.
[urok5.fx]
//--------------------------------------------------------------------------------------
// Константные буферы
//--------------------------------------------------------------------------------------
cbuffer ConstantBuffer : register( b0 )
{
matrix World; // Матрица мира
matrix View; // Матрица вида
matrix
Projection; // Матрица проекции
float4 vLightDir[2]; // Направление источника света
float4
vLightColor[2]; // Цвет
источника света
float4
vOutputColor; // Активный цвет
}
Последняя
переменная – активный цвет – как раз и будет использоваться для заливки наших
кубиков, изображающих источники света.
//--------------------------------------------------------------------------------------
struct VS_INPUT // Входящие данные вершинного шейдера
{
float4 Pos : POSITION; // Позиция по X, Y, Z
float3 Norm : NORMAL; // Нормаль по X, Y, Z
};
struct PS_INPUT // Входящие данные пиксельного шейдера
{
float4 Pos :
SV_POSITION; // Позиция пикселя в
проекции (экранная)
float3 Norm :
TEXCOORD0; // Относительная нормаль
пикселя по tu, tv
};
Как
видите, структура вершины опять изменилась. Мы выбросили информацию о цвете и
добавили нормаль. Нормаль задается как вектор по X, Y, Z, причем должно
выполняться соотношение X2+Y2+Z2 = 1.
//--------------------------------------------------------------------------------------
// Вершинный шейдер
//--------------------------------------------------------------------------------------
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 );
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.a = 1;
return finalColor;
}
Поскольку
у нас два источника света, надо сложить их действие. Главную строчку здесь мы
уже обсудили.
//--------------------------------------------------------------------------------------
// Пиксельный шейдер для источников света
//--------------------------------------------------------------------------------------
float4 PSSolid( PS_INPUT input)
: SV_Target
{
return
vOutputColor;
}
Это
самый простой пиксельный шейдер, который встретился
нам во втором уроке. Просто закрашиваем объекты одним цветом.
3. Код программы.
Код
программы претерпел значительные изменения, как вы понимаете.
//--------------------------------------------------------------------------------------
// Урок 4. Нормали и простейший свет. Основан на примере
из DX SDK (c) Microsoft Corp.
//--------------------------------------------------------------------------------------
#include <windows.h>
#include <d3d11.h>
#include <d3dx11.h>
#include <d3dcompiler.h>
#include <xnamath.h>
#include "resource.h"
#define MX_SETWORLD 0x101
//--------------------------------------------------------------------------------------
// Структуры
//--------------------------------------------------------------------------------------
// Структура вершины
struct SimpleVertex
{
XMFLOAT3 Pos; // Координаты точки
в пространстве
XMFLOAT3 Normal; // Нормаль
вершины
};
// Структура константного буфера (совпадает со структурой
в шейдере)
struct ConstantBuffer
{
XMMATRIX mWorld; // Матрица мира
XMMATRIX mView; // Матрица вида
XMMATRIX mProjection; // Матрица проекции
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_pConstantBuffer = NULL; // Константный буфер
XMMATRIX g_World; // Матрица мира
XMMATRIX g_View; // Матрица вида
XMMATRIX g_Projection; // Матрица проекции
FLOAT t =
0.0f; // Переменная-время
XMFLOAT4 vLightDirs[2]; //
Направление света (позиция источников)
XMFLOAT4 vLightColors[2]; // Цвет
источников
//--------------------------------------------------------------------------------------
// Предварительные объявления функций
//--------------------------------------------------------------------------------------
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 ); // Функция окна
В
объявлениях на этот раз много изменений. Переменную-время t я вынес в глобальную область,
потому что она используется в двух функциях: UpdateLight(), которая передвигает свет при рисовании каждого кадра и UpdateMatrix(…), которая устанваливает
необходимую матрицу мира. Обе функции вызываются в процессе рендеринга.
Следующий
огромный кусок кода (вплоть до функции инициализации геометрии) не изменился с
прошлого урока.
//--------------------------------------------------------------------------------------
// Точка входа в программу. Инициализация всех объектов и
вход в цикл сообщений.
// Свободное время используется для отрисовки сцены.
//--------------------------------------------------------------------------------------
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"Urok5WindowClass";
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"Urok5WindowClass", L"Урок 5. Нормали и простейший свет", 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;
// Переходим к созданию буфера глубин
// Создаем текстуру-описание буфера глубин
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"urok5.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 },
{ "NORMAL",
0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0 },
};
UINT numElements = ARRAYSIZE(
layout );
// Создание шаблона вершин
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"urok5.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"urok5.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;
Здесь
необходимо загрузить второй пиксельный шейдер, через
который будут прогоняться пиксели при рендеринге
мелких кубиков, означающих источники света. Эта процедура ничем не отличается
от загрузки первого шейдера, только мы указываем
название функции “PSSolid”.
// Создание буфера вершин (по 4 точки на каждую сторону
куба, всего 24 вершины)
SimpleVertex vertices[] =
{ /* координаты X, Y, Z нормаль
X, Y, Z */
{ XMFLOAT3( -1.0f, 1.0f, -1.0f
), XMFLOAT3( 0.0f, 1.0f, 0.0f ) },
{ XMFLOAT3( 1.0f, 1.0f, -1.0f ), XMFLOAT3( 0.0f, 1.0f, 0.0f ) },
{ XMFLOAT3( 1.0f, 1.0f, 1.0f ), XMFLOAT3( 0.0f, 1.0f, 0.0f ) },
{ XMFLOAT3( -1.0f, 1.0f, 1.0f ), XMFLOAT3( 0.0f, 1.0f, 0.0f ) },
{ XMFLOAT3( -1.0f, -1.0f, -1.0f ), XMFLOAT3( 0.0f, -1.0f, 0.0f ) },
{ XMFLOAT3( 1.0f, -1.0f, -1.0f ), XMFLOAT3( 0.0f, -1.0f, 0.0f ) },
{ XMFLOAT3( 1.0f, -1.0f, 1.0f ), XMFLOAT3( 0.0f, -1.0f, 0.0f ) },
{ XMFLOAT3( -1.0f, -1.0f, 1.0f ), XMFLOAT3( 0.0f, -1.0f, 0.0f ) },
{ XMFLOAT3( -1.0f, -1.0f, 1.0f ), XMFLOAT3( -1.0f, 0.0f, 0.0f ) },
{ XMFLOAT3( -1.0f, -1.0f, -1.0f ), XMFLOAT3( -1.0f, 0.0f, 0.0f ) },
{ XMFLOAT3( -1.0f, 1.0f, -1.0f ), XMFLOAT3( -1.0f, 0.0f, 0.0f ) },
{ XMFLOAT3( -1.0f, 1.0f, 1.0f ), XMFLOAT3( -1.0f, 0.0f, 0.0f ) },
{ XMFLOAT3( 1.0f, -1.0f, 1.0f ), XMFLOAT3( 1.0f, 0.0f, 0.0f ) },
{ XMFLOAT3( 1.0f, -1.0f, -1.0f ), XMFLOAT3( 1.0f, 0.0f, 0.0f ) },
{ XMFLOAT3( 1.0f, 1.0f, -1.0f ), XMFLOAT3( 1.0f, 0.0f, 0.0f ) },
{ XMFLOAT3( 1.0f, 1.0f, 1.0f ), XMFLOAT3( 1.0f, 0.0f, 0.0f ) },
{ XMFLOAT3( -1.0f, -1.0f, -1.0f ), XMFLOAT3( 0.0f, 0.0f, -1.0f ) },
{ XMFLOAT3( 1.0f, -1.0f, -1.0f ), XMFLOAT3( 0.0f, 0.0f, -1.0f ) },
{ XMFLOAT3( 1.0f, 1.0f, -1.0f ), XMFLOAT3( 0.0f, 0.0f, -1.0f ) },
{ XMFLOAT3( -1.0f, 1.0f, -1.0f ), XMFLOAT3( 0.0f, 0.0f, -1.0f ) },
{ XMFLOAT3( -1.0f, -1.0f, 1.0f ), XMFLOAT3( 0.0f, 0.0f, 1.0f ) },
{ XMFLOAT3( 1.0f, -1.0f, 1.0f ), XMFLOAT3( 0.0f, 0.0f, 1.0f ) },
{ XMFLOAT3( 1.0f, 1.0f, 1.0f ), XMFLOAT3( 0.0f, 0.0f, 1.0f ) },
{ XMFLOAT3( -1.0f, 1.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(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, 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;
}
Тут
мы заканчиваем функцию InitGeometry(); в установке матрицы вида поменялась
позиция камеры. Далее рассмотрим ключевую функцию, которая перемещает источники
света.
//--------------------------------------------------------------------------------------
// Вычисляем направление света
//--------------------------------------------------------------------------------------
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 );
Увы,
сначала ничего интересного. Мы просто задаем начальные координаты источников
света, относительно которых потом будем находить текущие координаты при помощи
переменной t,
а также устанавливаем цвета. Первый источник будет белым, второй – красным.
Дальше можно было вычислять координаты при помощи тригонометрических функций,
но с этой задачей отлично справляются матрицы Direct3D, которые легко
преобразовать в простые координаты X, Y, Z:
// При помощи
трансформаций поворачиваем второй источник света
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 );
}
Последовательность
действий такая: Мы 1) создаем матрицу вращения, 2) загружаем в
переменную-вектор начальную позицию источника света и 3) трансформируем позицию
в соответствии с матрицей. Функция XMStoreFloat4(…)
копирует координаты из расширенного формата (вектора) во float4.
Осталось
рассмотреть установку матриц. Помните, что у нас надо рисовать один куб в
центре и два в координатах источников (ну или направлений, как настаивает
Микрософт) света. Последние два куба необходимо еще уменьшить. Всем этим
занимается функция UpdateMatrix(…), которая вызывается перед рендерингом каждого объекта. Параметр функции как раз и
указывает, матрицу для какого куба надо установить.
//--------------------------------------------------------------------------------------
// Устанавливаем матрицы для текущего источника света
(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;
}
При
отрисовке источников света матрица загружается прямо
из координат источника, которые умножаются на 5, т. е. отодвигаются от центра в
5 раз. Если этого не сделать, кубики будут вращаться на орбите, равной единице
(как вы помните, расстояние до источника направленного света не имеет значения
при вычислении освещенности; поэтому мы можем смело отодвигать или придвигать
кубики, они все равно будут точно указывать направление света).
Здесь
осталось загрузить обновленные матрицы и направление света в константный буфер.
// Обновление
содержимого константного буфера
ConstantBuffer cb1; // временный
контейнер
cb1.mWorld =
XMMatrixTranspose( g_World ); // загружаем в него матрицы
cb1.mView = XMMatrixTranspose( g_View );
cb1.mProjection = XMMatrixTranspose( g_Projection
);
cb1.vLightDir[0] =
vLightDirs[0]; // загружаем данные о свете
cb1.vLightDir[1] =
vLightDirs[1];
cb1.vLightColor[0] = vLightColors[0];
cb1.vLightColor[1] = vLightColors[1];
cb1.vOutputColor =
vLightColors[nLightIndex];
g_pImmediateContext->UpdateSubresource( g_pConstantBuffer, 0, NULL,
&cb1, 0, 0 );
}
Конечно,
ничего не мешает создать глобальный объект структуры константного буфера и
работать непосредственно с ним, но вызывать UpdateSubresource(…) все равно придется.
//--------------------------------------------------------------------------------------
// Рендеринг кадра
//--------------------------------------------------------------------------------------
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(); // Устанвока
освещения
На
самом деле в мы не устанавливаем освещение, а только перемещаем свет.
// Рисуем центральный куб
// 1) Установка матрицы центрального куба
UpdateMatrix(MX_SETWORLD);
// 2) Устанавливаем шейдеры и константные буферы
g_pImmediateContext->VSSetShader(
g_pVertexShader, NULL, 0 );
g_pImmediateContext->VSSetConstantBuffers( 0, 1,
&g_pConstantBuffer );
g_pImmediateContext->PSSetShader(
g_pPixelShader, NULL, 0 );
g_pImmediateContext->PSSetConstantBuffers( 0, 1,
&g_pConstantBuffer );
// 3) Рисуем в заднем буфере 36 вершин
g_pImmediateContext->DrawIndexed( 36, 0,
0 );
Даже
не знаю, что тут пояснять. Разве что стоит отметить, что теперь константный
буфер используется и в пиксельном шейдере, поэтому
приходится подключать его дважды.
// Рисуем все источники света
// 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 );
}
Поскольку
буферы вершин, индексов, шейдеры и константный буфер
уже подключены, остается только модифицировать матрицу мира и вызвать DrawIndexed(…) для каждого света.
// Копируем задний буфер в переднйи (на экран)
g_pSwapChain->Present( 0, 0 );
}
//--------------------------------------------------------------------------------------
// Освобождение всех созданных объектов
//--------------------------------------------------------------------------------------
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_pPixelShaderSolid ) g_pPixelShaderSolid->Release();
if(
g_pPixelShader ) g_pPixelShader->Release();
if(
g_pDepthStencil ) g_pDepthStencil->Release();
if(
g_pDepthStencilView ) g_pDepthStencilView->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();
}
Компилируем
и смотрим на результат. Не очень красиво, зато что-то новенькое.
В
следующий раз постараюсь придумать что-то более веселое. До встречи!
P.
S. Из любопытства попробуйте закомментировать установку пиксельного шейдера PSSolid для рендеринга
маленьких кубов и посмотрите, что получится.
Скачать урок 5 (doc) Скачать готовый проект (rar)