XNA Draw: улучшаем графику игры

в 6:34, , рубрики: bloom, effects, game development, motion blur, shaders, xna, метки: , , , , , ,

XNA Draw: улучшаем графику игры

Всем привет.

Все мои восемь статьей на хабре — статьи о геймдеве, большая часть из них связанна с таким замечательным фреймворком, как XNA. Первым знакомством с XNA была статья о создании музыкальной игрушки, потом сложность статей нарастала, я начал писать об системах частиц, а затем о шейдерах и еще шейдерах.

В целом — на шейдерах я и хотел закончить, однако, стоить немного дополнить их, я расскажу о нескольких алгоритмах для улучшения графике в игре. Примеры улучшений:

Если интересно — под хабракат.

Введение

Затронем мы опять 2D составляющую игр и будем работать только с пиксельными шейдерами.

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

Рассмотрим все эффекты, которые использовались в игре и пару эффектов, которые там отсутствовали, видео довольно старое, а игра уже потерпела качественные изменения с тех пор.

Distortion (искажение)

Самый первый эффект, который бросается в глаза — искажения:
XNA Draw: улучшаем графику игры

Этот эффект не обрабатывается специальным шейдером, в целом, он такой же, какой и в статье, которую я писал ранее.

Давайте разберем тот эффект взрыва.

Тут используются следующие текстуры:
XNA Draw: улучшаем графику игры

В самом верхнем левом углу — текстура дыма, дым образует «кольца» вокруг взрыва, яркую область в взрыве, которая быстро гаснет и шлейф от ракеты. Является основной текстурой этого эффекта. Используется как видимая текстура и искажающая текстура.

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

Ну и для дополнительной красоты — частицы от взрыва, самая большая текстура на стрипе. Но тут стоит отметить, что игрок её никогда не увидит на прямую. Она является искажающей и довольно быстро исчезает.

P.S. пару советов для новичков, делающие систему частиц:

  • Никогда не создавайте частицы в процессе игры, используйте паттерн Pool Object.
  • Никогда не создавайте свойство «текстура» у класса частицы. Используйте enum и единую текустуру (стрип), где лежат все текстуры ваших частиц. Т.к. если в процессе прорисовки частиц вы постоянно меняете состояние графического устройства (меняете текстуру, например) — не ждите высоких скоростей от вашей системы.
  • Правило: «Чем больше частиц, тем красивее» — работает далеко не всегда.

HUD Distortion (искажение интерфейса)

Второй эффект, который можно было заметь в ролике — искажение интерфейса.
На этот эффект меня вдохновили такие игры, как, например, Crysis 2 и Deus Ex: Human revolution. Там, при гибели — интерфейсы начинали разнообразно искажаться. Мне это показалось интересным. Так же, я еще усилил искажения интерфейса при простом попадании.

К примеру, гибель игрока:
XNA Draw: улучшаем графику игры

Этот шейдер очень похож на тот, что был раньше — искажение изображения. Однако, в корню отличается от него. Искажения происходят не от карты искажений, а от математических формул, давайте рассмотрим код шейдер (шейдер максимально упрощен для понимания):

float force; // сила "тряски" интерфейса
float timer; // некотое значение, которое постоянно инкриментиуется.
float random1; // случаное число
float random2; // случаное число
float initialization; // временная шкала "инициализации" интерфейса
float desaturation_float; // степень потери цвета

sampler TextureSampler : register(s0);

