Бесплатная реклама

Автор: Лев Симонов «LEVel»

Опубликовано: 24.02.2009

Изменено: 24.02.2009

Постоянная ссылка

Комментарии [4]

Моделирование жидкости в 2D с использованием GPU





Страницы: 1 2 3

8. Рендеринг

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

При работе над рендерингом я преследовал несколько целей: *Вода должна выглядеть как вода, а не кучка частиц *Цвет и прозрачность воды должны зависеть от цвета и прозрачности отдельных частиц *На ней должны быть блики *Она должна преломлять

Теперь обо всем по порядку. Я решил использовать для этого технику HWGI (Hardware Geometry Instancing), суть которой заключается в том, что помимо буфера вершин и буфера индексов в видеокарту передается так называемый инстанс-буффер, который содержит «личные» данные каждого отдельного объекта – в нашем случае частицы. Но чтобы записать эти данные в инстанс-буфер, нужно сначала достать их из текстуры частиц. Делается все это очень просто:

V( pd3dDevice->GetRenderTargetData(PosSurf, RTPosSurf));
if(SUCCEEDED(g_pPositionSys->LockRect(0,&rect, NULL, NULL)))
{
	float* data = (float*)rect.pBits;
	memcpy(data_m, data, cur_particle*4*sizeof(float));
	V( g_pPositionSys->UnlockRect(0));
}

QUAD_INSTANCEDATA *pIPos;
if( SUCCEEDED( g_pVBInstanceData->Lock( 0, NULL,  (void**) &pIPos, NULL) ) )
{
	for (int i = 0; i < cur_particle; i++)
	{
		pIPos[i].pos.x = data_m[i*4+0];
		pIPos[i].pos.y = data_m[i*4+1];
		pIPos[i].vel.x = data_m[i*4+2];
		pIPos[i].vel.y = data_m[i*4+3];

		float normAddr2Dx = i*1.0f/size;
		float normAddr2Dy = floor(normAddr2Dx)*1.0f/size;

		pIPos[i].UV.x = normAddr2Dx - floor(normAddr2Dx) + 0.5f/size;
		pIPos[i].UV.y = normAddr2Dy + 0.5f/size;
	}
	g_pVBInstanceData->Unlock();
}

Для каждой частицы указываются ее координаты, скорость и текстурные координаты, по которым можно будет получить ее цвет. Частицы рисуются группами по 4000, так как при тестировании на некоторых видеокартах возникали сложности с рисованием большего количества частиц за один вызов.

Теперь можно рисовать сами частицы. Делается это два раза в две разные текстуры: текстуру высоты и текстуру цвета. Текстура высоты, по сути, содержит данные о толщине водного слоя и нужна для вычисления нормалей, которые используются для рисования бликов и преломления задника. Такое разделение нужно по двум причинам. Во-первых, я хотел, чтобы цвет и прозрачность каждой частицы были уникальны, поэтому данные о высоте и цвете в одну текстуру просто не влезали. Во-вторых, при рисовании в текстуру высоты используется другой режим смешивания, что делает невозможным использование MRT для рисования обеих текстур за один проход.

При рисовании в текстуру цвета и текстуру высоты используется один и тот же вершинный шейдер:

VS_OUTPUT VS_HWInstancing( float4 vPos : POSITION,
					float2 vTex0 : TEXCOORD0,
					float2 vOffset : TEXCOORD1,
					float2 vVelosity : TEXCOORD2,
					float2 vColor : COLOR0)
{
	VS_OUTPUT Output;
	
	float vln = length(vVelosity)*0.02;
	vPos.x *= 1.0+vln;
	vPos.y *= 1.0-vln*0.1;
	vVelosity = normalize(vVelosity);
	float2x2 fMatrix = { vVelosity.x, vVelosity.y,
                     -vVelosity.y, vVelosity.x};    
                     
	vPos.xy = mul(vPos.xy, fMatrix);
	vPos.xy = vPos.xy*dotSize*dotSize_m + vOffset;

	Output.Position = mul(vPos, g_mWorldViewProjection);
	Output.TextureUV = vTex0;
	Output.Diffuse = float4(vColor,0,0);
	
	return Output;
}

