Делаем простое освещение в 2D игре. Подробные примеры на C# и XNA для новичков

в 13:40, , рубрики: 2d игры, game development, xna, освещение

В этой публикации я постараюсь предельно просто рассказать, как можно легко и быстро сделать динамическое освещение в 2D игре на XNA; без шейдеров, карт нормалей и вообще, без дополнительных ресурсов. Целью у нас будет красивая игровая сцена с ночным небом, темным задним фоном, освещенным фонарями, передним фоном и игровым меню. Количество источников света не ограничено (в разумных пределах, конечно), форма произвольная. Освещается только передний фон. Читателю желательно иметь элементарные навыки работы с XNA, поскольку будет много кода.

2d освещение

Несколько слов в качестве вступления. Я перерыл гору статей о том, как можно сделать динамическое освещение в 2D игре, и для меня стало откровением, что несмотря на обилие материала, большинство из них попросту бесполезны, т.к. я элементарно не могу повторить в коде то, о чем там пишут. Есть статьи с примерами в несколько строк кода на псевдокоде. Для новичка они практически бесполезные. Есть работающие примеры с шейдерами, которые можно даже скачать и запустить. Горы кода, 2 источника света и ни слова о том, как добавить еще. Да и сама тема шейдеров для простенькой 2D игры — это явный перебор. Вот так и родилась идея этой статьи: дать возможность читателю за 30 минут добавить в свою игру приличное динамическое освещение, тем самым сэкономив кучу времени и нервов.

Итак, поехали. Для начала немного теории и простых примеров. Нарисуем несколько квадратов на сером фоне:

Квадраты на сером фоне

GraphicsDevice.Clear(Color.Black);

spriteBatch.Begin();
spriteBatch.Draw(box1, Vector2.Zero, Color.White);
spriteBatch.Draw(box2, new Vector2(40, 50), Color.White);
spriteBatch.Draw(box4, new Vector2(150, 50), Color.White);
spriteBatch.Draw(box3, new Vector2(260, 50), Color.White);
spriteBatch.End();

Здесь мы заливаем экран черным цветом. Потом рисуем серый прямоугольник box1, и поверху 3 квадрата: box2, box3 и box4. Теперь попробуем немного изменить параметры метода spriteBatch.Begin(), а именно:

spriteBatch.Begin(SpriteSortMode.BackToFront, blendState);

Переменную blendState мы объявляем так:

var blendState = BlendState.Additive;

Или так. Что одно и то же:

var blendState = new BlendState();

blendState.AlphaBlendFunction = BlendFunction.Add;
blendState.AlphaDestinationBlend = Blend.One;
blendState.AlphaSourceBlend = Blend.SourceAlpha;
blendState.BlendFactor = Color.White;
blendState.ColorBlendFunction = BlendFunction.Add;
blendState.ColorDestinationBlend = Blend.One;
blendState.ColorSourceBlend = Blend.SourceAlpha;
blendState.ColorWriteChannels = ColorWriteChannels.All;
blendState.ColorWriteChannels1 = ColorWriteChannels.All;
blendState.ColorWriteChannels2 = ColorWriteChannels.All;
blendState.ColorWriteChannels3 = ColorWriteChannels.All;
blendState.MultiSampleMask = -1;            

Получится такой результат:

Квадраты на сером фоне

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

  • (source * sourceBlendFactor) blendFunction (destination * destinationBlendFactor)
  • (box2.RGB * BlendState.ColorSourceBlend) BlendFunction.Add (box1.RGB * BlendState.ColorDestinationBlend)
  • (box2.RGB * Blend.SourceAlpha) + (box1.RGB * BlendState.One)
  • (box2.RGB) + (box1.RGB) (если картинки не прозрачны)

Другими словами, если у вас есть 2 пикселя с одинаковыми координатами и цветами: R:10 G:20 B:255 и R:1 G:2 B:255, то результирующий пиксель выйдет с цветом R:11 G:22 B:255, т.е. станет светлее. Из этого можно сделать вывод, что играя настройками класса BlendState, можно затенять и засвечивать отдельные области спрайтов, тем самым получая желаемы эффект 2D освещения. Чтобы немного засветить круглую область на спрайте, нужно нарисовать круглый темно-серый спрайт поверху. Чтобы, наоборот, сделать круглую область темнее, нужно опять же нарисовать круглый темно-серый спрайт, но использовать функцию BlendFunction.ReverseSubtract.

На этом с теорией все, переходим к практике. Чтобы получить красивую сцену, как на первой картинке, будем делать следующее:

  1. Готовим спрайт с задним планом. Для этого весь задний план рисуем на одном спрайте (RenderTarget2D);
  2. Готовим спрайт с передним планом;
  3. Готовим спрайт с тенями. Для этого берем спрайт со второго шага и в точках источников света рисуем черные круглые спрайты — это области, которые не будут затеняться.

спрайт с тенями

Теперь всё это выводим на экран:

  1. Рисуем ночное небо;
  2. Рисуем спрайт с задним планом;
  3. Еще раз рисуем спрайт с задним планом, но с коэффициентом -0.9 (см. код). Получим темный задний план;
  4. Рисуем передний план;
  5. Рисуем спрайт с тенями, коэффициент -0.9;
  6. Рисуем меню.

