Введение в программирование трехмерных игр с DX9

       

Пример приложения: рассеянный свет


В качестве разминки напишем вершинный шейдер, который будет реализовать для вершин обычное рассеянное освещение для направленного (параллельного) источника света. Напомним, что для рассеянного света количество получаемого вершиной света вычисляется на основании угла между нормалью вершины и вектором света (который указывает в направлении на источник света). Чем меньше угол, тем больше света получает вершина, и чем больше угол, тем меньше света получает вершина. Если угол больше или равен 90 градусам, вершина вообще не освещена. Подробное описание алгоритма рассеянного освещения приводилось в разделе 13.4.1.

Начнем с исследования кода вершинного шейдера.

// Файл: diffuse.txt // Описание: Вершинный шейдер, реализующий рассеянное освещение.

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

matrix ViewMatrix; matrix ViewProjMatrix;

vector AmbientMtrl; vector DiffuseMtrl;

vector LightDirection;

// // Глобальные переменные, используемые для хранения // интенсивности фонового света (фоновая составляющая // испускаемого источником света) и интенсивности рассеиваемого // света (рассеиваемая составляющая испускаемого источником света). // Эти переменные инициализируются в шейдере. //

vector DiffuseLightIntensity = {0.0f, 0.0f, 1.0f, 1.0f}; vector AmbientLightIntensity = {0.0f, 0.0f, 0.2f, 1.0f};

// // Входная и выходная структуры //

struct VS_INPUT { vector position : POSITION; vector normal : NORMAL; }; struct VS_OUTPUT { vector position : POSITION; vector diffuse : COLOR; };

// // Точка входа //

VS_OUTPUT Main(VS_INPUT input) { // Обнуляем все члены экземпляра выходной структуры VS_OUTPUT output = (VS_OUTPUT)0;

// // Преобразуем местоположение вершины в однородное пространство // отсечения и сохраняем его в члене output.position // output.position = mul(input.position, ViewProjMatrix);


// // Преобразуем вектор освещения и нормаль в пространство вида. // Присваиваем компоненте w значение 0, поскольку мы преобразуем // векторы, а не точки. // LightDirection.w = 0.0f; input.normal.w = 0.0f; LightDirection = mul(LightDirection, ViewMatrix); input.normal = mul(input.normal, ViewMatrix);
// // Вычисляем косинус угла между вектором света и нормалью // float s = dot(LightDirection, input.normal);
// // Вспомните, что если угол между нормалью поверхности // и вектором освещения больше 90 градусов, поверхность // не получает света. Следовательно, если угол больше // 90 градусов, мы присваиваем s ноль, сообщая тем самым, // что поверхность не освещена. // if(s < 0.0f) s = 0.0f;
// // Отраженный фоновый свет вычисляется путем покомпонентного // умножения вектора фоновой составляющей материала и вектора // интенсивности фонового света. // // Отраженный рассеиваемый свет вычисляется путем покомпонентного // умножения вектора рассеиваемой составляющей материала на вектор // интенсивности рассеиваемого света. Затем мы масштабируем полученный // вектор, умножая каждую его компоненту на коэффициент затенения s, // чтобы затемнить цвет в зависимости от того, сколько света получает // вершина от источника. // // Сумма фоновой и рассеиваемой компонент дает нам // итоговый цвет вершины. // output.diffuse = (AmbientMtrl * AmbientLightIntensity) + (s * (DiffuseLightIntensity * DiffuseMtrl));
return output; }
Теперь, когда мы посмотрели на код вершинного шейдера, давайте переключим передачу и взглянем на код приложения. В приложении используются следующие, относящиеся к рассматриваемой теме, глобальные переменные:
IDirect3DVertexShader9* DiffuseShader = 0; ID3DXConstantTable* DiffuseConstTable = 0;
ID3DXMesh* Teapot = 0;
D3DXHANDLE ViewMatrixHandle = 0; D3DXHANDLE ViewProjMatrixHandle = 0; D3DXHANDLE AmbientMtrlHandle = 0; D3DXHANDLE DiffuseMtrlHandle = 0; D3DXHANDLE LightDirHandle = 0;
D3DXMATRIX Proj;
У нас есть переменные, представляющие сам вершинный шейдер и его таблицу констант.

