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

Пришло время рассказать вам о пиксельных шейдерах и о том, как сделать post-proccesing. Это вторая часть статьи о графических методах в XNA, в прошлой статье — мы рассматривали методы Draw и Begin у spriteBatch. Для примера: улучшим нашу систему частиц добавлением пиксельного шейдера, который будет искажать пространство.







В этой части:
  • Что такое пиксельный шейдер
  • Что такое post-processing
  • Кратко: Что такое RenderTarget2D и с чем его едят заправляют
  • Искажающий шейдер с Displacemenet-map
  • Практика: дорабатываем систему частиц


Шейдер



Немного поговорим о шейдарах. Существуют два типа шейдера (в Shader Model 2.0, её то мы и используем): вертексный и пиксельный.

Вершинный шейдер оперирует данными, сопоставленными с вершинами многогранников. К таким данным, в частности, относятся координаты вершины в пространстве, текстурные координаты, тангенс-вектор, вектор бинормали, вектор нормали. Вершинный шейдер может быть использован для видового и перспективного преобразования вершин, генерации текстурных координат, расcчета освещения и т. д.

Пиксельные шейдеры выполняются для каждого фрагмента в фазе растеризации треугольников. Фрагмент (или пиксель) — точка, с оконными координатами, полученная растеризатором после выполнения над ней ряда операций. Про��е говоря, результирующая точка буфере кадра, совокупность этих точек потом формирует изображение. Пиксельные шейдеры оперируют над фрагментами до заключительных стадий, т.е. до тестов глубины, альфы и stencil. Пиксельный шейдер получает интерполированные данные (цвет, текстурные координаты) из вершинного шейдера.



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

В случае с Displacement-шейдером — вершинные шейдеры не нужны, рассмотрим пиксельные.

Post-processing


Если быть ленивым лаконичным человеком, то Post-processing шейдеры выполняются тогда, когда вся картинка игры уже отрисована: шейдер накладывается сразу на всю картинку, никак не на отдельные спрайты.

-Ведь у spriteBatch.Begin есть параметр, effect, не проще применять шейдер сразу, как мы его рисуем?
Отвечаю: вот именно, что такой шейдер применяется к единичным спрайтам, как итог, Displacement-шейдер будет функционировать криво.

Для создания Post-process обработки, нужно сначала рисовать то, что должно быть нарисовано на экране — на отдельную текстуру, а потом рисовать эту самую текстуру с использованием Post-process шейдера. Таким образом, шейдер воздействует не на единичные спрайты, а на картинку в целом.

-Стоп, а как рисовать на отдельную текстуру?
Отвечаю: знакомьтесь — RenderTarget2D

RenderTarget2D



И опять, привет мой друг — лаконичность. RenderTarget2D — по сути является текстурой, на которую можно рисовать.

Идем туда, где обычно мы рисуем сцену, перед отчищением вставляем:

GraphicsDevice.SetRenderTarget(renderTarget);


Теперь все будет рисоваться не на экран, а на RenderTarget2D.
Чтобы переключиться опять на экран, используем конструкцию:

GraphicsDevice.SetRenderTarget(null);


Не забудьте очистить RenderTarget, перед прорисовкой.

Искажающий шейдер с Displacemenet-map



Идея такого пиксельного шейдера очень проста: на вход поступает текстура, которую нужно «погнуть», на второй вход — карта, о том, как гнуть.

Карту мы будем генерировать, о том как — в практике.

Кстати, о карте. Карта представляет собой такое же по размерам изображение, как и текстура сцены, за исключением того, пожалуй, что нарисованного изображения — не увидим.

Более подобно о карте и о том, как действует шейдер:

В процессе обработки изображения — получаем текущую позицию пикселя, получаем цвет. Тоже самое делаем и для карты. Т.е. в конечном итоге, у нас будет доступно для модификации: цвет пикселя, позиция пикселя, цвет пикселя на карте соответствующего позиции пикселя на изображении.

Будем использовать цвета карты, чтобы передать информацию шейдеру, как погнуть пиксель.

К примеру, R-канал (красный) получает значения от 0f до 1f. Если мы видим на карте искажения R=0.5f, то просто сдвигаем позицию пикселя изображения на 10f * 0.5f пикселя. 10f — это сила, с которой мы сдвигаем.

Соответственно, R-канал будет соответствовать X координате, а G-канал — Y.

Если вам нужны картинки, получите их:

Исходная картинка:


Карта:


Итоговая картинка:


Так, с теорией вроде разборались, сейчас попробуем это все реализовать кодом.

План действий:
  • Программируем шейдер.
  • Реализуем post-processing
  • Создает еще одну систему частиц, но на этот раз необычных, эти частицы будут рисоваться в карту для шейдера.
  • Передаем шейдеру карту и применяем c рисованием Post-process.
  • ???
  • PROFIT!


