HLSL шейдеры для чайников

Шейдерная графика сейчас наиболее популярна. Большинство игр так и пестрят системными требованиями Shader 2.0, что конечно разочаровывает владельцев стареньких ПК, и радует картинкой современных геймеров. В этой статье я попытаюсь рассказать вкратце о том как писать HLSL шейдеры на XNA и в результате получить шейдер радиальной градиентной заливки.


Итак. Нам понадобяться следующие инструменты:

  1. XNA Game Studio 
  2. Видеокарта с поддержкой шейдеров
Итак для начала мы создадим "Windows Game" проект в студии. Далее нам необходимо создать небольшую текстурку. Неважно её содержимое, сколько само её существование. Поэтому добавляем к Content новый элемент - текстуру 2*2. 

Далее нам необходимо нарисовать её, поэтому добавляем в класс Game переменную Texture2D и загружаем её в LoadContent

Texture2D texture;
protected override void LoadContent()
        {
            // Create a new SpriteBatch
            spriteBatch = new SpriteBatch(GraphicsDevice);
            texture = Content.Load("tex");
        }


и далее отрисовываем её, например так:


         protected override void Draw(GameTime gameTime)
        {
            GraphicsDevice.Clear(Color.CornflowerBlue);
                spriteBatch.Begin(SpriteBlendMode.None, SpriteSortMode.Immediate, SaveStateMode.SaveState);
                    spriteBatch.Draw(texture,
                        new Rectangle(0, 0,
                            400, 300), Color.White);
                spriteBatch.End();
            base.Draw(gameTime);
        }

Если всё сделано правильно, то запустив мы увидим белый прямоугольник на голубом фоне.
Кстати советую spriteBatch.Begin написать  именно так как описано выше, т.к иначе шейдер может не сработать.


Наконец можно приступить к написанию шейдера =)



  1. Добавляем к Content новый шейдер (Effect file). Студия автоматически создаст шаблон шейдера, однако мы далее его переделаем, т.к он содержит много лишнего. 
  2. Далее добавим переменную в класс Game - Effect effect;
  3. Загружаем шейдер в LoadContent
effect = Content.Load("gradient");

И переписываем процедуру рисования:

protected override void Draw(GameTime gameTime)
        {
            GraphicsDevice.Clear(Color.CornflowerBlue);

                spriteBatch.Begin(SpriteBlendMode.None, SpriteSortMode.Immediate, SaveStateMode.SaveState);
                effect.Begin();
                foreach (EffectPass pass in effect.Techniques[0].Passes)
                {
                    pass.Begin();
                    spriteBatch.Draw(texture,
                        new Rectangle(0, 0,
                            400, 300), Color.White);
                    pass.End();
                }
                effect.End();
                spriteBatch.End();
            base.Draw(gameTime);
        }

Теперь не спешите запускать проект. Последний мазок - переписываем шейдер:

sampler ColorMapSampler : register(s0);

// Grayscale
float4 PixelShader(float2 Tex: TEXCOORD0) : COLOR
{
return float4(1,0,0,1);
}

technique PostProcess
{
pass P0
{
PixelShader = compile ps_2_0 PixelShader();
}
}

Здесь я удалил вершинный шейдер и лишние переменные. Для простоты оставил только пиксельный. Теперь если запустить проект то должен получится красный квадратик


Не очень впечатляет, но это ведь только начало ;)

Давайте посмотрим ближе на шейдер. Шейдер - это программа которая выполняется на видеоадаптере компьютера. Как известно видеоадаптер в разы быстрее обрабатывает информацию, поэтому шейдеры так удобны. Существуют различные виды шейдеров:
  • Пиксельный
  • Вершинный
Пиксельный обрабатывает пиксели,а вершинный соотвественно вершины. Так вот у нас пиксельный. На вход мы получаем текстурные координаты пикселя ( его положение в пространстве). Они изменяется от [0..1]. А на выход пиксельный шейдер ( по сути это функция) должна возвращать float4 - вектор из 4 компонент, который указывает выходной цвет пикселя.
В нашем шейдере мы всем пикселям указали цвет (1,0,0,1), т.е красный по цветовой модели RGBA.

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

