_mini_oszone.png)
Увеличить
Несколько слов в качестве вступления. Я перерыл гору статей о том, как можно сделать динамическое освещение в 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
Надеюсь, эта статья будет полезна начинающим игроделам. Желаю удачи!