Здесь координаты нашего универсального квада преобразуются так, чтобы частица оказалась в нужном месте и была повернута «вдоль» скорости. При этом частица немного растягивается в зависимости от того, с какой скоростью она движется, получается какое-то подобие motion-blur’a. Текстурные координаты, по которые можно извлечь цвет частицы просто транслируются в пиксельный шейдер.

Понятное дело, что частицы рисуются не как квадраты. Для частиц используется специальная черно-белая текстурка, на которой нарисовано что-то вроде радиального распределения гаусса:

текстурка

Прозрачность берется равной одной из компонент цвета (например, красной). Полученный из этой текстуры цвет умножается на цвет самой частицы и рисуется. Параметры смешивания на этом шаге устанавливаются следующим образом:

pd3dDevice->SetRenderState( D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA );
pd3dDevice->SetRenderState( D3DRS_SRCBLEND, D3DBLEND_ONE );

Шейдер

PS_OUTPUT RenderParticlesPS( VS_OUTPUT In ) 
{ 
    PS_OUTPUT Output;
    Output.RGBColor = tex2D(MeshTextureSampler, In.TextureUV)*0.5;
    Output.RGBColor.a = Output.RGBColor.r;
    Output.RGBColor *= tex2D(ColormapSampler, In.Diffuse.xy);	
    return Output;
}

Рендеринг в текстуру высоты идет с другими параметрами смешивания:

pd3dDevice->SetRenderState( D3DRS_DESTBLEND, D3DBLEND_ONE );
pd3dDevice->SetRenderState( D3DRS_SRCBLEND, D3DBLEND_SRCALPHA );

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

PS_OUTPUT RenderParticlesHeiPS( VS_OUTPUT In ) 
{ 
    PS_OUTPUT Output;
    float4 color = tex2D(MeshTextureSampler, In.TextureUV)*2.0f;
	Output.RGBColor.a = color.r;
	Output.RGBColor.xyz = color.r*19.0f/255.0f;
    return Output;
}

Карта цвета и карта высот
Рис. 14. Карта цвета и карта высот

Теперь у нас есть все необходимое, чтобы нарисовать нашу воду. Сначала мы динамически генерируем карту нормалей из полученной карты высот. Я просто взял этот кусочек из RenderMonkey с одним лишь отличием. Чтобы вода выглядела более гладкой, нормаль рассчитывается не по соседним текселям, а со сдвигом в 2 текселя:

А: оригинальный вариант; В: вариант со сдвигом в 2 текселя
Рис. 15. А: оригинальный вариант; В: вариант со сдвигом в 2 текселя

Чтобы это не вызывало побочных эффектов частицы рисуются в текстуру высоты с меньшим коэффициентом масштабирования, чем при рисовании в текстуру цвета.

Далее, когда у нас уже есть нормаль, рассчитывается спекуляр и преломления. Тут нет совершенно ничего необычного. Основной цвет берется из текстуры цвета. Смешивание проводится прямо в шейдере.

