В этой публикации я постараюсь предельно просто рассказать, как можно легко и быстро сделать динамическое освещение в 2D игре на XNA; без шейдеров, карт нормалей и вообще, без дополнительных ресурсов. Целью у нас будет красивая игровая сцена с ночным небом, темным задним фоном, освещенным фонарями, передним фоном и игровым меню. Количество источников света не ограничено (в разумных пределах, конечно), форма произвольная. Освещается только передний фон. Читателю желательно иметь элементарные навыки работы с XNA, поскольку будет много кода.
Несколько слов в качестве вступления. Я перерыл гору статей о том, как можно сделать динамическое освещение в 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.
На этом с теорией все, переходим к практике. Чтобы получить красивую сцену, как на первой картинке, будем делать следующее:
- Готовим спрайт с задним планом. Для этого весь задний план рисуем на одном спрайте (RenderTarget2D);
- Готовим спрайт с передним планом;
- Готовим спрайт с тенями. Для этого берем спрайт со второго шага и в точках источников света рисуем черные круглые спрайты — это области, которые не будут затеняться.
Теперь всё это выводим на экран:
- Рисуем ночное небо;
- Рисуем спрайт с задним планом;
- Еще раз рисуем спрайт с задним планом, но с коэффициентом -0.9 (см. код). Получим темный задний план;
- Рисуем передний план;
- Рисуем спрайт с тенями, коэффициент -0.9;
- Рисуем меню.
Результат:
И, собственно готовый код:
// 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