Pull to refresh

XNA Draw или пишем систему частиц. Часть II: шейдеры

Reading time 6 min
Views 17K
Привет всем разработчикам игр и просто людям, которые интересуются геймдевом.

Пришло время рассказать вам о пиксельных шейдерах и о том, как сделать 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 днем! ;)
Tags:
Hubs:
+43
Comments 19
Comments Comments 19

Articles