Pull to refresh

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

Reading time 10 min
Views 13K
Этот пост является 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 прилагаются :)
Tags:
Hubs:
+38
Comments 40
Comments Comments 40

Articles