Делаем простое освещение в 2D игре. Подробные примеры на C# и 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

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

Similar posts

AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 8

    +4
    Если посмотреть на Ваш готовый результат, то можно увидеть, что свет проходит через стены, а это не хорошо. С тенями вообще что-то непонятное…
    А чем Вам пример не угодил?
    P.S.
    Нарисуем несколько квадратов на сером фоне:
    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();

    Может проще закрасить серым, а не черным, а не выводить как бекграунт непонятную текстуру…
      0
      А мне понравилось, хотя мысль про тени тоже возникла. Но в движении, для не очень амбициозной игры — подходит, почему бы и нет? Я в своей игре для андроида похожую идею использую для создания эффекта «огненного хвоста»:)
        0
        Ну если Вас не смущает, что свет проникает через стены и тень заранее задана, как мы видим в «Готовим спрайт с тенями», то почему бы и нет.
        Идея не новая, но действительно интересная, в совокупности с частицами можно получать очень красивые взрывы :-D
          0
          На самом деле для серьезного проекта здесь еще работать и работать: добавлять объемность через затенение отдельных областей игровых объектов, добавлять тени, отбрасываемые на другие объекты, пол и стены. Отдельная тема для случаев, когда свет не должен проходить через стены или выходить за рамки туннелей. Еще нужно добавить разные формы освещенных участков. Например, от ручного фонарика, или от лазерного луча. Еще было бы круто сделать поддержку разноцветных источников света: красного закатного солнца, когда грани объектов окрашиваются мягким красноватым цветом и вытягиваются длинные контрастные тени. Или зеленых радиоактивных отходов. В общем, работы много, хватит еще на несколько статей. А для новичков, что ищут с чего вообще начать, думаю, статьи должно хватить.
      0
      Вообще, могу посоветовать одну хитрость из 2D освещения, где по каким-то причинам нельзя использовать шейдеры:

      Разбиваем мир на особые тайлы фиксированного размера (чем меньше, тем менее производительнее и более точнее будет освещение).
      Считаем освещенность для каждого тайла, записывая эту освещенность в RT размером с x / tileSizeX, y / tileSizeY. Тени легко можно рассчитать и учитывать при освещении, используя, например, алгоритм Брезенхема или DDA-линии. Далее умножаем цвет RT-сцены на RT-лайткарты (причем благодаря несоответствию размеров последнего с первым — происходит своеобразный Upsampling, при условии линейной фильтрации).и получаются довольное красивое освещение без использования шейдеров. Этот способ хорошо подходит для тайловых игр.

      ___

      В дополнение к методу выше (из статьи) — можно еще делать subtractive-рисование примитивами-треугольниками (правда, от spritebatch отказаться нужно для рисования, но там все просто в совмещении матрицы вида spritebatch'a и нативной матрицы вида) для создания теней.
        0
        А что за игра на изображениях, посмотреть где-то уже можно?
          0
          Игра называеться «Craft the World». Скачать, поиграть можно. Ищите в сети.
            0
            Спасибо :-)

        Only users with full accounts can post comments. Log in, please.