Пример 2D освещения в XNA Game Studio 4.0

В этой статье я опишу достаточно простой способ сделать освещение для 2D игры. В его основе будет лежать шейдер постобработки.

Начнем с создания базовой функциональности нашего будущего приложения.

Как обычно загрузим текстуры и выведем их на экран:

 

Texture2D wood;

Texture2D hedgehog;

Rectangle position = new Rectangle(250, 350, 101, 72);

 

        public Game1()
        {
            graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";
 
            graphics.PreferredBackBufferWidth = 640;
            graphics.PreferredBackBufferHeight = 480;
        }

 

 

 

        /// <summary>
        /// LoadContent will be called once per game and is the place to load
        /// all of your content.
        /// </summary>
        protected override void LoadContent()
        {
            // Create a new SpriteBatch, which can be used to draw textures.
            spriteBatch = new SpriteBatch(GraphicsDevice);
            wood = Content.Load<Texture2D>("wood");
            hedgehog = Content.Load<Texture2D>("hedgehog");
 
            // TODO: use this.Content to load your game content here
        }

 

 

 

        /// <summary>
        /// This is called when the game should draw itself.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Draw(GameTime gameTime)
        {
 
            // TODO: Add your drawing code here
 
            GraphicsDevice.Clear(Color.Black);
 
            spriteBatch.Begin();
            spriteBatch.Draw(wood, new Rectangle(0, 0, graphics.PreferredBackBufferWidth, graphics.PreferredBackBufferHeight), Color.White);
            spriteBatch.Draw(hedgehog, position, null, Color.White, 0, Vector2.Zero, SpriteEffects.None, 0);
            spriteBatch.End();
 
 
            base.Draw(gameTime);
        }

 

 

 

Итак, у нас есть фоновая картинка леса и спрайт ежа (который, правда у меня получился не слишком хорошо).

Дальше я постараюсь сделать так, чтобы вокруг этого ежа было свечение, как будто он носит с собой лампу.

Сначала я добавлю возможность управления нашим героем с клавиатуры. Пока она будет без всяких проверок, только для примера.

 
        SpriteEffects spriteEffect;

 /// <summary>
        /// Allows the game to run logic such as updating the world,
        /// checking for collisions, gathering input, and playing audio.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Update(GameTime gameTime)
        {
            // Allows the game to exit
            if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
                this.Exit();
 
            // TODO: Add your update logic here
            KeyboardState kb = Keyboard.GetState();
 
            int speed = (int) (100 * gameTime.ElapsedGameTime.TotalSeconds);
            if (kb.IsKeyDown(Keys.Up))
                position.Offset(0, -speed);
            if (kb.IsKeyDown(Keys.Down))
                position.Offset(0, +speed);
            if (kb.IsKeyDown(Keys.Left))
            {
                position.Offset(-speed, 0);
                spriteEffect = SpriteEffects.FlipHorizontally;
            }
            if (kb.IsKeyDown(Keys.Right))
            {
                position.Offset(+speed, 0);
                spriteEffect = SpriteEffects.None;
            }
 
            base.Update(gameTime);
        }

       /// <summary>
        /// This is called when the game should draw itself.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Draw(GameTime gameTime)
        {
 
            // TODO: Add your drawing code here
 
            GraphicsDevice.Clear(Color.Black);
 
            spriteBatch.Begin();
            spriteBatch.Draw(wood, new Rectangle(0, 0, graphics.PreferredBackBufferWidth, graphics.PreferredBackBufferHeight), Color.White);
            spriteBatch.Draw(hedgehog, position, null, Color.White, 0, Vector2.Zero, spriteEffect, 0);
            spriteBatch.End();
 
 
            base.Draw(gameTime);
        }

