Всем привет.
Все мои восемь статьей на хабре — статьи о геймдеве, большая часть из них связана с таким замечательным фреймворком, как XNA. Первым знакомством с XNA была статья о создании музыкальной игрушки, потом сложность статей нарастала, я начал писать об системах частиц, а затем о шейдерах и еще шейдерах.
В целом — на шейдерах я и хотел закончить, однако, стоить немного дополнить их, я расскажу о нескольких алгоритмах для улучшения графики в игре. Примеры улучшений:
Если интересно — под хабракат.
Введение
Затронем мы опять 2D составляющую игр и будем работать только с пиксельными шейдерами.
Эта статья будет немного отличаться от других моих, тут сразу будет идти и теория и практика, подразумевается, что вы уже читали статьи о шейдерах тут и еще тут.
Рассмотрим все эффекты, которые использовались в игре и пару эффектов, которые там отсутствовали, видео довольно старое, а игра уже потерпела качественные изменения с тех пор.
Distortion (искажение)
Самый первый эффект, который бросается в глаза — искажения:
Этот эффект не обрабатывается специальным шейдером, в целом, он такой же, какой и в статье, которую я писал ранее.
Давайте разберем тот эффект взрыва.
Тут используются следующие текстуры:
В самом верхнем левом углу — текстура дыма, дым образует «кольца» вокруг взрыва, яркую область в взрыве, которая быстро гаснет и шлейф от ракеты. Является основной текстурой этого эффекта. Используется как видимая текстура и искажающая текстура.
Далее — точка, рядом с дымом, благодаря ей — есть молнии, которые исходят из центра взрыва. Так же, используется как видимая текстура и искажающая текстура.
Ну и для дополнительной красоты — частицы от взрыва, самая большая текстура на стрипе. Но тут стоит отметить, что игрок её никогда не увидит на прямую. Она является искажающей и довольно быстро исчезает.
P.S. пару советов для новичков, делающих систему частиц:
- Никогда не создавайте частицы в процессе игры, используйте паттерн Pool Object.
- Никогда не создавайте свойство «текстура» у класса частицы. Используйте enum и единую текустуру (стрип), где лежат все текстуры ваших частиц. Т.к. если в процессе прорисовки частиц вы постоянно меняете состояние графического устройства (меняете текстуру, например) — не ждите высоких скоростей от вашей системы.
- Правило: «Чем больше частиц, тем красивее» — работает далеко не всегда.
HUD Distortion (искажение интерфейса)
Второй эффект, который можно было заметь в ролике — искажение интерфейса.
На этот эффект меня вдохновили такие игры, как, например, Crysis 2 и Deus Ex: Human revolution. Там, при гибели — интерфейсы начинали разнообразно искажаться. Мне это показалось интересным. Так же, я еще усилил искажения интерфейса при простом попадании.
К примеру, гибель игрока:
Этот шейдер очень похож на тот, что был раньше — искажение изображения. Однако, в корню отличается от него. Искажения происходят не от карты искажений, а от математических формул, давайте рассмотрим код шейдер (шейдер максимально упрощен для понимания):
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 (блюм).
Идея проста, извлекаем из изображения яркие области (введен некоторых порог), затем рисуем нашу сцену и поверх размытую сцену. Яркие области начинают светиться.
Примеры в картинках:
Яркость сцены:
Оригинальная сцена:
Готовая сцена:
Рассмотрим код шейдера, он состоит из двух частей, шейдер который извлекает яркость и шейдер который формирует финальное изображение.
Листинг шейдера, который извлекает яркость:
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), в совокупности с другими эффектами — придает им «мягкость», резко двигаем мышкой и:
Реализуется он тоже, довольно просто:
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. огромная просьба, об очепятках/ошибках писать мне личным сообщением, не стоит писать комментарии без полезной смысловой нагрузки.
P.S.S.S. буду рад новым контактам ;-)
Вам же желаю успехов!