PS_OUTPUT PS_WaterFinal( float4 vPos : POSITION, float2 vTex0 : TEXCOORD0 )
{
	PS_OUTPUT Output;
	float4 base = tex2D(MeshTextureSampler, vTex0);
	float4 Back = base;
	Back.w *= 2;

	float offx = 2.0 / BBw;
	float offy = 2.0 / BBh;

	// Take all neighbor samples
	float s00 = tex2D(HeightMapSampler, vTex0 + float2(-offx, -offy)).x;
	float s01 = tex2D(HeightMapSampler, vTex0 + float2( 0,   -offy)).x;
	float s02 = tex2D(HeightMapSampler, vTex0 + float2( offx, -offy)).x;

	float s10 = tex2D(HeightMapSampler, vTex0 + float2(-offx,  0)).x;
	float s12 = tex2D(HeightMapSampler, vTex0 + float2( offx,  0)).x;

 	float s20 = tex2D(HeightMapSampler, vTex0 + float2(-offx,  offy)).x;
 	float s21 = tex2D(HeightMapSampler, vTex0 + float2( 0,    offy)).x;
 	float s22 = tex2D(HeightMapSampler, vTex0 + float2( offx,  offy)).x;
   
 	float4 sobelX = s00 + 2 * s10 + s20 - s02 - 2 * s12 - s22;
 	float4 sobelY = s00 + 2 * s01 + s02 - s20 - 2 * s21 - s22;

 	float sx = dot(sobelX, lightness);
 	float sy = dot(sobelY, lightness);

 	float3 normal = normalize(float3(sx, sy, 1)); // Сoбираем нормаль
 	float3 lVec = normalize(lightVec);
   	float diffuse = saturate(dot(lVec, normal));
   	float specular = pow(saturate(dot(reflect(-viewVec, normal), lVec)), 16);

   	float3 refr_v = refract(-viewVec, normal, refr);

   	float4 backColor = tex2D(BackSampler, vTex0 + refr_v.xy*0.04);
   	Output.RGBColor = (diffuse * Back * 0.5 + specular*0.95) + 0.25 * Back;

   	//смешиваем воду с задником
   	Output.RGBColor.xyz = Output.RGBColor.xyz*Output.RGBColor.w + 
	backColor*(1 - Output.RGBColor.w);
   	Output.RGBColor.w = 1.0f;

    return Output;
}

Все, теперь можно любоваться результатами :)

Скриншот
Рис. 16. Скриншот

9. Демонстрация (исходный код)

К статье прилагается демонстрационная программа с исходниками, выполненная в стиле примеров из DX SDK.

Исходный код можно скачать здесь: gpu_water.zip

Демонстрационная программа
Рис. 17. Демонстрационная программа

Интерфейс позволяет приостановить симуляцию (pause); запустить ее заново (restart); изменить количество обрабатываемых частиц (max particles number), новые частицы при этом «подкачиваются» через эмиттер; изменить размер эмиттера (emitter size) и амплитуду его колебаний (emitter magnitude); силу действия аттрактора (attractor force). Есть возможность включения геометрии (bounding geometry); режима рисования частиц (draw as particles), отключающего «водный» рендеринг; и режима нулевой гравитации (zero gravity). Цвет воды, которая в данный момент выливается их эмиттера можно задать ползунками Water color, а скорость диффузии – ползунком diffusion.

Управление производится мышью, левая кнопка активирует отталкивающий эмиттер, левая кнопка с ctrl – притягивающий эмиттер, а правая кнопка + движение мышью – глобальное воздействие на частицы.

Для работы демонстрационной программы требуется видеокарта, поддерживающая шейдеры версии 3.0. Для комфортной обработки большого количества частиц желательно иметь видеокарту класса GF8800 или выше.

Анализ производительности реализации, проведенный с помощью NVPerfHUD показал, что при обработке 65536 частиц самыми «тяжелыми» для GPU задачами являются (в порядке убывания): Расчет сил (28.6 мс), расчет диффузии (24.0 мс), расчет давления (17.7 мс), сортировка хэшей (15.9 мс), рендеринг карты цвета (9.1 мс) и рендеринг карты высот (4.4 мс). Время выполнения различных этапов работы программы было замерено на следующей системе: C2D E6550 2.33GHz, 2 GB, GeForce 7600GT.

Производительность отдельных блоков
Рис. 18. Производительность отдельных блоков, по данным NVPerfHUD

Unit Utilization Graph
Рис. 19. Unit Utilization Graph

Ролик, демонстрирующий работу программы. Там же можно найти пару роликов из предыдущих версий.

10. Приложение. Сортировка методом bitonic merge sort на GPU

