Этот пост является 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 прилагаются :)