Практика: дорабатываем систему частиц



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



Копируем ParticleController и называем его ShaderController, в нем нам нужно изменить только сам процесс создания частицы, а конкретно:

public void EngineRocketShader(Vector2 position) // функция, которая будет генерировать частицы шейдера
{
            for (int a = 0; a < 2; a++) // создаем 2 частицы дыма для трейла
            {
                Vector2 velocity = AngleToV2((float)(Math.PI * 2d * random.NextDouble()), 1.6f);
                float angle = (float)(Math.PI * 2d * random.NextDouble());
                float angleVel = 0;
                Vector4 color = new Vector4((float)random.NextDouble(), (float)random.NextDouble(), 1f, (float)random.NextDouble()); // задаем случайными R и G и A каналы.

                float size = 1f;
                int ttl = 80;
                float sizeVel = 0;
                float alphaVel = 0.01f;


                GenerateNewParticle(smoke, position, velocity, angle, angleVel, color, size, ttl, sizeVel, alphaVel);
            }

}


Реализуем post-processing, создаем новые переменные:

RenderTarget2D shader_map; // карта для шейдера
        RenderTarget2D renderTarget; // готовое к обработке изображение


Инициализируем их:

shader_map = new RenderTarget2D(GraphicsDevice, 800, 600);
renderTarget = new RenderTarget2D(GraphicsDevice, 800, 600);


Идем к методу Draw главного класса и пишем:

protected override void Draw(GameTime gameTime)
        {
            GraphicsDevice.SetRenderTarget(renderTarget); // рисуем в renderTarget

            GraphicsDevice.Clear(Color.Black);

            spriteBatch.Begin();
            spriteBatch.Draw(background, new Rectangle(0, 0, 800, 600), Color.White);
            spriteBatch.End();

            part.Draw(spriteBatch);

            GraphicsDevice.SetRenderTarget(shader_map); // рисуем в карту шейдера
            GraphicsDevice.Clear(Color.Black);
            shad.Draw(spriteBatch);

            GraphicsDevice.SetRenderTarget(null); // рисуем в сцену
            GraphicsDevice.Clear(Color.Black);

            spriteBatch.Begin();
            
                spriteBatch.Draw(renderTarget, new Rectangle(0, 0, 800, 600), Color.White);

            spriteBatch.End();

            base.Draw(gameTime);
        }


Post-processing готов, теперь создадим шейдер.

Создаем новый Effect (fx) файл (это файл шейдера, написанного на HLSL), вписываем туда, что-то вроде:

texture displacementMap; // наша карта

sampler TextureSampler : register(s0); // тут та текстура, которая отрисовалась на экран
sampler DisplacementSampler : samplerState{ // устанавливаем TextureAddress
Texture = displacementMap;
MinFilter = Linear;
MagFilter = Linear;
AddressU = Clamp;
AddressV = Clamp;

};

float4 main(float4 color : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0
{
	
   /* PIXEL DISTORTION BY DISPLACEMENT MAP */
    float3 displacement = tex2D(DisplacementSampler, texCoord); // получаем R,G,B из карты
    
    // Offset the main texture coordinates.
    texCoord.x += displacement.r * 0.1; // меняем позицию пикселя
    texCoord.y += displacement.g * 0.1; // меняем позицию пикселя


   float4 output = tex2D(TextureSampler, texCoord); // получаем цвет для нашей текстуры

    return color * output;
}

technique DistortionPosteffect
{
    pass Pass1
    {
        PixelShader = compile ps_2_0 main(); // компилируем шейдер
    }
}


Шейдер создан, загрузить его можно так же, как и обычную текстуру, за исключением того, что тип не Texture2D, а Effect.

Теперь обновим наш Draw:

effect1.Parameters["displacementMap"].SetValue(shader_map); // задаем карту шейдеру

            spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.Opaque, SamplerState.LinearClamp, DepthStencilState.None, RasterizerState.CullCounterClockwise, effect1); // рисуем с приминением шейдера

                spriteBatch.Draw(renderTarget, new Rectangle(0, 0, 800, 600), Color.White);

            spriteBatch.End();


Запускаем, любуемся красивыми, реалистичными животными искажениями (лучше посмотреть демо):


На самом деле, эта реализация системы частиц (не шейдеры, а то что было в первом уроке) в целом — не совсем хороша для производительности. Сущесвтуют другие методы, более сложные в понимании, о них я расскажу как-нибудь потом.

Прикладываю исходники и демо (на этот раз, запустится на любом компьютере с XNA 4.0 и аппаратной подержкой DirectX9, inc sh 2.0)

Может быть на этой неделе, может быть неизвестно когда — расскажу о методе Update и как реализовать физику, используя Box2D.

Удачи вам и еще раз с праздником программиста 0xFF+1 днем! ;)