Теперь приготовления закончены и можно обсудить детали нашего бушующего алгоритма: мы всегда знаем, в какой части сцены находится источник света. Нам нужно затемнять все точки сцены, которые удалены на определенное расстояние от источника света (а еще лучше сделать постепенное затемнение). Очень удобно это делать в пиксельном шейдере, правда нужно учитывать, что шейдер должен обладать информацией обо всей сцене (также можно вырезать из сцены на время обработки героя, все зависит от того, чтобы Вы хотите получить). Таким образом, нужно сначала нарисовать всю сцену (в нашем случае, сцена состоит всего из двух картинок: еж и задний план. В общем случае, нужно будет рисовать и другие объекты сцены на этом шаге) на дополнительную поверхность рисования.

 
        RenderTarget2D target;

        /// <summary>
        /// Allows the game to perform any initialization it needs to before starting to run.
        /// This is where it can query for any required services and load any non-graphic
        /// related content.  Calling base.Initialize will enumerate through any components
        /// and initialize them as well.
        /// </summary>
        protected override void Initialize()
        {
            // TODO: Add your initialization logic here
            target = new RenderTarget2D(GraphicsDevice, graphics.PreferredBackBufferWidth, graphics.PreferredBackBufferHeight);
            base.Initialize();
        }

       /// <summary>
        /// This is called when the game should draw itself.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Draw(GameTime gameTime)
        {
 
            // TODO: Add your drawing code here
 
            GraphicsDevice.SetRenderTarget(target);
            GraphicsDevice.Clear(Color.Black);
 
            spriteBatch.Begin();
            spriteBatch.Draw(wood, new Rectangle(0, 0, graphics.PreferredBackBufferWidth, graphics.PreferredBackBufferHeight), Color.White);
            spriteBatch.Draw(hedgehog, position, null, Color.White, 0, Vector2.Zero, spriteEffect, 0);
            spriteBatch.End();
 
            GraphicsDevice.SetRenderTarget(null);
             GraphicsDevice.Clear(Color.Black);
  
             spriteBatch.Begin();
             spriteBatch.Draw(target, Vector2.Zero, Color.White);
             spriteBatch.End();

 
            base.Draw(gameTime);
        }

Пока что я просто рисую на экране то, что находится в поверхности рисования (то есть всю нашу сцену).

Теперь мы создадим шейдер для обработки нашей сцены. Самый первый, который приходит в голову:

 
sampler  ColorSampler  : register(s0);
float2 position;
 
float4 LampPS(float2 TexCoords : TEXCOORD0) : COLOR0
{

          float d = distance(TexCoords, position);

          float4 color = tex2D(ColorSampler, TexCoords);
          if (d > 0.3)
                    color = float4(0,0,0,1);
          return color;
}


 
technique Lamp
{
    pass Pass1
    {
        PixelShader = compile ps_2_0 LampPS();
    }
}

Здесь мы просто определяем дистанцию от источника света до точки и, если она больше некоторого значения (например, 0.3), то закрашиваем пиксель черным цветом.

Нам также понадобится передавать в шейдер позицию источника света, которая у нас будет совпадать с центром спрайта ежа.

Только обязательно нужно учитывать, что текстурные координаты в пиксельном шейдере могут принимать значения от 0 до 1, в то время как позиция спрайта ежа является целым значением и лежит в нашем случае где-то от 0 до 640 (или 480 по Y). Соответственно, нужно сделать небольшое преобразование:

 
        /// <summary>
        /// This is called when the game should draw itself.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Draw(GameTime gameTime)
        {
 
            // TODO: Add your drawing code here
 
            GraphicsDevice.SetRenderTarget(target);
            GraphicsDevice.Clear(Color.Black);
 
            spriteBatch.Begin();
            spriteBatch.Draw(wood, new Rectangle(0, 0, graphics.PreferredBackBufferWidth, graphics.PreferredBackBufferHeight), Color.White);
            spriteBatch.Draw(hedgehog, position, null, Color.White, 0, Vector2.Zero, spriteEffect, 0);
            spriteBatch.End();
 
            GraphicsDevice.SetRenderTarget(null);
            GraphicsDevice.Clear(Color.Black);
 
            float posx = (float)(position.X + position.Width / 2) / graphics.PreferredBackBufferWidth;
             float posy = (float)(position.Y + position.Height / 2) / graphics.PreferredBackBufferHeight;
             lampEffect.Parameters["position"].SetValue(new Vector2(posx, posy));
  
             spriteBatch.Begin(0, BlendState.AlphaBlend, null, null, null, lampEffect);
             spriteBatch.Draw(target, Vector2.Zero, Color.White);
             spriteBatch.End();
 
            base.Draw(gameTime);
        }

 

Мы сразу видим несколько проблем в полученном примере:

  1. Рамка света не является круглой (а является овальной)
  2. Хотелось бы получить более реалистичное изображение, добавив реалистичное затухание света

Первый пункт исправить достаточно легко, правда сначала стоит понять причину появления этой проблемы. Дело в том, что код шейдера всегда работает с квадратным изображением 1х1, а разрешение окна у нас выставлено не квадратное. Когда изображение будет обработано пиксельным шейдером оно автоматически будет спроецировано на прямоугольное окно и мы увидим овал вместо окружности.

К счастью мы можем вычислить и воспользоваться коэффициентом пропорциональности, как это сейчас будет сделано в новой версии шейдера

 
sampler  ColorSampler  : register(s0);
float AspectRatio = 640.0/480.0;
 