Результат:

2d освещение

И, собственно готовый код:

// 1. Готовим спрайт с задним планом.

GraphicsDevice.SetRenderTarget(backgroundSprite);
GraphicsDevice.Clear(Color.Transparent);

spriteBatch.Begin();
spriteBatch.Draw(background, Vector2.Zero, Color.White);
spriteBatch.End();

// 2. Готовим спрайт с передним планом.

GraphicsDevice.SetRenderTarget(foregroundSprite);
GraphicsDevice.Clear(Color.Transparent);

spriteBatch.Begin();
spriteBatch.Draw(foreground, Vector2.Zero, Color.White);
spriteBatch.End();

// 3. Готовим спрайт с тенями.

GraphicsDevice.SetRenderTarget(foregroundShadow);
GraphicsDevice.Clear(Color.Black);

spriteBatch.Begin();
spriteBatch.Draw(foregroundSprite, Vector2.Zero, Color.White);
spriteBatch.Draw(light, new Vector2(620, 490), null, Color.White, 0.0f, new Vector2(light.Width / 2, light.Height / 2), 1.0f, SpriteEffects.None, 0.0f);
spriteBatch.Draw(light, new Vector2(100, 500), null, Color.White, 0.0f, new Vector2(light.Width / 2, light.Height / 2), 1.0f, SpriteEffects.None, 0.0f);
spriteBatch.Draw(light, new Vector2(620, 90), null, Color.White, 0.0f, new Vector2(light.Width / 2, light.Height / 2), 1.0f, SpriteEffects.None, 0.0f);
spriteBatch.Draw(light, new Vector2(290, 270), null, Color.White, 0.0f, new Vector2(light.Width / 2, light.Height / 2), 1.0f, SpriteEffects.None, 0.0f);
spriteBatch.Draw(light, new Vector2(400, 270), null, Color.White, 0.0f, new Vector2(light.Width / 2, light.Height / 2), 1.0f, SpriteEffects.None, 0.0f);
spriteBatch.Draw(light, new Vector2(510, 270), null, Color.White, 0.0f, new Vector2(light.Width / 2, light.Height / 2), 1.0f, SpriteEffects.None, 0.0f);
spriteBatch.End();

// Теперь это все выводим на экран.

GraphicsDevice.SetRenderTarget(null);
GraphicsDevice.Clear(Color.Black);

// 1. Рисуем ночное небо.
// 2. Рисуем спрайт с задним планом.

spriteBatch.Begin();
spriteBatch.Draw(sky, Vector2.Zero, Color.White);
spriteBatch.Draw(backgroundSprite, Vector2.Zero, Color.White);
spriteBatch.End();

// Готовим обьект BlendState.

var blendState = new BlendState();

blendState.AlphaBlendFunction = BlendFunction.ReverseSubtract;
blendState.AlphaDestinationBlend = Blend.One;
blendState.AlphaSourceBlend = Blend.BlendFactor;

// Тот самый загадочный коэффициент -0.9 (255 * 0.9 = 230, BlendFunction.ReverseSubtract = -1)

{
blendState.BlendFactor = new Color(230, 230, 230, 255);
blendState.ColorBlendFunction = BlendFunction.ReverseSubtract;
}

blendState.ColorDestinationBlend = Blend.One;
blendState.ColorSourceBlend = Blend.BlendFactor;
blendState.ColorWriteChannels = ColorWriteChannels.All;
blendState.ColorWriteChannels1 = ColorWriteChannels.All;
blendState.ColorWriteChannels2 = ColorWriteChannels.All;
blendState.ColorWriteChannels3 = ColorWriteChannels.All;
blendState.MultiSampleMask = -1;

// 3. Еще раз рисуем спрайт с задним планом, но с коэффициентом -0.9.

spriteBatch.Begin(SpriteSortMode.BackToFront, blendState);
spriteBatch.Draw(backgroundSprite, Vector2.Zero, Color.White);
spriteBatch.End();

// 4. Рисуем передний план. 

spriteBatch.Begin();
spriteBatch.Draw(foregroundSprite, Vector2.Zero, Color.White);
spriteBatch.End();

// 5. Рисуем спрайт с тенями, коэффициент -0.9.

spriteBatch.Begin(SpriteSortMode.BackToFront, blendState);
spriteBatch.Draw(foregroundShadow, Vector2.Zero, Color.White);
spriteBatch.End();

// 6. Рисуем меню.

spriteBatch.Begin();
spriteBatch.Draw(menu, Vector2.Zero, Color.White);
spriteBatch.End();

Рисунки я взял с игры «Craft the World», хотя сам к игре никакого отношения не имею. Внимательный читатель может отметить странные артефакты на гранях здания и деревьев — это все потому, что я довольно небрежно порезал скриншот в фотошопе. Сама техника освещения на скорость отображения практически не влияет, поэтому общая производительность игры страдать не должна. Готовый проект для Visual Studio 2010 можно скачать здесь: xnagames.codeplex.com/releases/view/136161

Надеюсь, эта статья будет полезна начинающим игроделам. Желаю удачи!

Автор: Juae

Источник

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


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