float4 main(float4 color : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0
{
	texCoord.y += cos(texCoord.x) * timer * 0.0002f * force; // тряска
	
	if(initialization > 0)
	{
		texCoord.x += cos(texCoord.y) * initialization; // инициализация
	}

	if(texCoord.y > random1 && texCoord.y < random2) // искажения
	{
		float moving = force;
		if(timer > 100) moving *= -1.0;

		texCoord.x += timer / 5000.0 * moving * random2;
		color *= 1 + random2 * force;
	}

	if(timer < 20 && force > 0.3) // искажения цветов
	{
		color.b = random2;
		color.g = random1;
	}

	if(timer > 50) // эффект "голограммы"
	{
		color *= 1 + random1/3 * (1 + force);
	}

	float4 source = tex2D(TextureSampler, texCoord);

	float4 sourceR = tex2D(TextureSampler, texCoord + float2(0.01*force*random1, 0));
	sourceR.g = 0;
	sourceR.b = 0;

	float4 sourceB = tex2D(TextureSampler, texCoord - float2(0.01*force*force*random2, 0));
	sourceB.r = 0;
	sourceB.g = 0;

	float4 sourceG = tex2D(TextureSampler, texCoord - float2(0.01*force*((random1+random2) / 2), 0));
	sourceG.r = 0;
	sourceG.b = 0;

	float4 output = (sourceR+sourceB+sourceG);
	output.a = source.a;

	float greyscale = dot(output.rgb, float3(0.3, 0.59, 0.11));
	output.rgb = lerp(greyscale, output.rgb, 1.0 - desaturation_float);

    return color * output;
}

technique HUDDisplacer
{
    pass DefaultPass
    {
        PixelShader = compile ps_2_0 main();
    }
}

Код довольно прост, а сам по себе шейдер смотрится эффективно.

Static texture to dynamic texture

Создание «живых» текстур из менее живых. К примеру, мало кто заметил — мерцания далеких звезд в ролике выше. Хотя, сама текстура — является статической.

Рассмотрим этот шейдер:

float modifer; // случаное число

sampler TextureSampler : register(s0);

float4 main(float4 color : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0
{	
	float4 output = tex2D(TextureSampler, texCoord);

	float greyscale = dot(output.rgb, float3(0.3, 0.59, 0.11));

	if(greyscale > 0.2)
	{
		color *= 1 + (modifer*greyscale / 1.5);
		if(greyscale > 0.8)
		{ color *= 1 + (modifer*2); }
	}

    return color * output;
}

technique BackgroundShader
{
    pass DefaultPass
    {
        PixelShader = compile ps_2_0 main();
    }
}

Если яркость пикселя (grayscale) больше 20% — создается легкое мерцание, если больше 80% — сильное.
На картинке этого показать нельзя, все видно в ролике.

Ну и рассмотрим еще два эффекта, которых в ролике нет и реализованы они в более новых версиях.

Bloom (эффект свечения)

Эффект свечения знаком всем, так же именуется как Bloom (блюм).
Идея проста, извлекаем из изображения яркие области (введен некоторых порог), затем рисуем нашу сцену и поверх размытую сцену. Яркие области начинают светиться.

Примеры в картинках:
Яркость сцены:
XNA Draw: улучшаем графику игры

Оригинальная сцена:
XNA Draw: улучшаем графику игры

Готовая сцена:
XNA Draw: улучшаем графику игры

Рассмотрим код шейдера, он состоит из двух частей, шейдер который извлекает яркость и шейдер который формирует финальное изображение.
Листинг шейдера, который извлекает яркость:

sampler TextureSampler : register(s0);

float4 main(float2 texCoord : TEXCOORD0) : COLOR0 
{ 
  float4 c = tex2D(TextureSampler, texCoord); 
  float BloomThreshold = 0.1; 

  return saturate((c - BloomThreshold) / (1 - BloomThreshold)); 
}

technique ThresholdEffect
{
    pass DefaultPass
    {
        PixelShader = compile ps_2_0 main();
    }
}

Листинг шейдера, который дает финальный результат:

texture bloomMap;

sampler TextureSampler : register(s0);

sampler BloomSampler : samplerState
{
	Texture = bloomMap;
	MinFilter = Linear;
	MagFilter = Linear;
	AddressU = Clamp;
	AddressV = Clamp;
}; 

// Псевдо-гауусово размытие
const float2 offsets[12] = { 
 -0.326212, -0.405805, 
 -0.840144, -0.073580, 
 -0.695914,   0.457137, 
 -0.203345,   0.620716, 
  0.962340, -0.194983, 
  0.473434, -0.480026, 
  0.519456,   0.767022, 
  0.185461, -0.893124, 
  0.507431,   0.064425, 
  0.896420,   0.412458, 
 -0.321940, -0.932615, 
 -0.791559, -0.597705, 
}; 

float4 AdjustSaturation(float4 color, float saturation) { 
  float grey = dot(color, float3(0.3, 0.59, 0.11)); 
  
  return lerp(grey, color, saturation); 
} 

float4 main(float4 color : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0
{
  float BlurPower = 0.01; // 0.01
  float BaseIntensity = 1;
  float BloomIntensity = 0.4; // 0.4
  float BaseSaturation = 1;
  float BloomSaturation = 1;

  float4 original = tex2D(TextureSampler, texCoord); 
  
  // размытие
  float4 sum = tex2D(BloomSampler, texCoord); 
  for(int i = 0; i < 12; i++){ 
    sum += tex2D(BloomSampler, texCoord + BlurPower * offsets[i]); 
  } 
  sum /= 13; 
  
  original = AdjustSaturation(original, BaseSaturation) * BaseIntensity; 
  sum = AdjustSaturation(sum, BloomSaturation) * BloomIntensity; 
  
  return sum + original; 
}

technique BloomEffect
{
    pass DefaultPass
    {
        PixelShader = compile ps_2_0 main();
    }
}

Motion Blur (размытие в движении)

Ну и последний эффект, это размытие в движении (motion blur), в совокупности с другими эффектами — придает им «мягкость», резко двигаем мышкой и:

XNA Draw: улучшаем графику игры

Реализуется он тоже, довольно просто:

float rad = direction_move.x;
float xOffset = cos(rad);
float yOffset = sin(rad);
 
for(int idx=0; idx<15; idx++)
{
	texCoord.x = texCoord.x - 0.001 * xOffset * direction_move.y;
	texCoord.y = texCoord.y - 0.001 * yOffset * direction_move.y;
	c += tex2D(TextureSampler, texCoord);
}
c /= 15;

Где direction_move — вектор движения.

Заключение

С помощью таких вот вещей — можно придать своей игре большую «изюминку», причем такие вещи делаются довольно просто.

На этом, я думаю, «курс» по 2D играм окончен, через некоторое время — начну писать о создании трехмерных игр.

P.S. создание этой игры (та что в ролике) — скорее всего так и останется на этой стадии. Мне не хватает ресурсов (времени, энтузиазма, вдохновения), работать над таким большим проектом в одиночку — самоубийство.

P.S.S. огромная просьба, об очепятках/ошибках писать мне личным сообщением, не стоит писать комментарии без полезной смысловой нагрузки.

Вам же желаю успехов!

Автор: ForhaxeD

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js