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

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

Пришло время рассказать вам о пиксельных шейдерах и о том, как сделать 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 днем! ;)
Share post

Similar posts

Comments 19

    +5
    Пишите еще. Очень интересует взаимодействие с Box2D
      +2
      Box2D реализация для XNA (еще добавляет некоторые фичи даже)
      farseerphysics.codeplex.com/
        0
        Хм, спасибо, даже не знал о нем :)
        Инструменты очень полезные :)
      0
      блин я оказывается первую статью пропустил(
        0
        Выложите пожалуйста видео-демонстрацию :)
          0
          Видео-демонстрацию — видео снятое с работы демо?
            0
            Да
              0
              А вы скажите что-нибудь такое бесплатное и простое, чем можно видео размером в 200 мб пережать в привычный вид и залить на ютуб :)
                +1
                Единственное что приходит на ум из довольно просто и бесплатного это ffmpeg :) Но если у Вас есть Sony Vegas то там можно в H.264 пожать буквально за 5 минут. Еще если у Вас видеокарта от ATI то там в CCC есть встроенный конвертер для видео, вот только он далеко не все кушает и далеко не во все может сконвертировать, зато весьма быстро работает :) Больше, к сожалению, адекватных программ не знаю:) Для ffmpeg наверняка есть прикрученные GUI, но какие именно хорошие не могу подсказать, увы)
          +1
          Отлично! Очень обрадовался статье о xna. Тем более о системе частиц(когда игрался с xna как раз с частицами были проблемы, искал решения). В общем, автору большой плюс, ждем еще статей.
            0
            а у меня при запуске ошибка:

            Сигнатура проблемы:
            Имя события проблемы: APPCRASH
            Имя приложения: ParticleSystem.exe
            Версия приложения: 1.0.0.0
            Отметка времени приложения: 4e6f46db
            Имя модуля с ошибкой: KERNELBASE.dll
            Версия модуля с ошибкой: 6.1.7601.17651
            Отметка времени модуля с ошибкой: 4e2111c0
            Код исключения: e0434352
            Смещение исключения: 0000d36f
            Версия ОС: 6.1.7601.2.1.0.256.1
            Код языка: 1049
            Дополнительные сведения 1: 0a9e
            Дополнительные сведения 2: 0a9e372d3b4ad19135b953a78882e789
            Дополнительные сведения 3: 0a9e
            Дополнительные сведения 4: 0a9e372d3b4ad19135b953a78882e789

            Ознакомьтесь с заявлением о конфиденциальности в Интернете:
            go.microsoft.com/fwlink/?linkid=104288&clcid=0x0419

            Если заявление о конфиденциальности в Интернете недоступно, ознакомьтесь с его локальным вариантом:
            C:\Windows\system32\ru-RU\erofflps.txt

            Система:
            Win7 32
            Ati 4870
            AMD Athlon™
              0
              А у вас фреймверк XNA 4.0 стоит?
                0
                спасибо, обновил заработало
              0
              Ура-ура, очередная интересная статья! Вы первый в рейтинге читаемых мной авторов :D
                0
                Прочитал все ваши статьи. И могу сказать только одно у вас хорошо получается и объясняете все доступно. У меня к вам только один вопрос. С год назад я изучал XNA по курсу Intuit. Там каждый игровой компонент наследовался от класса GameComponent в котором прописывалась вся логика, и при инициализации объектов они просто добавлялись в коллекцию Components, что избавляло от использования классов Controller. У вас же присутствуют два класса на компонент например Particle и ParticleController. Почему вы не используете подход описанный мной в своих примерах? Может он имеет какие-то недостатки, которые в курсе Intuit не озвучили? или вам просто так удобнее? Надеюсь я нормально объяснил суть вопроса.
                  0
                  GameComponent — этот класс призван упорядочить иерархию и внутреннюю структуру игры. Например, не нагружать GameLogic. Тут уже вступает дело вкуса, я ранее программировал на других языках, Action Script 3 тому в пример, там как раз можно/нужно было использовать контроллеры. Различия от использования контроллеров и GameComponent — нет, разве, что удобности у них разные. GameComponent — более «модульный», чтобы подключить/отключить контроллер, нужно побольше выполнить действий.
                    0
                    Благодарю за развернутый ответ.
                  0
                  Очень интересно, спасибо.
                  В качестве поделки или пробы — можно таки методом делать красивые интерактивные открытки из собственных фотографий
                  Буду ждать следующей статьи
                    0
                    Блин, ну как так можно, ни исходников ни демо теперь не посмотреть, ссылки дохлые. Можно перезалить куда-нибудь?

                    Only users with full accounts can post comments. Log in, please.