Есть переменная для хранения сетки чайника, а за ней идет набор переменных D3DXHANDLE, чьи имена показывают для связи с какими переменными шейдера они используются.
Функция Setup выполняет следующие действия:
Создает сетку чайника.
Компилирует вершинный шейдер.
Создает вершинный шейдер на основе скомпилированного кода.
Получает через таблицу констант дескрипторы нескольких переменных программы шейдера.
Инициализирует через таблицу констант некоторые переменные шейдера.
ПРИМЕЧАНИЕ
В данном приложении для структуры данных вершины не требуются никакие дополнительные компоненты, которые нельзя описать с помощью настраиваемого формата вершин. Поэтому в данном примере мы используем настраиваемый формат вершин, а не объявление вершин. Вспомните, что описание настраиваемого формата вершин автоматически преобразуется в объявление вершин. bool Setup() { HRESULT hr = 0;
// // Создание геометрии: //
D3DXCreateTeapot(Device, &Teapot, 0);
// // Компиляция шейдера //
ID3DXBuffer* shader = 0; ID3DXBuffer* errorBuffer = 0;
hr = D3DXCompileShaderFromFile( "diffuse.txt", 0, 0, "Main", // имя точки входа "vs_1_1", D3DXSHADER_DEBUG, &shader, &errorBuffer, &DiffuseConstTable);
// Выводим сообщения об ошибках if(errorBuffer) { ::MessageBox(0, (char*)errorBuffer->GetBufferPointer(), 0, 0); d3d::Release<ID3DXBuffer*>(errorBuffer); }
if(FAILED(hr)) { ::MessageBox(0, "D3DXCompileShaderFromFile() - FAILED", 0, 0); return false; }
// // Создаем шейдер //
hr = Device->CreateVertexShader( (DWORD*)shader->GetBufferPointer(), &DiffuseShader);
if(FAILED(hr)) { ::MessageBox(0, "CreateVertexShader - FAILED", 0, 0); return false; }
d3d::Release<ID3DXBuffer*>(shader);
// // Получаем дескрипторы // ViewMatrixHandle = DiffuseConstTable->GetConstantByName( 0, "ViewMatrix"); ViewProjMatrixHandle = DiffuseConstTable->GetConstantByName( 0, "ViewProjMatrix"); AmbientMtrlHandle = DiffuseConstTable->GetConstantByName( 0, "AmbientMtrl"); DiffuseMtrlHandle = DiffuseConstTable->GetConstantByName( 0, "DiffuseMtrl"); LightDirHandle = DiffuseConstTable->GetConstantByName( 0, "LightDirection");


// // Устанавливаем константы шейдера: //
// Направление на источник света: D3DXVECTOR4 directionToLight(-0.57f, 0.57f, -0.57f, 0.0f); DiffuseConstTable->SetVector(Device, LightDirHandle, &directionToLight);
// Материалы: D3DXVECTOR4 ambientMtrl(0.0f, 0.0f, 1.0f, 1.0f); D3DXVECTOR4 diffuseMtrl(0.0f, 0.0f, 1.0f, 1.0f); DiffuseConstTable->SetVector(Device, AmbientMtrlHandle, &ambientMtrl); DiffuseConstTable->SetVector(Device, DiffuseMtrlHandle, &diffuseMtrl); DiffuseConstTable->SetDefaults(Device);
// Вычисляем матрицу проекции D3DXMatrixPerspectiveFovLH( &Proj, D3DX_PI * 0.25f, (float)Width / (float)Height, 1.0f, 1000.0f);
return true; }
Функция Display достаточно простая. Она проверяет какие клавиши нажал пользователь и соотвествующим образом изменяет матрицу вида. Однако, поскольку преобразование вида мы выполняем в шейдере, нам необходимо обновить значение переменной шейдера, которая хранит матрицу вида. Мы делаем это через таблицу констант:
bool Display(float timeDelta) { if(Device) { // // Код обновления матрицы вида пропущен... //
D3DXMATRIX V; D3DXMatrixLookAtLH(&V, &position, &target, &up);
DiffuseConstTable->SetMatrix(Device, ViewMatrixHandle, &V);
D3DXMATRIX ViewProj = V * Proj; DiffuseConstTable->SetMatrix(Device, ViewProjMatrixHandle, &ViewProj);
// // Визуализация //
Device->Clear(0, 0, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER, 0xffffffff, 1.0f, 0); Device->BeginScene();
Device->SetVertexShader(DiffuseShader);
Teapot->DrawSubset(0);
Device->EndScene(); Device->Present(0, 0, 0, 0); } return true; }
Обратите внимание, что мы включаем вершинный шейдер, который хотим использовать, перед вызовом DrawSubset.
Очистка выполняется как обычно; мы просто освобождаем все запрошенные интерфейсы:
void Cleanup() { d3d::Release<ID3DXMesh*>(Teapot); d3d::Release<IDirect3DVertexShader9*>(DiffuseShader); d3d::Release<ID3DXConstantTable*>(DiffuseConstTable); }

Содержание раздела