float2 position;
 
float4 LampPS(float2 TexCoords : TEXCOORD0) : COLOR0
{
    float2 centerToPixel = TexCoords - position;
     float d = length(centerToPixel / float2(1, AspectRatio));
 
    float4 color = tex2D(ColorSampler, TexCoords);
          if (d > 0.3)
                    color = float4(0,0,0,1);
          return color;
}
 
technique Lamp
{
    pass Pass1
    {
        PixelShader = compile ps_2_0 LampPS();
    }
}

То есть будем вычислять расстояние между точками по разным осям с учетом коэффициента пропорциональности.

Я думаю, что можно придумать достаточно много интересных формул для реализации красивого освещения и эту задачу я оставлю читателям для самостоятельного изучения. Здесь я лишь приведу свою реализацию, которая показалась мне достаточно эффектной.

 
sampler  ColorSampler  : register(s0);
float AspectRatio = 640.0/480.0;
 
float2 position;
 
float4 LampPS(float2 TexCoords : TEXCOORD0) : COLOR0
{
    float2 centerToPixel = TexCoords - position;
           float d = length(centerToPixel / float2(1, AspectRatio)) * 6;
  
           float4 color = tex2D(ColorSampler, TexCoords) / (d*d);
           color = lerp(color, float4(0,0,0,1), d/2);
     return color;
}
 
technique Lamp
{
    pass Pass1
    {
        PixelShader = compile ps_2_0 LampPS();
    }
}

В этой шейдере я делаю несколько шагов:

  1. Сначала вычисляю расстояние от точки до источника света с учетов некоторого подобранного коэффициента.
  2. После этого делю цвет пикселя на квадрат расстояния до источника. (Вроде бы с точки зрения физики это разумно)
  3. Если сейчас остановиться, то практически вся сцена будет освещена (хотя и слабо). Так что я смешиваю текущий цвет с черным в зависимости от расстояния до источника (опять с подобранным коэффициентом)

Получится вот такой эффект:

Выглядит как-то слишком «засвечено», но, возможно, где-то это пригодится.

Получившийся код:

Game1.cs

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;
 
namespace FogGame
{
    /// <summary>
    /// This is the main type for your game
    /// </summary>
    public class Game1 : Microsoft.Xna.Framework.Game
    {
        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch;
 
        Texture2D wood;
        Texture2D hedgehog;
        SpriteEffects spriteEffect;
 
        Rectangle position = new Rectangle(250, 350, 101, 72);
 
        Effect lampEffect;
        RenderTarget2D target;
 
        public Game1()
        {
            graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";
 
            graphics.PreferredBackBufferWidth = 640;
            graphics.PreferredBackBufferHeight = 480;
        }
 
        /// <summary>
        /// Allows the game to perform any initialization it needs to before starting to run.
        /// This is where it can query for any required services and load any non-graphic
        /// related content.  Calling base.Initialize will enumerate through any components
        /// and initialize them as well.
        /// </summary>
        protected override void Initialize()
        {
            // TODO: Add your initialization logic here
            target = new RenderTarget2D(GraphicsDevice, graphics.PreferredBackBufferWidth, graphics.PreferredBackBufferHeight);
            base.Initialize();
        }
 
        /// <summary>
        /// LoadContent will be called once per game and is the place to load
        /// all of your content.
        /// </summary>
        protected override void LoadContent()
        {
            // Create a new SpriteBatch, which can be used to draw textures.
            spriteBatch = new SpriteBatch(GraphicsDevice);
            wood = Content.Load<Texture2D>("wood");
            hedgehog = Content.Load<Texture2D>("hedgehog");
 
            lampEffect = Content.Load<Effect>("lamp");
            // TODO: use this.Content to load your game content here
        }
 
        /// <summary>
        /// UnloadContent will be called once per game and is the place to unload
        /// all content.
        /// </summary>
        protected override void UnloadContent()
        {
            // TODO: Unload any non ContentManager content here
        }
 
        /// <summary>
        /// Allows the game to run logic such as updating the world,
        /// checking for collisions, gathering input, and playing audio.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Update(GameTime gameTime)
        {
            // Allows the game to exit
            if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
                this.Exit();
 
            // TODO: Add your update logic here
            KeyboardState kb = Keyboard.GetState();
 
            int speed = (int) (100 * gameTime.ElapsedGameTime.TotalSeconds);
            if (kb.IsKeyDown(Keys.Up))
                position.Offset(0, -speed);
            if (kb.IsKeyDown(Keys.Down))
                position.Offset(0, +speed);
            if (kb.IsKeyDown(Keys.Left))
            {
                position.Offset(-speed, 0);
                spriteEffect = SpriteEffects.FlipHorizontally;
            }
            if (kb.IsKeyDown(Keys.Right))
            {
                position.Offset(+speed, 0);
                spriteEffect = SpriteEffects.None;
            }
 
            base.Update(gameTime);
        }
 
