Этот пост является 2-ой и последней частью статьи о разработке системы частиц на DirectX 9. Если вы еще не читали первую часть, то рекомендую с ней ознакомиться.
В этой части статьи будет рассмотрено: работа со спрайтами, вершинные и пиксельные шейдеры, эффекты, пост-эффекты. В частности для реализации пост-эффекта — приём рендера в текстуру.
Спрайты представляют из себя текстуру, перемещаемую по экрану, и изображающую объект или часть объекта. Так как частицы в нашей системе представляют из себя всего лишь точки, то накладывая на них различные текстуры можно визуализовать любой объект (например облака). Поскольку спрайт это простая текстура, то необходимо иметь базовые представления о них.
Текстура вместо пикселей, как мы привыкли, имеет тексели (texel). Direct3D использует для текстур систему координат, образованную горизонтальной осью U и вертикальной осью V.
Вершинные шейдеры это программа, которая создается на специальном языке HLSL (или ассемблере), и занимается преобразованием вершин и освещением. В вершинном шейдере мы можем взять положение вершины и переместить её в совсем другое место. В статье вершинные шейдер будет так же использоваться для генерации координат текстур.
Похожи на вершинные шейдеры, только вместо них они занимаются растеризацией изображения. В такой шейдер передаются данные о текстуре, цвете и много других, а на основании этого шейдер обязан вернуть цвет пикселя. Мы будем использовать их для текстурирования.
Эффекты включат пиксельные и\или вершинные шейдеры, и один или несколько проходов визуализации. С помощью них можно реализовать, например, эффекты размытия или свечения.
Пост-эффекты отличаются от обычных тем, что применяются к уже растрезированой сцене.
Перед тем как накладывать текстуру на частицы, необходимо изменить тип, который мы использовали для представления вершин в буфере на следующий:
Значения u и v, необходимо инициализовать нулем при создании.
Так же необходимо изменить флаги при создании буфера, и описание буфера:
Добавляем флаг D3DFVF_TEX0, указывая, что мы будем хранить координаты текстуры. Так же добавляем строку в описание вершин.
А теперь осталось загрузить текстуру и изменить состояния рендера:
Все состояния описывать не буду, информацию о них можно найти на MSDN. Скажу только, что часть из них нам понадобятся для эффектов.
Загружаем текстуру и устанавилваем из файла, которая будет представлять частицу.
Все, теперь при запуске приложения вы увидите вместо простых точек текстурированные частицы, но мы пойдем дальше и добавим простой эффект к получившемуся изображению.
Результат визуализации:
Для разработки эффектов существует замечательная программа от NVIDIA, называется она Fx Composer. Поддерживается отладка шейдеров, шейдеры 4-ой версии, DIrect3D (9, 10) и OpenGL. Очень рекомендую, но в данной статье эта среда разработки рассматриваться не будет.
Для начала рассмотрим основную структуру эффектов:
Как видно из кода каждый из шейдеров принимает и возвращает какое-либо значение. Вершинный шейдер обязан вернуть координаты вершины, а пиксельный цвет обрабатываемого пикселя.
Эффект разделяется на несколько техник. Каждая из техник может представлять свой способ применения эффектов, или же вообще другой эффект.
Каждая техника имеет в себе один или несколько проходов визуализации.
Настало время написать свой простой эффект, который, например будет окрашивать частицы в красный цвет:
Код этого эффекта мало отличается от базовой структуры, ранее рассмотренной нами. Мы добавили лишь смешивание с красным цветом методом умножение (Multiply Blend). Вот что у нас получилось:
Неплохо, но можно изменить режим наложения на другой, и сделать смешивание не с одним цветом, а с целой текстурой.
Для того, чтобы у нас получилось правильно смешать визуализацию частиц и текстуру, нам необходимо воспользоваться приемом, который называется Render Target (цель визуализации). Суть приема проста, мы визуализируем нашу сцену в текстуру, а потом уже накладываем эффекты на уже растрированное изображение.
Вот полный код эффекта реализующего это:
Как вы заметили, появился еще один этап визуализации. На первом этапе мы визуализируем частицы такими, какие они есть. Причем визуализацию мы должны будем выполнить в текстуру. А уже во втором проходе визуализации мы накладываем на изображение другую текстуру используя смешивание Linear Light.
Эффекты мы создали, настало время изменить код, добавив использование эффектов.
Нам необходимо создать и скомпилировать код эффектов, загрузить дополнительную текстуру, а так же создать текстуру, в которую мы будем выполнять визуализацию.
Как мы видим, эффект перед использованием необходимо скомпилировать, выбрать технику, а так же установить все используемые им данные.
Для визуализации в текстуру нам необходимо создать саму текстуру, размерами с оригинальную сцену, и поверхность для нее. Поверхность будет использована при визуализации.
Теперь осталось только отрисовать текстуры с использованием эффекта. Делается это так:
В коде мы использовали DrawRect(), эта функция рисует прямоугольник, на которые наложена текстура RenderTexture. Это особенность приема, после визуализации в текстуру, нам необходимо как-то вывести её на экран для последующей обработки. В этом нам и помогает прямоугольник, который мы рисуем так, чтобы он занимал все экранное пространство. Код инициализации вершин и визуализации прямоугольника я приводить не буду, чтобы не раздувать статью еще больше. Скажу только, что все необходимые действия аналогичны тем, что мы проводили при инициализации частиц. Если у вас возникли трудности, то вы можете посмотреть как реализована эта функция в коде примера.
Эффекты используется так: сначала мы вызываем метод Begin(), получая количество проходов визуализации в эффекте. Затем перед каждым проходом вызываем BeginPass(i), а после EndPass(). И наконец после окончания визуализации мы вызываем метод End().
Вот что у нас получилось:
На этом статья заканчивается, всем спасибо за внимание. Буду рад ответить на возникшие у вас вопросы в комментариях.
Полный исходный код проекта доступен на GitHub. Внимание, для запуска скомпилированного примера необходимо установить VisualC++ Redistributable 2012
UPD
Для тех, кто считает, что D3D9 безнадежно устарел, или тем, кому просто хочется, чтобы все расчеты производились на GPU — имеется еще один пример, только уже на D3D10. Как обычно пример и скомпилированное демо доступны на GitHub. Расчеты на GPU прилагаются :)
В этой части статьи будет рассмотрено: работа со спрайтами, вершинные и пиксельные шейдеры, эффекты, пост-эффекты. В частности для реализации пост-эффекта — приём рендера в текстуру.
0. Базовые сведения
Спрайты
Спрайты представляют из себя текстуру, перемещаемую по экрану, и изображающую объект или часть объекта. Так как частицы в нашей системе представляют из себя всего лишь точки, то накладывая на них различные текстуры можно визуализовать любой объект (например облака). Поскольку спрайт это простая текстура, то необходимо иметь базовые представления о них.
Текстура вместо пикселей, как мы привыкли, имеет тексели (texel). Direct3D использует для текстур систему координат, образованную горизонтальной осью U и вертикальной осью V.
Вершинные шейдеры
Вершинные шейдеры это программа, которая создается на специальном языке HLSL (или ассемблере), и занимается преобразованием вершин и освещением. В вершинном шейдере мы можем взять положение вершины и переместить её в совсем другое место. В статье вершинные шейдер будет так же использоваться для генерации координат текстур.
Пиксельные шейдеры
Похожи на вершинные шейдеры, только вместо них они занимаются растеризацией изображения. В такой шейдер передаются данные о текстуре, цвете и много других, а на основании этого шейдер обязан вернуть цвет пикселя. Мы будем использовать их для текстурирования.
Эффекты и пост-эффекты
Эффекты включат пиксельные и\или вершинные шейдеры, и один или несколько проходов визуализации. С помощью них можно реализовать, например, эффекты размытия или свечения.
Пост-эффекты отличаются от обычных тем, что применяются к уже растрезированой сцене.
1. Текстурируем частицы
Перед тем как накладывать текстуру на частицы, необходимо изменить тип, который мы использовали для представления вершин в буфере на следующий:
struct VertexData
{
float x,y,z;
float u,v; // Храним коориднаты текстуры
};
Значения u и v, необходимо инициализовать нулем при создании.
Так же необходимо изменить флаги при создании буфера, и описание буфера:
device->CreateVertexBuffer(count*sizeof(VertexData), D3DUSAGE_WRITEONLY,
D3DFVF_XYZ | D3DFVF_TEX0, D3DPOOL_DEFAULT, &pVertexObject, NULL);
// ...
D3DVERTEXELEMENT9 decl[] =
{
{ 0, 0, D3DDECLTYPE_FLOAT3, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_POSITION, 0 },
{ 0, 12, D3DDECLTYPE_FLOAT2, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_TEXCOORD, 0 },
D3DDECL_END()
};
Добавляем флаг D3DFVF_TEX0, указывая, что мы будем хранить координаты текстуры. Так же добавляем строку в описание вершин.
А теперь осталось загрузить текстуру и изменить состояния рендера:
float pointSize = 5; // Размер частиц в единицах пространства вида
device->SetRenderState(D3DRS_POINTSIZE_MAX, *((DWORD*)&pointSize));
device->SetRenderState(D3DRS_POINTSIZE, *((DWORD*)&pointSize));
device->SetRenderState(D3DRS_LIGHTING,FALSE);
device->SetRenderState(D3DRS_POINTSPRITEENABLE, TRUE ); //Включаем рисование спрайтов поверх точек
device->SetTextureStageState(0, D3DTSS_ALPHAARG1, D3DTA_TEXTURE);
device->SetTextureStageState(0, D3DTSS_ALPHAOP, D3DTOP_SELECTARG1);
device->SetRenderState(D3DRS_ALPHABLENDENABLE, TRUE);
device->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_SRCALPHA);
device->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA);
device->SetRenderState(D3DRS_ZENABLE, FALSE);
Все состояния описывать не буду, информацию о них можно найти на MSDN. Скажу только, что часть из них нам понадобятся для эффектов.
IDirect3DTexture9 *particleTexture = NULL,
D3DXCreateTextureFromFile(device, L"particle.png", &particleTexture); //Создаем текстуру
device->SetTexture(0, particleTexture); //Устанавливаем текстуру
Загружаем текстуру и устанавилваем из файла, которая будет представлять частицу.
Все, теперь при запуске приложения вы увидите вместо простых точек текстурированные частицы, но мы пойдем дальше и добавим простой эффект к получившемуся изображению.
Результат визуализации:
2. Эффекты
Для разработки эффектов существует замечательная программа от NVIDIA, называется она Fx Composer. Поддерживается отладка шейдеров, шейдеры 4-ой версии, DIrect3D (9, 10) и OpenGL. Очень рекомендую, но в данной статье эта среда разработки рассматриваться не будет.
Для начала рассмотрим основную структуру эффектов:
Скрытый текст
float4x4 WorldViewProj; // Входной параметр. Матрица 4x4
//Входной параметр текстура
texture Base <
string UIName = "Base Texture";
string ResourceType = "2D";
>;
//Сэмплер, используется для выборки текселей
sampler2D BaseTexture = sampler_state {
Texture = <Base>;
AddressU = Wrap;
AddressV = Wrap;
};
//Структура, описывающая входные параметры для вершинного шейдера
struct VS_INPUT
{
float4 Position : POSITION0;
float2 Tex : TEXCOORD0;
};
//Структура для выходных параметров
struct VS_OUTPUT
{
float4 Position : POSITION0;
float2 Tex : TEXCOORD0;
};
// Вершинный шейдер
VS_OUTPUT mainVS(VS_INPUT Input)
{
VS_OUTPUT Output;
Output.Position = mul( Input.Position, WorldViewProj );
Output.Tex = Input.Tex;
return( Output );
}
// Пиксельный шейдер
float4 mainPS(float2 tex: TEXCOORD0) : COLOR
{
return tex2D(BaseTexture, tex);
}
// Описание "Техники"
technique technique0
{
//Описание прохода визуализации
pass p0
{
CullMode = None; // Устанавливаем состояние рендера
// Выолняем
VertexShader = compile vs_2_0 mainVS(); // вершинный шейдер
PixelShader = compile ps_2_0 mainPS(); // пиксельный шейдер
}
}
Как видно из кода каждый из шейдеров принимает и возвращает какое-либо значение. Вершинный шейдер обязан вернуть координаты вершины, а пиксельный цвет обрабатываемого пикселя.
Эффект разделяется на несколько техник. Каждая из техник может представлять свой способ применения эффектов, или же вообще другой эффект.
Каждая техника имеет в себе один или несколько проходов визуализации.
Настало время написать свой простой эффект, который, например будет окрашивать частицы в красный цвет:
Скрытый текст
float4x4 WorldViewProj; // Входной параметр. Матрица 4x4
//Входной параметр текстура (спрайт)
texture Base <
string UIName = "Base Texture";
string ResourceType = "2D";
>;
//Сэмплер, используется для выборки текселей
sampler2D BaseTexture = sampler_state {
Texture = <Base>;
AddressU = Wrap;
AddressV = Wrap;
};
//Структура, описывающая входные параметры для вершинного шейдера
struct VS_INPUT
{
float4 Position : POSITION0;
float2 Tex : TEXCOORD0;
};
//Структура для выходных параметров
struct VS_OUTPUT
{
float4 Position : POSITION0;
float2 Tex : TEXCOORD0;
};
// Вершинный шейдер
VS_OUTPUT mainVS(VS_INPUT Input)
{
VS_OUTPUT Output;
Output.Position = mul( Input.Position, WorldViewProj ); // Преобразуем координаты вершин в пространство вида
Output.Tex = Input.Tex; // Координаты текстуры мы не будем модифицировать
return( Output );
}
// Пиксельный шейдер
float4 mainPS(float2 tex: TEXCOORD0) : COLOR
{
return tex2D(BaseTexture, tex) * float4(1.0, 0, 0, 1.0); // Смешиваем цвет текстуры с красным
}
// Описание "Техники"
technique technique0
{
//Описание прохода визуализации
pass p0
{
CullMode = None; // Устанавливаем состояние рендера
// Выолняем
VertexShader = compile vs_2_0 mainVS(); // вершинный шейдер
PixelShader = compile ps_2_0 mainPS(); // пиксельный шейдер
}
}
Код этого эффекта мало отличается от базовой структуры, ранее рассмотренной нами. Мы добавили лишь смешивание с красным цветом методом умножение (Multiply Blend). Вот что у нас получилось:
Неплохо, но можно изменить режим наложения на другой, и сделать смешивание не с одним цветом, а с целой текстурой.
Для того, чтобы у нас получилось правильно смешать визуализацию частиц и текстуру, нам необходимо воспользоваться приемом, который называется Render Target (цель визуализации). Суть приема проста, мы визуализируем нашу сцену в текстуру, а потом уже накладываем эффекты на уже растрированное изображение.
Вот полный код эффекта реализующего это:
Скрытый текст
float4x4 WorldViewProj;
texture Base <
string UIName = "Base Texture";
string ResourceType = "2D";
>;
sampler2D BaseTexture = sampler_state {
Texture = <Base>;
AddressU = Wrap;
AddressV = Wrap;
};
texture Overlay <
string UIName = "Overlay Texture";
string ResourceType = "2D";
>;
sampler2D OverlayTexture = sampler_state {
Texture = <Overlay>;
AddressU = Wrap;
AddressV = Wrap;
};
// Текстура, которая будет использоваться для рендера
texture PreRender : RENDERCOLORTARGET
<
string Format = "X8R8G8B8" ;
>;
// И сэмплер для неё
sampler2D PreRenderSampler = sampler_state {
Texture = <PreRender>;
};
struct VS_INPUT
{
float4 Position : POSITION0;
float2 Tex : TEXCOORD0;
};
struct VS_OUTPUT
{
float4 Position : POSITION0;
float2 Tex : TEXCOORD0;
};
VS_OUTPUT cap_mainVS(VS_INPUT Input)
{
VS_OUTPUT Output;
Output.Position = mul( Input.Position, WorldViewProj );
Output.Tex = Input.Tex;
return( Output );
}
float4 cap_mainPS(float2 tex: TEXCOORD0) : COLOR
{
return tex2D(BaseTexture, tex);
}
///////////////////////////////////////////////////////
struct Overlay_VS_INPUT
{
float4 Position : POSITION0;
float2 Texture1 : TEXCOORD0;
};
struct Overlay_VS_OUTPUT
{
float4 Position : POSITION0;
float2 Texture1 : TEXCOORD0;
float2 Texture2 : TEXCOORD1;
};
vector blend(vector bottom, vector top)
{
//Linear light
float r = (top.r < 0.5)? (bottom.r + 2*top.r - 1) : (bottom.r + top.r);
float g = (top.g < 0.5)? (bottom.g + 2*top.g - 1) : (bottom.g + top.g);
float b = (top.b < 0.5)? (bottom.b + 2*top.b - 1) : (bottom.b + top.b);
return vector(r,g,b,bottom.a);
}
Overlay_VS_OUTPUT over_mainVS(Overlay_VS_INPUT Input)
{
Overlay_VS_OUTPUT Output;
Output.Position = mul( Input.Position, WorldViewProj );
Output.Texture1 = Input.Texture1;
Output.Texture2 = Output.Position.xy*float2(0.5,0.5) + float2(0.5,0.5); // преобразуем координаты вершины, в координаты текстуры
return( Output );
}
float4 over_mainPS(float2 tex :TEXCOORD0, float2 pos :TEXCOORD1) : COLOR
{
return blend(tex2D(OverlayTexture, pos), tex2D(PreRenderSampler, tex));
}
technique technique0
{
pass p0
{
CullMode = None;
VertexShader = compile vs_2_0 cap_mainVS();
PixelShader = compile ps_2_0 cap_mainPS();
}
pass p1
{
CullMode = None;
VertexShader = compile vs_2_0 over_mainVS();
PixelShader = compile ps_2_0 over_mainPS();
}
}
Как вы заметили, появился еще один этап визуализации. На первом этапе мы визуализируем частицы такими, какие они есть. Причем визуализацию мы должны будем выполнить в текстуру. А уже во втором проходе визуализации мы накладываем на изображение другую текстуру используя смешивание 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 прилагаются :)