Этот пост является 2-ой и последней частью статьи о разработки системы частиц на DirectX 9. Если вы еще не читали первую часть, то рекомендую с ней ознакомится.
В этой части статьи будет рассмотрено: работа со спрайтами, вершинные и пиксельные шейдеры, эффекты, пост-эффекты. В частности для реализации пост-эффекта приём рендера в текстуру.
0. Базовые сведения
Спрайты
Спрайты представляют из себя текстуру, перемещаемую по экрану, и изображающую объект или часть объекта. Так как частицы в нашей системе представляют из себя всего лишь точки, то накладывая на них различные текстуры можно визуализовать любой объект (например облака). Поскольку спрайт это простая текстура, то необходимо иметь базовые представления о них.
Текстура вместо пикселей, как мы привыкли, имеет тексели (texel). Direct3D использует для текстур систему координат, образованную горизонтальной осью U и вертикальной осью V.
Вершинные шейдеры
Вершинные шейдеры это программа, которая создается на специальном языке HLSL (или ассемблере), и занимается преобразованием вершин и освещением. В вершинном шейдере мы можем взять положение вершины и переместить её в совсем другое место. В статье вершинные шейдер будет так же использоваться для генерации координат текстур.
Пиксельные шейдеры
Похожи на вершинные шейдеры, только вместо них они занимаются растеризацией изображения. В такой шейдер передаются данные о текстуре, цвете и много других, а на основании этого шейдер обязан вернуть цвет пикселя. Мы будем использовать их для текстурирования.
Эффекты и пост-эффекты
Эффекты включат пиксельные и\или вершинные шейдеры, и один или несколько проходов визуализации. С помощью них можно реализовать, например, эффекты размытия или свечения. Пост-эффекты отличаются от обычных тем, что применяются к уже растрезированой сцене.
1. Текстурируем частицы
Перед тем как накладывать текстуру на частицы, необходимо изменить тип, который мы использовали для представления вершин в буфере на следующий:
Загружаем текстуру и устанавилваем из файла, которая будет представлять частицу.
Все, теперь при запуске приложения вы увидите вместо простых точек текстурированные частицы, но мы пойдем дальше и добавим простой эффект к получившемуся изображению.
Результат визуализации:
2. Эффекты
Для разработки эффектов существует замечательная программа от NVIDIA, называется она Fx Composer. Поддерживается отладка шейдеров, шейдеры 4-ой версии, DIrect3D (9, 10) и OpenGL. Очень рекомендую, но в данной статье эта среда разработки рассматриваться не будет.
Для начала рассмотрим основную структуру эффектов:
Как видно из кода каждый из шейдеров принимает и возвращает какое-либо значение. Вершинный шейдер обязан вернуть координаты вершины, а пиксельный цвет обрабатываемого пикселя. Эффект разделяется на несколько техник. Каждая из техник может представлять свой способ применения эффектов, или же вообще другой эффект. Каждая техника имеет в себе один или несколько проходов визуализации.
Настало время написать свой простой эффект, который, например будет окрашивать частицы в красный цвет:
Код этого эффекта мало отличается от базовой структуры, ранее рассмотренной нами. Мы добавили лишь смешивание с красным цветом методом умножение (Multiply Blend). Вот что у нас получилось:
Неплохо, но можно изменить режим наложения на другой, и сделать смешивание не с одним цветом, а с целой текстурой. Для того, чтобы у нас получилось правильно смешать визуализацию частиц и текстуру, нам необходимо воспользоваться приемом, который называется Render Target (цель визуализации). Суть приема проста, мы визуализируем нашу сцену в текстуру, а потом уже накладываем эффекты на уже растрированное изображение.
Как вы заметили, появился еще один этап визуализации. На первом этапе мы визуализируем частицы такими, какие они есть. Причем визуализацию мы должны будем выполнить в текстуру. А уже во втором проходе визуализации мы накладываем на изображение другую текстуру используя смешивание Linear Light.
Использование эффектов в программе
Эффекты мы создали, настало время изменить код, добавив использование эффектов. Нам необходимо создать и скомпилировать код эффектов, загрузить дополнительную текстуру, а так же создать текстуру, в которую мы будем выполнять визуализацию.
Скрытый текст
ID3DXBuffer* errorBuffer = 0;
D3DXCreateEffectFromFile( // Создаем и компилируем эффект
device,
L"effect.fx",
NULL,
NULL,
D3DXSHADER_USE_LEGACY_D3DX9_31_DLL, //Используем компилятор для DirectX 9
NULL,
&effect,
&errorBuffer );
if( errorBuffer ) //Выводим ошибки, если они есть
{
MessageBoxA(hMainWnd, (char*)errorBuffer->GetBufferPointer(), 0, 0);
errorBuffer->Release();
terminate();
}
// Создаем матрицу, которую передадим в качестве WorldViewProj
// Она необходима для работы вершинного шейдера
D3DXMATRIX W, V, P, Result;
D3DXMatrixIdentity(&Result);
device->GetTransform(D3DTS_WORLD, &W);
device->GetTransform(D3DTS_VIEW, &V);
device->GetTransform(D3DTS_PROJECTION, &P);
D3DXMatrixMultiply(&Result, &W, &V);
D3DXMatrixMultiply(&Result, &Result, &P);
effect->SetMatrix(effect->GetParameterByName(0, "WorldViewProj"), &Result);
// Выбираем самую первую технику
effect->SetTechnique( effect->GetTechnique(0) );
IDirect3DTexture9 *renderTexture = NULL,
*overlayTexture = NULL;
// Поверхности будут использованы для установки цели визуализации
IDirect3DSurface9* orig =NULL
, *renderTarget = NULL;
D3DXCreateTextureFromFile(device, L"overlay.png", &overlayTexture);
// Создаем текстуру, в которую будет выполняться визуализация
D3DXCreateTexture(device, Width, Height, 0, D3DUSAGE_RENDERTARGET, D3DFMT_X8B8G8R8, D3DPOOL_DEFAULT, &renderTexture);
// Сохраняем поверхность, для рендера в текстуру
renderTexture->GetSurfaceLevel(0, &renderTarget);
// Сохраняем оригинальную поверхность
device->GetRenderTarget(0, &orig);
// Устанавлим текстуры эффекта
auto hr = effect->SetTexture( effect->GetParameterByName(NULL, "Overlay"), overlayTexture);
hr /= effect->SetTexture( effect->GetParameterByName(NULL, "Base"), particleTexture);
hr /= effect->SetTexture( effect->GetParameterByName(NULL, "PreRender"), renderTexture);
if(hr != 0)
{
MessageBox(hMainWnd, L"Unable to set effect textures.", L"", MB_ICONHAND);
}
Как мы видим, эффект перед использованием необходимо скомпилировать, выбрать технику, а так же установить все используемые им данные. Для визуализации в текстуру нам необходимо создать саму текстуру, размерами с оригинальную сцену, и поверхность для нее. Поверхность будет использована при визуализации.
Теперь осталось только отрисовать текстуры с использованием эффекта. Делается это так:
Скрытый текст
UINT passes = 0; // Здесь будет хранится количество этапов визуализации
effect->Begin(&passes, 0);
for(UINT i=0; i<passes; ++i)
{
effect->BeginPass(i);
if(i == 0)
{
// Очищаем экранный буфер
device->Clear( 0, NULL, D3DCLEAR_TARGET, D3DCOLOR_XRGB(0,0,0), 1.0f, 0 );
// Устанавливаем текстуру, а точнее её поверхность, в качестве цели визуализации
device->SetRenderTarget(0, renderTarget);
// Очищаем текстуру, в которую будет произведен рендер
device->Clear(0, NULL, D3DCLEAR_TARGET, D3DCOLOR_XRGB(0,0,0), 1.0f, 0);
// Рисуем частицы
DrawParticles();
}
else if(i == 1)
{
// Востанавливаем оригинальную поверхность
device->SetRenderTarget(0, orig);
// Рисуем прямоугольник, с наложенной на него текстурой (RenderTexture)
DrawRect();
}
effect->EndPass();
}
effect->End();
// Выводим частицы на экран
device->Present(NULL, NULL, NULL, NULL);
В коде мы использовали DrawRect(), эта функция рисует прямоугольник, на которые наложена текстураRenderTexture. Это особенность приема, после визуализации в текстуру, нам необходимо как-то вывести её на экран для последующей обработки. В этом нам и помогает прямоугольник, который мы рисуем так, чтобы он занимал все экранное пространство. Код инициализации вершин и визуализации прямоугольника я приводить не буду, чтобы не раздувать статью еще больше. Скажу только, что все необходимые действия аналогичны тем, что мы проводили при инициализации частиц. Если у вас возникли трудности, то вы можете посмотреть как реализована эта функция в коде примера.
Эффекты используется так: сначала мы вызываем метод Begin(), получая количество проходов визуализации в эффекте. Затем перед каждым проходом вызываем BeginPass(i), а после EndPass(). И наконец после окончания визуализации мы вызываем метод End().
Вот что у нас получилось:
На этом статья заканчивается, всем спасибо за внимание. Буду рад ответить на возникшие у вас вопросы в комментариях. Полный исходный код проекта доступен на GitHub. Внимание, для запуска скомпилированного примера необходимо установить VisualC++ Redistributable 2012
UPD Для тех, кто считает, что D3D9 безнадежно устарел, или тем, кому просто хочется, чтобы все расчеты производились на GPU - имеется еще один пример, только уже на D3D10. Как обычно пример и скомпилированное демо доступны на GitHub. Расчеты на GPU прилагаются :)
Страница сайта http://test.interface.ru
Оригинал находится по адресу http://test.interface.ru/home.asp?artId=30938