        /// <summary>
        /// This is called when the game should draw itself.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Draw(GameTime gameTime)
        {
 
            // TODO: Add your drawing code here
 
            GraphicsDevice.SetRenderTarget(target);
            GraphicsDevice.Clear(Color.Black);
 
            spriteBatch.Begin();
            spriteBatch.Draw(wood, new Rectangle(0, 0, graphics.PreferredBackBufferWidth, graphics.PreferredBackBufferHeight), Color.White);
            spriteBatch.Draw(hedgehog, position, null, Color.White, 0, Vector2.Zero, spriteEffect, 0);
            spriteBatch.End();
 
            GraphicsDevice.SetRenderTarget(null);
            GraphicsDevice.Clear(Color.Black);
 
            float posx = (float)(position.X + position.Width / 2) / graphics.PreferredBackBufferWidth;
            float posy = (float)(position.Y + position.Height / 2) / graphics.PreferredBackBufferHeight;
            lampEffect.Parameters["position"].SetValue(new Vector2(posx, posy));
 
            spriteBatch.Begin(0, BlendState.AlphaBlend, null, null, null, lampEffect);
            spriteBatch.Draw(target, Vector2.Zero, Color.White);
            spriteBatch.End();
 
            base.Draw(gameTime);
        }
    }
}

 

Lamp.fx

sampler  ColorSampler  : register(s0);
float AspectRatio = 640.0/480.0;
 
float2 position;
 
float4 LampPS(float2 TexCoords : TEXCOORD0) : COLOR0
{
    float2 centerToPixel = TexCoords - position;
          float d = length(centerToPixel / float2(1, AspectRatio)) * 6;
 
          float4 color = tex2D(ColorSampler, TexCoords) / (d*d);
          color = lerp(color, float4(0,0,0,1), d/2);
    return color;
}
 
technique Lamp
{
    pass Pass1
    {
        PixelShader = compile ps_2_0 LampPS();
    }
}

 


					
Реклама
Запись опубликована в рубрике Компьютерная графика с метками , , , , , , , , . Добавьте в закладки постоянную ссылку.

13 комментариев на «Пример 2D освещения в XNA Game Studio 4.0»

  1. Artyom:

    Ваня, ты меня спас!

  2. 0xAF:

    Огромное спасибо!

  3. nightman665:

    Присоединяюсь, отлично пишете, начинал с Ваших видеоуроков, найти, правда, их проблематично 🙂

    • Спасибо! Разве видео сложно найти?

      • nightman665:

        Извиняюсь, спешил, не так выразился. Найди видео в хорошем качестве (чтобы видно код было) с возможностью его скачать. Такое у меня получилось только на каком-то сервисе яндекса. Может быть плохо искал…

    • Я изначально не планировал возможности скачивания видео 🙂 Есть качество получше, но уж больно долго все это выкладывать.

  4. nightman665:

    Очень интересует вопрос как такое освещение можно реализовать для Windos Phone? Т.к. компилятор говорит, что WP не поддерживает кастомные шейдеры. Может быть уже есть какие-нибудь библиотеки, помогающие обойти это ограничение? Пока что гугл не дал результатов 😦

    • Для Windows Phone 7 использовать кастомные шейдеры нельзя. Для WP8 вроде можно.
      Но вообще можно все сделать и без шейдеров. Например, просто использовать черную текстуру с прозрачной дыркой.

  5. nightman665:

    Оно-то, конечно, можно, но уж больно красивые шейдеры сейчас вижу на андроид под их движки, а наложить текстуру — это не выход 😦 Что ж, спасибо и на этом 🙂

  6. Zamarus:

    А где создать шейдер?Точнее в каком песте я долже код вставить, я не начинающий необессудьте.

Добавить комментарий

Заполните поля или щелкните по значку, чтобы оставить свой комментарий:

Логотип WordPress.com

Для комментария используется ваша учётная запись WordPress.com. Выход / Изменить )

Фотография Twitter

Для комментария используется ваша учётная запись Twitter. Выход / Изменить )

Фотография Facebook

Для комментария используется ваша учётная запись Facebook. Выход / Изменить )

Google+ photo

Для комментария используется ваша учётная запись Google+. Выход / Изменить )

Connecting to %s