Сразу скажу, что я видел две реализации этого алгоритма на GPU, однако я все равно решил изобретать свой велосипед. Во-первых, потому что многое не понял в найденных исходниках. Во-вторых, чтобы глубже разобраться в вопросе. К чему я в конечном итоге пришел – опишу тут.

Для начала пара слов о сути алгоритма. Здесь я опускаю теоретическое обоснование сказанного, наша цель – практическая реализация. Сортировка массива проходит за log(n) стадий (stage), где n – количество элементов, каждая из которых содержит в свою очередь s+1 проходов (pass), где s – номер стадии. Проходы удобно нумеровать в порядке убывания, начиная с s.

bitonic merge sort
Рис. 20. bitonic merge sort для массива из 8 элементов

Стрелка обозначает, какие элементы нужно сравнить. Например, если стрелка идет от элемента A к элементу B, то это значит, что мы поменяем A и B местами, если A > B. Заметьте, что в рамках одного прохода длина стрелки не меняется – то есть сравниваемые элементы всегда находятся на одном расстоянии. Позже мы этим воспользуемся.

В отличие от реализаций, которые я встречал, я решил действовать по принципу один проход = один квад. Все, что для этого нужно – имея текстурные координаты данного конкретного текселя узнать, с каким текселем его надо сравнить и как именно нужно сравнить – A > B или B > A.

Займемся решением этой задачи. Первое, что нужно сделать – преобразовать текстурные координаты в индекс текущего элемента. Делается это элементарно:

ModUV.xy = UV*float2(RTW,RTH);
float id = ModUV.y*RTW + ModUV.x;

Теперь нужно узнать, с каким элементом нужно сравнить текущий элемент. Как уже было замечено ранее, расстояние до нужного элемента постоянно и равно 2^p, поэтому нужно только узнать, в каком направлении нужно сдвинуться – вперед или назад. Если присмотреться, то можно заметить, что эти направления чередуются с шагом, равным 2^p, где p – номер прохода:

Чередование направления сравнения
Рис. 21. Чередование направления сравненияв

Аналогично чередуется и знак сравнения (A > B или A < B), только с шагом 2^s. Коэффициенты для направления и сравнения, которые могут принимать значение 1 или -1, можно легко получить из индекса частицы, достаточно поделить его на шаг и взять остаток от деления на 2, после чего преобразовать 0 в -1, а 1 в 1:

ModUV.zw = float2(id*invsgn_sz,id*invdir_sz);
float2 fvec = f11 - fmod(floor(ModUV.zw), 2.0f)*2.0f;

Найденное нами направление сравнения – это еще не совсем то, что нам нужно. Его можно сравнить с направлением стрелок на рисунке, а нам нужно знать, как нужно сравнивать текущий элемент с парным ему. Для этого достаточно просто перемножить коэффициенты направления и сравнения. Умножив коэффициент направления на шаг, получим величину, на которую нужно сдвинуться относительно текущих текстурных координат, чтобы попасть на нужный элемент. Так как мы имеем дело с квадратной текстурой, нужно проверить, не вышли ли мы за край и, если это произошло, скорректировать текстурные координаты. Делается такая корректировка двумя строчками. Используя полученный сдвиг, найдем координаты партнера:

float istep = (dir_sz*invTexSize);

float2 dUV;
dUV.x = modf(istep, dUV.y);
dUV.y *= invTexSize;

float2 nextUV = TextureUV + dUV*fvec.y;

Теперь, когда мы знаем все необходимое, можно приступать к сравнению. Сортируем мы по x-компоненте – и у текущего элемента и у партнера ее нужно умножить на коэффициент сравнения, а затем сравнить полученные числа:

float4 self = tex2D(HashSampler, TextureUV);
float4 partner = tex2D(HashSampler, nextUV);

float2 key = float2(self.x,partner.x)*fvec.x;

if (key.x < key.y)
	Output.RGBColor = self;
else
	Output.RGBColor = partner;
return Output;

Кое-что можно перенести в вершинный шейдер. Если собрать все вместе, то получится примерно так:

