
Всем привет.
Все мои восемь статьей на хабре — статьи о геймдеве, большая часть из них связана с таким замечательным фреймворком, как 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. буду рад новым контактам ;-)
Вам же желаю успехов!
