Разработка системы частиц на платформе DirectX 9. Часть II

Источник: habrahabr
A1ex

Этот пост является 2-ой и последней частью статьи о разработки системы частиц на DirectX 9. Если вы еще не читали первую часть, то рекомендую с ней ознакомится.

В этой части статьи будет рассмотрено: работа со спрайтами, вершинные и пиксельные шейдеры, эффекты, пост-эффекты. В частности для реализации пост-эффекта приём рендера в текстуру.

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


Страница сайта http://test.interface.ru
Оригинал находится по адресу http://test.interface.ru/home.asp?artId=30938