void SAQuadSortVS(	float4 Pos : POSITION,
					float2 UV : TEXCOORD0,
					out float4 Position   : POSITION0,
					out float4 ModUV   : NORMAL0,
					out float2 TextureUV  : TEXCOORD0)
{
	Pos.y = Pos.y*part + 1.0f - part;
	UV.y *= part;

    	Position = Pos;
	TextureUV = UV + float2(0.5,0.5)*invTexSize;
	ModUV.xy = UV*float2(RTW,RTH);
	float id = ModUV.y*RTW + ModUV.x;
	ModUV.zw = float2(id*invsgn_sz,id*invdir_sz);
}

PS_OUTPUT BitonalSortPS ( float4 ModUV   : NORMAL0,
						 float2 TextureUV : TEXCOORD0 )
{
	PS_OUTPUT Output;

	float2 fvec = f11 - fmod(floor(ModUV.zw), 2.0f)*2.0f;
	fvec.x *= fvec.y;

	float istep = (dir_sz*invTexSize);

	float2 dUV;
	dUV.x = modf(istep, dUV.y);
	dUV.y *= invTexSize;

	float2 nextUV = TextureUV + dUV*fvec.y;

	float4 self = tex2D(HashSampler, TextureUV);
	float4 partner = tex2D(HashSampler, nextUV);

	float2 key = float2(self.x,partner.x)*fvec.x;

	if (key.x < key.y)
		Output.RGBColor = self;
	else
		Output.RGBColor = partner;
	return Output;
}

Все эти телодвижения проделываются много раз, при этом текстура преобразуется одним и тем же шейдером, но с разными коэффициентами p и s. Используется две текстуры, которые по очереди становятся рендертаргетом (обычный пинг-понг). Код, ответственный за сортировку выглядит так:

int sgn_sz = 1;
int dir_sz = 1;

int smax = log2(size*size*part);
bool sping = true;

V( g_pPhysics->SetTechnique( "BitonalSort" ) );
V( g_pPhysics->SetInt(pHanldeArr[h_p_RTW],size));
V( g_pPhysics->SetInt(pHanldeArr[h_p_RTH],size));

V( g_pPhysics->Begin( NULL, 0 ) );
for (int s = 0; s < smax; s++)
{
	sgn_sz *= 2;
	dir_sz = sgn_sz;
	for (int p = s; p >= 0; p--)
	{
		dir_sz /= 2;
		IDirect3DSurface9* TmpSurf = NULL;
		if (sping)
			g_pHash2->GetSurfaceLevel(0,&TmpSurf);
		else
			g_pHash1->GetSurfaceLevel(0,&TmpSurf);
		V( pd3dDevice->SetRenderTarget( 0, TmpSurf ));
		SAFE_RELEASE(TmpSurf);

		if (sping)
			g_pPhysics->SetTexture( pHanldeArr[h_p_g_Hash], g_pHash1 );
		else
			g_pPhysics->SetTexture( pHanldeArr[h_p_g_Hash], g_pHash2 );

		g_pPhysics->SetInt(pHanldeArr[h_p_dir_sz],dir_sz);
		g_pPhysics->SetInt(pHanldeArr[h_p_sgn_sz],sgn_sz);

		g_pPhysics->Setfloat(pHanldeArr[h_p_invdir_sz],1.0f/dir_sz);
		g_pPhysics->Setfloat(pHanldeArr[h_p_invsgn_sz],1.0f/sgn_sz);

		V( g_pPhysics->CommitChanges());
		V( g_pPhysics->BeginPass(0));
		V( pd3dDevice->DrawIndexedPrimitive(D3DPT_TRIANGLELIST,0,0,4,0,2));
		V( g_pPhysics->EndPass());

		sping = !sping;
	}
}
V( g_pPhysics->End());

Спасибо за внимание :)

 


 

Статья участвовала в конкурсе статей по программированию #3 (2008).

 

Страницы: 1 2 3