float4 PixelShader(float2 Tex: TEXCOORD0) : COLOR
{
return float4(Tex.x,Tex.y,0,1);
}



Помойму смотрится уже лучше =) Как видим там где x текстурная координата ближе к 1 мы видим больше красного,а где ближе к 1 y координата больше зелёного. Что в принципе и должен был делать наш шейдер.

Давайте двигаться дальше. Конечной целью статьи было написание градиентной круговой заливки. В качестве параметров заливка должна иметь:
  1. Радиус градиента
  2. Центр градиента
  3. Начальный и конечный цвет
Итак, начнём:

float4 PixelShader(float2 Tex: TEXCOORD0) : COLOR
{
float2 Center=float2(0.5f,0.5f);
float  Radius=0.2f;
float4 start=(1,0,0,1);
float4 end=(0,1,0,1);
float dist=distance(Center,Tex);
return float4(dist,0,0,1);
}

Мы добавили в шейдер переменные центра, радиуса, начального и конечного цвета градиента. Кроме того здесь мы вычисляем с помощью встроенной функции в HLSL расстояние между центром и текстурными координатами пикселя. А далее мы расстояние визуализируем. Получаем:


Получили красный градиент с центром в (0.5,0.5). Нам однако всё ещё нужно чтобы были начальные и конечные цвета,а также радиус градиента. Поэтому переписываем шейдер:

float4 PixelShader(float2 Tex: TEXCOORD0) : COLOR
{
float2 Center=float2(0.5f,0.5f);
float  Radius=0.6f;
float4 start=float4(0,0,1,1);
float4 end=float4(0,1,0,1);
float dist=min(1,distance(Center,Tex)/Radius);
float4 realcolor=smoothstep(start,end,dist);
return realcolor;
}

Теперь помедленней:
  • distance - возвращает float - расстояние между двумя векторами
  • smoothstep - делает переход на основе 3-го параметра между двумя векторами. В данном случае это делает переход из одного цвета в другой.
  • min - функция минимума
Теперь если запустим всё будет по честному, только одно но... Хочется все параметры задавать из программы. Поэтому можете запускать посмотреть результат... а я дальше пойду. Опять переписываем шейдер:

float2 Center;
float  Radius;
float4 start;
float4 end;

float4 PixelShader(float2 Tex: TEXCOORD0) : COLOR
{
float dist=min(1,distance(Center,Tex)/Radius);
float4 realcolor=smoothstep(start,end,dist);
return realcolor;
}

И также переписываем метод рисования:



spriteBatch.Begin(SpriteBlendMode.None, SpriteSortMode.Immediate, SaveStateMode.SaveState);
                effect.Parameters["Radius"].SetValue(0.9f);
                effect.Parameters["Center"].SetValue(new Vector2(0.5f, 0.5f));
                effect.Parameters["start"].SetValue(Color.White.ToVector4());
                effect.Parameters["end"].SetValue(Color.Red.ToVector4());
                effect.CommitChanges();
                effect.Begin();

Что получилось? 

Довольно симпатично. Что поменялось? Мы вынесли переменные из функции шейдера, таким образом сделали их глобальными параметрами шейдера. А в процедуре рисования мы присваивали им значения. Таким образом шейдер управляется из программы. 

На этом пока что всё. Статья получилось немного длинная, но надеюсь суть ясна. Исходники можно скачать по адресу:





Комментарии

  1. Пост-скриптум: Недавно переделал метод получения живого фона.Именно для него я использовал данный шейдер.

    1. Создаю несколько спрайтов большого разрешения с радиальным градиентом различных цветов, причём с альфа каналом.
    2. Рисую их , при этом двигая по экрану.

    ФПС вырос довольно значительно.Видимо обработка шейдером спрайта размером с экран довольно ресурсоёмкая операция.

    ОтветитьУдалить

Отправить комментарий

Популярные сообщения из этого блога

Структуры данных ( АВЛ-дерево , обход графа и построение минимального остовного дерева графа)

2D Физика для игр - Separate Axis Theorem